Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
558 lines
23 KiB
C++
558 lines
23 KiB
C++
// Framing tests for the incrementally delivered request body used by
|
|
// POST /v1/audio/transcriptions/live.
|
|
//
|
|
// Driven over a real loopback socket against serve_http rather than against the
|
|
// streambuf directly: the behaviour under test is the interaction between the
|
|
// header parser, the undrained socket and the handler, and a direct unit test of
|
|
// the buffer would not cover the part most likely to break.
|
|
//
|
|
// The cases that matter are the ones where a wrong answer is INVISIBLE. A body
|
|
// truncated by a stall, a disconnect or a desynchronised sender must surface as
|
|
// an error, because for audio a silently short body is indistinguishable from a
|
|
// speaker who simply stopped talking.
|
|
//
|
|
// Scope, stated so nobody mistakes this for end-to-end coverage: it links
|
|
// http.cpp and not the server runtime, so it exercises the framing, the bounds
|
|
// and the isolation of other routes — and NOT the `/v1/audio/transcriptions/live`
|
|
// route itself, its query-parameter defaults, the PCM adapter, the model
|
|
// invocation, or the conversion of a body error into an SSE `error` event. Those
|
|
// need a loaded model, are not covered anywhere in this suite, and were verified
|
|
// by hand against a running server. Removing the route would not fail this test.
|
|
|
|
#include "../../app/server/http.h"
|
|
#if defined(AUDIOCPP_TEST_PARALLEL_HTTP)
|
|
#include "../../app/server/parallel_http.h"
|
|
#endif
|
|
|
|
#include "test_assert.h"
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <csignal>
|
|
#include <iostream>
|
|
#include <istream>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#ifdef _WIN32
|
|
#include <winsock2.h>
|
|
#include <ws2tcpip.h>
|
|
#else
|
|
#include <arpa/inet.h>
|
|
#include <netinet/in.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
using engine::test::require;
|
|
using engine::test::require_eq;
|
|
|
|
constexpr int kPort = 18094;
|
|
constexpr const char * kLivePath = "/v1/audio/transcriptions/live";
|
|
|
|
// The socket API's handle type is not `int` on Windows, and narrowing it there
|
|
// truncates a valid handle.
|
|
#ifdef _WIN32
|
|
using socket_t = SOCKET;
|
|
constexpr socket_t kInvalidSocket = INVALID_SOCKET;
|
|
#else
|
|
using socket_t = int;
|
|
constexpr socket_t kInvalidSocket = -1;
|
|
#endif
|
|
|
|
std::atomic<bool> g_stop{false};
|
|
bool stop_requested() {
|
|
return g_stop.load();
|
|
}
|
|
|
|
// Reports what the body stream actually yielded, so a test can tell a complete
|
|
// body from a truncated one and a truncation from an error.
|
|
class EchoHandler final : public minitts::server::IHttpHandler {
|
|
public:
|
|
minitts::server::HttpResponse handle(const minitts::server::HttpRequest & request) override {
|
|
if (request.body_stream == nullptr) {
|
|
return minitts::server::json_response(
|
|
"{\"stream\":false,\"buffered\":" + std::to_string(request.body.size()) + "}");
|
|
}
|
|
std::string body;
|
|
try {
|
|
char buffer[4096];
|
|
while (request.body_stream->read(buffer, sizeof(buffer)) || request.body_stream->gcount() > 0) {
|
|
body.append(buffer, static_cast<size_t>(request.body_stream->gcount()));
|
|
}
|
|
} catch (const std::exception & ex) {
|
|
return minitts::server::json_response(
|
|
std::string("{\"error\":\"") + ex.what() + "\"}");
|
|
}
|
|
size_t sum = 0;
|
|
for (const unsigned char byte : body) {
|
|
sum += byte;
|
|
}
|
|
return minitts::server::json_response(
|
|
"{\"stream\":true,\"bytes\":" + std::to_string(body.size()) +
|
|
",\"sum\":" + std::to_string(sum) + "}");
|
|
}
|
|
|
|
// Stands in for the server's per-model resolution: a request naming
|
|
// `model=tight` gets tightened bounds, everything else the defaults. The real
|
|
// handler reads them from config; what matters here is that the transport asks,
|
|
// and that what it is told actually takes effect.
|
|
minitts::server::LiveIngestLimits live_ingest_limits(
|
|
const minitts::server::HttpRequest & request) const override {
|
|
limits_queries.fetch_add(1);
|
|
if (request.query.find("model=nocap") != std::string::npos) {
|
|
// Not reachable through config, which rejects it — but the transport must
|
|
// not depend on config having done so.
|
|
minitts::server::LiveIngestLimits nocap;
|
|
nocap.max_chunk_bytes = 0;
|
|
return nocap;
|
|
}
|
|
if (request.query.find("model=tiny") != std::string::npos) {
|
|
// Small enough that a single hex digit exceeds it, which is where the
|
|
// chunk-size guard used to underflow.
|
|
minitts::server::LiveIngestLimits tiny;
|
|
tiny.max_chunk_bytes = 1;
|
|
return tiny;
|
|
}
|
|
if (request.query.find("model=tight") == std::string::npos) {
|
|
return {};
|
|
}
|
|
minitts::server::LiveIngestLimits tight;
|
|
tight.max_chunk_bytes = 1024;
|
|
tight.max_body_bytes = 4096;
|
|
return tight;
|
|
}
|
|
|
|
mutable std::atomic<int> limits_queries{0};
|
|
};
|
|
|
|
void close_socket(socket_t handle) {
|
|
#ifdef _WIN32
|
|
closesocket(handle);
|
|
#else
|
|
close(handle);
|
|
#endif
|
|
}
|
|
|
|
// Retries rather than trusting a fixed startup sleep: the listener becomes ready
|
|
// on its own thread, and a sleep long enough to always win is either flaky or
|
|
// wasteful.
|
|
socket_t connect_to_server() {
|
|
sockaddr_in address{};
|
|
address.sin_family = AF_INET;
|
|
address.sin_port = htons(static_cast<uint16_t>(kPort));
|
|
inet_pton(AF_INET, "127.0.0.1", &address.sin_addr);
|
|
for (int attempt = 0; attempt < 200; ++attempt) {
|
|
const socket_t handle = socket(AF_INET, SOCK_STREAM, 0);
|
|
require(handle != kInvalidSocket, "could not create client socket");
|
|
if (connect(handle, reinterpret_cast<sockaddr *>(&address), sizeof(address)) == 0) {
|
|
return handle;
|
|
}
|
|
close_socket(handle);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(25));
|
|
}
|
|
require(false, "could not connect to the test server");
|
|
return kInvalidSocket;
|
|
}
|
|
|
|
void send_raw(socket_t handle, const std::string & data) {
|
|
size_t offset = 0;
|
|
while (offset < data.size()) {
|
|
#ifdef _WIN32
|
|
const int written = send(handle, data.data() + offset, static_cast<int>(data.size() - offset), 0);
|
|
#else
|
|
const ssize_t written = send(handle, data.data() + offset, data.size() - offset, 0);
|
|
#endif
|
|
require(written > 0, "send to the test server failed");
|
|
offset += static_cast<size_t>(written);
|
|
}
|
|
}
|
|
|
|
std::string read_reply(socket_t handle) {
|
|
std::string reply;
|
|
char buffer[4096];
|
|
for (;;) {
|
|
#ifdef _WIN32
|
|
const int received = recv(handle, buffer, sizeof(buffer), 0);
|
|
#else
|
|
const ssize_t received = recv(handle, buffer, sizeof(buffer), 0);
|
|
#endif
|
|
if (received <= 0) {
|
|
break;
|
|
}
|
|
reply.append(buffer, static_cast<size_t>(received));
|
|
}
|
|
return reply;
|
|
}
|
|
|
|
std::string headers_for(const char * path, bool chunked) {
|
|
std::string out = std::string("POST ") + path + " HTTP/1.1\r\nHost: test\r\n";
|
|
out += chunked ? "Transfer-Encoding: chunked\r\n" : "Content-Length: 0\r\n";
|
|
return out + "\r\n";
|
|
}
|
|
|
|
// Same, with a verbatim Transfer-Encoding value, for testing how the header is parsed.
|
|
std::string headers_with_encoding(const char * path, const std::string & encoding) {
|
|
return std::string("POST ") + path + " HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: " +
|
|
encoding + "\r\n\r\n";
|
|
}
|
|
|
|
// Sends `body` as one chunk per entry and returns the server's reply.
|
|
std::string round_trip(const std::vector<std::string> & chunks, bool terminate = true,
|
|
const char * path = kLivePath, const std::string & raw_tail = {}) {
|
|
const socket_t handle = connect_to_server();
|
|
send_raw(handle, headers_for(path, true));
|
|
for (const auto & piece : chunks) {
|
|
std::ostringstream frame;
|
|
frame << std::hex << piece.size() << "\r\n" << piece << "\r\n";
|
|
send_raw(handle, frame.str());
|
|
}
|
|
if (!raw_tail.empty()) {
|
|
send_raw(handle, raw_tail);
|
|
}
|
|
if (terminate) {
|
|
send_raw(handle, "0\r\n\r\n");
|
|
} else {
|
|
#ifdef _WIN32
|
|
shutdown(handle, SD_SEND);
|
|
#else
|
|
shutdown(handle, SHUT_WR);
|
|
#endif
|
|
}
|
|
const std::string reply = read_reply(handle);
|
|
close_socket(handle);
|
|
return reply;
|
|
}
|
|
|
|
bool contains(const std::string & haystack, const std::string & needle) {
|
|
return haystack.find(needle) != std::string::npos;
|
|
}
|
|
|
|
std::string payload(size_t length, unsigned char seed) {
|
|
std::string out;
|
|
out.reserve(length);
|
|
for (size_t i = 0; i < length; ++i) {
|
|
out.push_back(static_cast<char>((seed + i) % 251));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
size_t byte_sum(const std::string & data) {
|
|
size_t sum = 0;
|
|
for (const unsigned char byte : data) {
|
|
sum += byte;
|
|
}
|
|
return sum;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
#if defined(AUDIOCPP_TEST_PARALLEL_HTTP) && defined(SIGPIPE)
|
|
std::signal(SIGPIPE, SIG_IGN);
|
|
#endif
|
|
#ifdef _WIN32
|
|
WSADATA wsa;
|
|
WSAStartup(MAKEWORD(2, 2), &wsa);
|
|
#endif
|
|
EchoHandler handler;
|
|
// The server default, spelled out rather than pulled from config.h so this test
|
|
// keeps linking the transport alone. It bounds a Content-Length body and never
|
|
// the incremental one under test, which has its own LiveIngestLimits.
|
|
constexpr uint64_t kMaxBufferedBody = 2ull * 1024ull * 1024ull * 1024ull;
|
|
std::thread server([&] {
|
|
#if defined(AUDIOCPP_TEST_PARALLEL_HTTP)
|
|
minitts::server::serve_parallel_http("127.0.0.1", kPort, handler, stop_requested, kMaxBufferedBody);
|
|
#else
|
|
minitts::server::serve_http("127.0.0.1", kPort, handler, stop_requested, kMaxBufferedBody);
|
|
#endif
|
|
});
|
|
try {
|
|
// No startup sleep: connect_to_server() retries until the listener is up.
|
|
|
|
// A body split into several chunks must arrive byte-identical and complete.
|
|
{
|
|
const std::string a = payload(5000, 7);
|
|
const std::string b = payload(3000, 61);
|
|
const std::string reply = round_trip({a, b});
|
|
require(contains(reply, "\"stream\":true"), "live path should expose a body stream");
|
|
require(
|
|
contains(reply, "\"bytes\":" + std::to_string(a.size() + b.size())),
|
|
"multi-chunk body should arrive complete: " + reply);
|
|
require(
|
|
contains(reply, "\"sum\":" + std::to_string(byte_sum(a) + byte_sum(b))),
|
|
"multi-chunk body should arrive byte-identical: " + reply);
|
|
}
|
|
|
|
// A single chunk far larger than the 8 KiB receive buffer, so the chunk is
|
|
// assembled across many recv() calls rather than found whole in one.
|
|
{
|
|
const std::string big = payload(300'000, 11);
|
|
const std::string reply = round_trip({big});
|
|
require(contains(reply, "\"bytes\":" + std::to_string(big.size())),
|
|
"a chunk spanning many receives should arrive complete: " + reply);
|
|
require(contains(reply, "\"sum\":" + std::to_string(byte_sum(big))),
|
|
"a chunk spanning many receives should arrive intact: " + reply);
|
|
}
|
|
|
|
// Drives total consumed bytes past the 64 KiB mark at which the de-framer
|
|
// compacts its pending buffer. Compaction shifts the bytes the reader has not
|
|
// consumed yet, so a mistake here silently corrupts or drops audio rather than
|
|
// failing loudly — and it cannot happen at all in the small-payload cases.
|
|
{
|
|
std::vector<std::string> chunks;
|
|
size_t total = 0;
|
|
size_t sum = 0;
|
|
for (int i = 0; i < 80; ++i) {
|
|
chunks.push_back(payload(4096, static_cast<unsigned char>(i * 7)));
|
|
total += chunks.back().size();
|
|
sum += byte_sum(chunks.back());
|
|
}
|
|
require(total > 4 * 64 * 1024, "compaction case must exceed the compaction threshold");
|
|
const std::string reply = round_trip(chunks);
|
|
require(contains(reply, "\"bytes\":" + std::to_string(total)),
|
|
"body spanning several compactions should be complete: " + reply);
|
|
require(contains(reply, "\"sum\":" + std::to_string(sum)),
|
|
"compaction must not corrupt or drop unconsumed bytes: " + reply);
|
|
}
|
|
|
|
// Many small chunks exercise the path where several chunks arrive inside a
|
|
// single recv().
|
|
{
|
|
std::vector<std::string> chunks;
|
|
size_t total = 0;
|
|
size_t sum = 0;
|
|
for (int i = 0; i < 40; ++i) {
|
|
chunks.push_back(payload(97, static_cast<unsigned char>(i)));
|
|
total += chunks.back().size();
|
|
sum += byte_sum(chunks.back());
|
|
}
|
|
const std::string reply = round_trip(chunks);
|
|
require(contains(reply, "\"bytes\":" + std::to_string(total)), "many small chunks: " + reply);
|
|
require(contains(reply, "\"sum\":" + std::to_string(sum)), "many small chunks intact: " + reply);
|
|
}
|
|
|
|
// Chunk extensions are legal and must be ignored rather than parsed as size.
|
|
{
|
|
const socket_t handle = connect_to_server();
|
|
send_raw(handle, headers_for(kLivePath, true));
|
|
send_raw(handle, "4;name=value\r\nabcd\r\n0\r\n\r\n");
|
|
const std::string reply = read_reply(handle);
|
|
close_socket(handle);
|
|
require(contains(reply, "\"bytes\":4"), "chunk extensions should be ignored: " + reply);
|
|
}
|
|
|
|
// Trailer headers after the terminating chunk are legal.
|
|
{
|
|
const socket_t handle = connect_to_server();
|
|
send_raw(handle, headers_for(kLivePath, true));
|
|
send_raw(handle, "4\r\nwxyz\r\n0\r\nX-Trailer: value\r\n\r\n");
|
|
const std::string reply = read_reply(handle);
|
|
close_socket(handle);
|
|
require(contains(reply, "\"bytes\":4"), "trailers should be accepted: " + reply);
|
|
}
|
|
|
|
// Each malformed case asserts the SPECIFIC diagnostic, not merely that something
|
|
// failed. Half-closing the connection to end these requests would itself produce
|
|
// an error, so a bare "did it fail" check would pass even with the guard removed
|
|
// — it would just be failing later, for the wrong reason.
|
|
|
|
// A peer that closes without the terminating chunk must NOT look like a clean
|
|
// end of body — that is the silent-truncation failure this endpoint cannot have.
|
|
{
|
|
const std::string reply = round_trip({payload(2000, 3)}, /*terminate=*/false);
|
|
require(contains(reply, "closed before the terminating chunk"),
|
|
"premature close must be an error, not a short body: " + reply);
|
|
}
|
|
|
|
// A declared size that would overflow size_t must be rejected before any
|
|
// arithmetic on it, not wrap into a tiny length.
|
|
{
|
|
const std::string reply = round_trip({}, /*terminate=*/false, kLivePath, "ffffffffffffffff\r\nX");
|
|
require(contains(reply, "chunk size exceeds the maximum"),
|
|
"overflowing chunk size must be rejected by the size guard: " + reply);
|
|
}
|
|
|
|
// A chunk larger than the per-chunk cap must be rejected by its declared size,
|
|
// before the data behind it is waited for.
|
|
{
|
|
const std::string reply = round_trip({}, /*terminate=*/false, kLivePath, "1000000\r\n");
|
|
require(contains(reply, "chunk size exceeds the maximum"),
|
|
"oversized chunk must be rejected by the size guard: " + reply);
|
|
}
|
|
|
|
// Chunk data must be CRLF-terminated; otherwise a desynchronised sender's
|
|
// payload can be silently reinterpreted as framing.
|
|
{
|
|
const std::string reply = round_trip({}, /*terminate=*/false, kLivePath, "4\r\nabcdXX0\r\n\r\n");
|
|
require(contains(reply, "not terminated by CRLF"),
|
|
"chunk not CRLF-terminated must be rejected: " + reply);
|
|
}
|
|
|
|
// A non-hex size is a protocol error rather than an empty body.
|
|
{
|
|
const std::string reply = round_trip({}, /*terminate=*/false, kLivePath, "zz\r\n");
|
|
require(contains(reply, "invalid chunk size"),
|
|
"invalid chunk size must be rejected: " + reply);
|
|
}
|
|
|
|
// The header names a transfer-coding, so it must be matched as a token rather
|
|
// than searched for as a substring. "notchunked" is not chunked, and a chain
|
|
// like "gzip, chunked" is chunked framing around bytes this server cannot
|
|
// decode — de-framing that and handing the result to the PCM decoder would
|
|
// turn compressed data into confident nonsense. Both must decline the
|
|
// incremental path rather than accept it.
|
|
for (const std::string encoding : {"notchunked", "chunked-garbage", "gzip, chunked"}) {
|
|
const socket_t handle = connect_to_server();
|
|
send_raw(handle, headers_with_encoding(kLivePath, encoding));
|
|
send_raw(handle, "4\r\nabcd\r\n0\r\n\r\n");
|
|
const std::string reply = read_reply(handle);
|
|
close_socket(handle);
|
|
require(contains(reply, "\"stream\":false"),
|
|
"Transfer-Encoding \"" + encoding + "\" must not select the incremental path: " + reply);
|
|
}
|
|
|
|
// The same unsupported chain split across two header lines must be rejected the
|
|
// same way. Repeated field lines mean one comma-separated list, so a parser that
|
|
// overwrites instead of combining sees a bare "chunked" and accepts what it just
|
|
// rejected on a single line.
|
|
{
|
|
const socket_t handle = connect_to_server();
|
|
send_raw(handle,
|
|
std::string("POST ") + kLivePath +
|
|
" HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: gzip\r\n"
|
|
"Transfer-Encoding: chunked\r\n\r\n");
|
|
send_raw(handle, "4\r\nabcd\r\n0\r\n\r\n");
|
|
const std::string reply = read_reply(handle);
|
|
close_socket(handle);
|
|
require(contains(reply, "\"stream\":false"),
|
|
"a transfer-coding chain split across header lines must not select the "
|
|
"incremental path: " + reply);
|
|
}
|
|
|
|
// A chunk header line past the cap must be rejected, including when its
|
|
// terminator arrives in the same receive as the bytes that exceed it.
|
|
{
|
|
const std::string reply =
|
|
round_trip({}, /*terminate=*/false, kLivePath, std::string(9000, 'a') + ";x\r\n");
|
|
require(contains(reply, "oversized chunk header"),
|
|
"an over-long chunk header line must be rejected: " + reply);
|
|
}
|
|
|
|
// Every other endpoint must be untouched: a chunked request elsewhere still
|
|
// goes through the ordinary buffered path, with no body stream published.
|
|
{
|
|
const socket_t handle = connect_to_server();
|
|
send_raw(handle, headers_for("/v1/audio/speech", true));
|
|
send_raw(handle, "4\r\nabcd\r\n0\r\n\r\n");
|
|
const std::string reply = read_reply(handle);
|
|
close_socket(handle);
|
|
require(
|
|
contains(reply, "\"stream\":false"),
|
|
"only the live endpoint may consume an incremental body: " + reply);
|
|
// Current upstream de-frames non-live chunked bodies before dispatch.
|
|
// Both listeners must return the complete four-byte buffered payload.
|
|
require(
|
|
contains(reply, "\"buffered\":4"),
|
|
"chunked bodies on other routes must keep their existing handling: " + reply);
|
|
}
|
|
|
|
// Limits are resolved per request, not compiled in. The same 2000-byte chunk is
|
|
// fine under the defaults and rejected once the handler tightens max_chunk_bytes
|
|
// for this request — which is the whole point of the per-model override.
|
|
{
|
|
const std::string chunk = payload(2000, 3);
|
|
const std::string relaxed = round_trip({chunk}, true, kLivePath);
|
|
require(contains(relaxed, "\"bytes\":2000"),
|
|
"a 2000-byte chunk is within the default per-chunk cap: " + relaxed);
|
|
|
|
const std::string tightened =
|
|
round_trip({chunk}, true, "/v1/audio/transcriptions/live?model=tight");
|
|
require(contains(tightened, "chunk size exceeds the maximum"),
|
|
"a handler-supplied per-chunk cap must be enforced: " + tightened);
|
|
}
|
|
|
|
// Same for the whole-body cap, which is a separate axis: each chunk is legal on
|
|
// its own and only their total crosses the bound.
|
|
{
|
|
const std::vector<std::string> chunks(6, payload(1000, 11));
|
|
const std::string relaxed = round_trip(chunks, true, kLivePath);
|
|
require(contains(relaxed, "\"bytes\":6000"),
|
|
"6000 bytes is within the default body cap: " + relaxed);
|
|
|
|
const std::string tightened =
|
|
round_trip(chunks, true, "/v1/audio/transcriptions/live?model=tight");
|
|
require(contains(tightened, "exceeded its maximum size"),
|
|
"a handler-supplied body cap must be enforced: " + tightened);
|
|
}
|
|
|
|
// A cap smaller than one hex digit's value must still reject. `cap - value`
|
|
// underflows for any cap below the digit, so at cap=1 a declared `f` used to be
|
|
// accepted outright — a 15-byte chunk against a 1-byte bound.
|
|
{
|
|
const std::string reply =
|
|
round_trip({}, /*terminate=*/false, "/v1/audio/transcriptions/live?model=tiny",
|
|
"f\r\n123456789012345\r\n");
|
|
require(contains(reply, "chunk size exceeds the maximum"),
|
|
"a chunk cap below the digit value must not underflow: " + reply);
|
|
}
|
|
|
|
// The body cap must count bytes that arrived alongside the headers. A body short
|
|
// enough to fit in the first receive never calls receive_more(), so a cap checked
|
|
// only there is skipped by exactly the request most likely to be probing it.
|
|
{
|
|
std::string request = headers_for("/v1/audio/transcriptions/live?model=tight", true);
|
|
for (int i = 0; i < 6; ++i) {
|
|
std::ostringstream frame;
|
|
frame << std::hex << 1000 << "\r\n" << payload(1000, 23) << "\r\n";
|
|
request += frame.str();
|
|
}
|
|
request += "0\r\n\r\n";
|
|
// One write, so headers and the whole 6000-byte body land in a single recv().
|
|
const socket_t handle = connect_to_server();
|
|
send_raw(handle, request);
|
|
const std::string reply = read_reply(handle);
|
|
close_socket(handle);
|
|
require(contains(reply, "exceeded its maximum size"),
|
|
"a fully prefetched body must still be measured against the cap: " + reply);
|
|
}
|
|
|
|
// A zero per-chunk cap must not disable the overflow guard. Config rejects a 0,
|
|
// but a Limits built in code could still carry one, and this parse is the last
|
|
// thing between a declared SIZE_MAX and `size + 2` wrapping to 1.
|
|
{
|
|
const std::string reply =
|
|
round_trip({}, /*terminate=*/false, "/v1/audio/transcriptions/live?model=nocap",
|
|
"ffffffffffffffff\r\n");
|
|
require(contains(reply, "chunk size exceeds the maximum"),
|
|
"a zero max_chunk_bytes must fall back to a real cap, not disable it: " + reply);
|
|
}
|
|
|
|
// Asked for only when the request actually opts into an incremental body: every
|
|
// other route must not pay for a lookup it cannot use.
|
|
{
|
|
const int before = handler.limits_queries.load();
|
|
const socket_t handle = connect_to_server();
|
|
send_raw(handle, headers_for("/v1/audio/speech", true));
|
|
send_raw(handle, "4\r\nabcd\r\n0\r\n\r\n");
|
|
(void) read_reply(handle);
|
|
close_socket(handle);
|
|
require_eq(
|
|
handler.limits_queries.load(),
|
|
before,
|
|
"live-ingest limits must not be resolved for a non-live route");
|
|
}
|
|
|
|
} catch (const std::exception & error) {
|
|
std::cerr << "framing failure: " << error.what() << '\n';
|
|
g_stop.store(true); server.join(); return 1;
|
|
}
|
|
g_stop.store(true);
|
|
server.join();
|
|
std::cout << "http_live_body_test: all cases passed\n";
|
|
return 0;
|
|
}
|