// Serving-boundary framing headers (WP18 / decision-log 87). // // Every assertion here reads a CLIENT SOCKET, never a handler's return value. // That is the whole point: at a route's return a broken and a fixed response are // indistinguishable - both hold a valid `Response` object with a readable body. // Only delivery discriminates, so a test on the return value passes on broken // code. // // The fixture provider compresses with a real `CompressionStream("gzip")`. No // test in this repository had ever done that, which is why nothing caught the // break: Deno's `fetch` decompresses transparently but KEEPS `Content-Encoding` // and the COMPRESSED `Content-Length`, so any rebuild of that body hands the // client a lie. // // These live in tests/integration rather than beside middleware.ts because they // need a real `Deno.serve`, a real `fetch` client and a raw TCP framing read. // No test under packages/core opens a socket; the unit tests there are pure. import { assert, assertEquals } from "@std/assert"; import { serveDir } from "@std/http/file-server"; import { applyMiddleware, makeRequestLogger, } from "../../packages/core/src/middleware.ts"; import { ProviderClient } from "../../packages/providers/src/client.ts"; // --------------------------------------------------------------------------- // fixture helpers // --------------------------------------------------------------------------- type Bytes = Uint8Array; function gzipStream(bytes: Bytes): ReadableStream { // CompressionStream's writable takes BufferSource, so the source must too. const source = new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); }, }); return source.pipeThrough(new CompressionStream("gzip")); } async function drain(stream: ReadableStream): Promise { const parts: Uint8Array[] = []; for await (const chunk of stream) parts.push(chunk); const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); let offset = 0; for (const p of parts) { out.set(p, offset); offset += p.length; } return out; } function gzipBytes(bytes: Bytes): Promise { return drain(gzipStream(bytes)); } /** Serves `handler` through the real middleware chain, exactly as main.ts * composes it: applyMiddleware + makeRequestLogger. The sink is a no-op so the * suite output stays readable; console noise is unavoidable (the logger prints * unconditionally) and harmless. */ function serveThroughChain(handler: (req: Request) => Promise) { const composed = applyMiddleware(handler, [makeRequestLogger(() => {})]); const server = Deno.serve( { port: 0, onListen: () => {} }, (req) => composed(req), ); const port = (server.addr as Deno.NetAddr).port; return { server, url: `http://127.0.0.1:${port}` }; } /** Reads the raw HTTP response head off a socket, so framing (chunked vs a * declared Content-Length) is observed on the wire rather than inferred from * what `fetch` chose to expose. */ async function wireHead(url: string, path = "/"): Promise { const u = new URL(path, url); const conn = await Deno.connect({ hostname: u.hostname, port: Number(u.port), }); try { await conn.write( new TextEncoder().encode( `GET ${u.pathname} HTTP/1.1\r\nHost: ${u.host}\r\n` + `Accept-Encoding: identity\r\nConnection: close\r\n\r\n`, ), ); const decoder = new TextDecoder(); const buf = new Uint8Array(4096); let seen = ""; // One read is enough for a head on loopback, but loop until the blank line // appears so a split TCP segment cannot make this flaky. while (true) { const n = await conn.read(buf); if (n === null) break; seen += decoder.decode(buf.subarray(0, n), { stream: true }); if (seen.includes("\r\n\r\n")) break; } return seen.split("\r\n\r\n")[0]; } finally { try { conn.close(); } catch { // already closed by Connection: close } } } function assertFramingHeadersAbsent(res: Response, label: string): void { assertEquals( res.headers.get("content-encoding"), null, `${label}: content-encoding must not survive a rebuild`, ); assertEquals( res.headers.get("content-length"), null, `${label}: a provider content-length describes the ENCODED bytes`, ); // `transfer-encoding` cannot be asserted absent at a client: for an // unknown-length body the header IS the framing, and Deno.serve mints its own // `chunked` downstream of this middleware. Measured: the gateway forwards only // content-type/date/x-request-id, so the provider's value is genuinely dropped // and what remains is the runtime's. The observable invariant is therefore // "never a provider value", checked here and on the raw wire below. const te = res.headers.get("transfer-encoding"); assert( te === null || te === "chunked", `${label}: transfer-encoding must be the runtime's framing, got ${te}`, ); } /** Socket-level framing proof: no encoding claim, no declared length, and * exactly one framing header on the wire. */ async function assertWireIsCleanChunked( url: string, path: string, label: string, ): Promise { const lines = (await wireHead(url, path)).toLowerCase().split("\r\n"); assertEquals( lines.filter((l) => l.startsWith("content-encoding:")), [], `${label}: no encoding claim on the wire`, ); assertEquals( lines.filter((l) => l.startsWith("content-length:")), [], `${label}: no declared length on the wire`, ); assertEquals( lines.filter((l) => l.startsWith("transfer-encoding:")), ["transfer-encoding: chunked"], `${label}: exactly one framing mechanism, minted by the runtime`, ); } // --------------------------------------------------------------------------- // D1-T22(a) - the un-buffered surface (TTS shape: gzip, chunked, no CL) // --------------------------------------------------------------------------- Deno.test("D1-T22(a) gzipped un-buffered provider body (TTS shape) is delivered decompressed", async () => { // 120 000 bytes of pseudo-audio, so the compressed size differs by orders of // magnitude from the plaintext and a length mix-up cannot pass by accident. const audio = new Uint8Array(120_000); for (let i = 0; i < audio.length; i++) audio[i] = (i * 31 + 7) & 0xff; const provider = Deno.serve( { port: 0, onListen: () => {} }, () => new Response(gzipStream(audio), { headers: { // No content-length: this is the surface the gateway never buffers. "content-encoding": "gzip", "content-type": "audio/mpeg", }, }), ); const providerUrl = `http://127.0.0.1:${ (provider.addr as Deno.NetAddr).port }`; const { server, url } = serveThroughChain(async (_req) => { // A plain fetch handing the provider Response straight back, the shape // OpenAIAdapter.rawProxy produces (through the client's guarded fetch): the // Response reaches the middleware intact and is broken only by the rebuild // there. return await fetch(providerUrl); }); try { const res = await fetch(url); const body = new Uint8Array(await res.arrayBuffer()); assertEquals(res.status, 200); assertEquals( body.length, audio.length, "client must receive the uncompressed original", ); assertEquals(body, audio, "client bytes must be byte-identical"); assertEquals(res.headers.get("content-type"), "audio/mpeg"); assertFramingHeadersAbsent(res, "T22(a)"); assert(res.headers.get("x-request-id"), "the logger still stamps its id"); await assertWireIsCleanChunked(url, "/", "T22(a)"); } finally { await server.shutdown(); await provider.shutdown(); } }); // --------------------------------------------------------------------------- // D1-T22(b) - the buffered surface (image shape: gzip + compressed CL) // --------------------------------------------------------------------------- Deno.test("D1-T22(b) gzipped JSON with a compressed content-length is delivered whole", async () => { const payload = JSON.stringify({ created: 1_700_000_000, data: [ { b64_json: "Q".repeat(40_000) }, { b64_json: "Z".repeat(40_000) }, ], }); const plain = new TextEncoder().encode(payload); const compressed = await gzipBytes(plain); // The precondition of the whole defect: the declared length is the compressed // one, and it is far shorter than the body the client must actually read. assert( compressed.length < plain.length / 2, `fixture must actually compress (plain=${plain.length} gz=${compressed.length})`, ); const provider = Deno.serve( { port: 0, onListen: () => {} }, () => new Response(gzipStream(plain), { headers: { "content-encoding": "gzip", "content-length": String(compressed.length), "content-type": "application/json", "openai-organization": "org-frosty", "x-ratelimit-remaining-requests": "4999", "x-ratelimit-reset-requests": "6ms", }, }), ); const providerUrl = `http://127.0.0.1:${ (provider.addr as Deno.NetAddr).port }`; const { server, url } = serveThroughChain(async (_req) => { return await fetch(providerUrl); }); try { const res = await fetch(url); const text = await res.text(); const json = JSON.parse(text) as { data: unknown[] }; assertEquals(res.status, 200); assertEquals( text.length, payload.length, "full JSON, not a truncated read", ); assertEquals(json.data.length, 2); assertFramingHeadersAbsent(res, "T22(b)"); // The denylist is exactly three entries: every other provider header, // including the ones clients meter against, must still arrive. assertEquals(res.headers.get("openai-organization"), "org-frosty"); assertEquals(res.headers.get("x-ratelimit-remaining-requests"), "4999"); assertEquals(res.headers.get("x-ratelimit-reset-requests"), "6ms"); assertEquals(res.headers.get("content-type"), "application/json"); await assertWireIsCleanChunked(url, "/", "T22(b)"); } finally { await server.shutdown(); await provider.shutdown(); } }); // --------------------------------------------------------------------------- // D1-T31 - the second attach point, and the accepted static-asset regression // --------------------------------------------------------------------------- Deno.test("D1-T31 a ProviderClient.withBody response (stale CE + compressed CL) is delivered intact", async () => { // Not a simulated shape: this drives the real ProviderClient, whose // guardNonStreamBody -> withBody (client.ts:171-180) rebuilds the body while // copying the provider's headers verbatim. requestTimeoutMs is passed // explicitly so the guard is armed regardless of the ambient // FROSTY_HTTP_TIMEOUT_MS, matching the production default. const payload = JSON.stringify({ object: "list", data: [{ embedding: Array.from({ length: 1536 }, (_, i) => i / 1536) }], }); const plain = new TextEncoder().encode(payload); const compressed = await gzipBytes(plain); const provider = Deno.serve( { port: 0, onListen: () => {} }, () => new Response(gzipStream(plain), { headers: { "content-encoding": "gzip", "content-length": String(compressed.length), "content-type": "application/json", "openai-organization": "org-frosty", }, }), ); const providerUrl = `http://127.0.0.1:${ (provider.addr as Deno.NetAddr).port }`; const client = new ProviderClient({ requestTimeoutMs: 30_000 }); let attached: { ce: string | null; cl: string | null; mutable: boolean } = { ce: null, cl: null, mutable: false, }; const { server, url } = serveThroughChain(async (_req) => { const upstream = await client.fetchWithRetry(providerUrl); // Record what withBody handed us, to prove the corruption attached two // layers below the middleware. Mutable headers are the tell: a fetch() // Response is immutable, so a mutable one is a rebuild. let mutable = false; try { upstream.headers.set("x-frosty-probe", "1"); upstream.headers.delete("x-frosty-probe"); mutable = true; } catch { mutable = false; } attached = { ce: upstream.headers.get("content-encoding"), cl: upstream.headers.get("content-length"), mutable, }; return upstream; }); try { const res = await fetch(url); const text = await res.text(); assertEquals(attached.ce, "gzip", "withBody copies the stale encoding"); assertEquals( attached.cl, String(compressed.length), "withBody copies the COMPRESSED length over a decompressed body", ); assert( attached.mutable, "mutable headers prove this is withBody's object, not the fetch Response", ); assertEquals(res.status, 200); assertEquals(text.length, payload.length); assertEquals( (JSON.parse(text) as { data: { embedding: number[] }[] }).data[0] .embedding.length, 1536, ); assertFramingHeadersAbsent(res, "T31"); assertEquals(res.headers.get("openai-organization"), "org-frosty"); await assertWireIsCleanChunked(url, "/", "T31"); } finally { client.close(); await server.shutdown(); await provider.shutdown(); } }); Deno.test("D1-T31 accepted regression: a serveDir static asset keeps its bytes and moves to chunked framing", async () => { // Real serveDir over a real file, not a simulated known-length stream: this is // the one response class that legitimately carries a correct Content-Length. const root = await Deno.makeTempDir(); const asset = "S".repeat(50_000); await Deno.writeTextFile(`${root}/asset.txt`, asset); const { server, url } = serveThroughChain((req) => serveDir(req, { fsRoot: root, quiet: true }) ); try { const res = await fetch(`${url}/asset.txt`); const text = await res.text(); // The accepted cost, asserted so it is a decision and not a surprise. assertEquals(text.length, asset.length, "body bytes are identical"); assertEquals(text, asset); assertEquals( res.headers.get("content-length"), null, "accepted: static assets lose their declared length", ); // Framing observed on the wire, not inferred: the asset moves from a // declared length to chunked. await assertWireIsCleanChunked(url, "/asset.txt", "T31 static asset"); } finally { await server.shutdown(); await Deno.remove(root, { recursive: true }); } }); // --------------------------------------------------------------------------- // The three no-op cases. "Provably a no-op everywhere else" is the claim that // made this the chosen option, so it is asserted rather than argued. // --------------------------------------------------------------------------- Deno.test("no-op: a gateway-built JSON response keeps its content-length", async () => { const body = JSON.stringify({ pad: "p".repeat(5_000) }); const { server, url } = serveThroughChain(() => Promise.resolve( new Response(body, { headers: { "content-type": "application/json" } }), ) ); try { const res = await fetch(url); const text = await res.text(); assertEquals(text, body); // Deno.serve derives the length from the known-length body, so dropping the // header the gateway never set changes nothing observable. assertEquals(res.headers.get("content-length"), String(body.length)); const head = (await wireHead(url)).toLowerCase(); assert( head.includes(`content-length: ${body.length}`), `declared length must survive on the wire. head was:\n${head}`, ); } finally { await server.shutdown(); } }); Deno.test("no-op: Content-Range on a 206 survives", async () => { const root = await Deno.makeTempDir(); await Deno.writeTextFile(`${root}/asset.txt`, "R".repeat(50_000)); const { server, url } = serveThroughChain((req) => serveDir(req, { fsRoot: root, quiet: true }) ); try { const res = await fetch(`${url}/asset.txt`, { headers: { range: "bytes=0-99" }, }); const text = await res.text(); assertEquals(res.status, 206); assertEquals(text.length, 100); // Content-Range is NOT in the denylist: it describes the selection, not the // encoding, and the client needs it to place the bytes. assertEquals( res.headers.get("content-range"), "bytes 0-99/50000", ); } finally { await server.shutdown(); await Deno.remove(root, { recursive: true }); } }); Deno.test("no-op: a 304 is unaffected", async () => { const root = await Deno.makeTempDir(); await Deno.writeTextFile(`${root}/asset.txt`, "N".repeat(50_000)); const { server, url } = serveThroughChain((req) => serveDir(req, { fsRoot: root, quiet: true }) ); try { const first = await fetch(`${url}/asset.txt`); await first.body?.cancel(); const etag = first.headers.get("etag"); assert(etag, "serveDir must mint an etag for the conditional request"); const res = await fetch(`${url}/asset.txt`, { headers: { "if-none-match": etag }, }); await res.body?.cancel(); assertEquals(res.status, 304); assertEquals(res.headers.get("etag"), etag); // A 304 carries no body and no content-length either way, so there is // nothing for the denylist to remove. assertEquals(res.headers.get("content-length"), null); assert(res.headers.get("x-request-id"), "the logger still stamps its id"); } finally { await server.shutdown(); await Deno.remove(root, { recursive: true }); } });