import type { Socket } from "bun"; import { setSocketOptions } from "bun:internal-for-testing"; import { describe, expect, test } from "bun:test"; import { bunEnv, bunExe, isPosix } from "harness"; describe.if(isPosix)("HTTP server handles chunked transfer encoding", () => { test("handles fragmented chunk terminators", async () => { const script = ` const server = Bun.serve({ port: 0, async fetch(req) { const body = await req.text(); return new Response("Got: " + body); }, }); const { promise, resolve } = Promise.withResolvers(); const socket = await Bun.connect({ hostname: "localhost", port: server.port, socket: { data(socket, data) { console.log(data.toString()); socket.end(); }, open(socket) { socket.write("POST / HTTP/1.1\\r\\nHost: localhost\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n4\\r\\nWiki\\r"); socket.flush(); setTimeout(() => { socket.write("\\n0\\r\\n\\r\\n"); socket.flush(); }, 50); }, error() {}, close() { resolve(); }, }, }); await promise; server.stop(); `; await using proc = Bun.spawn({ cmd: [bunExe(), "-e", script], env: bunEnv, stdout: "pipe", stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([ new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited, ]); expect(stdout).toContain("200 OK"); expect(stdout).toContain("Got: Wiki"); expect(exitCode).toBe(0); }); test("rejects invalid terminator in fragmented reads", async () => { const script = ` const server = Bun.serve({ port: 0, async fetch(req) { const body = await req.text(); return new Response("Got: " + body); }, }); const { promise, resolve } = Promise.withResolvers(); const socket = await Bun.connect({ hostname: "localhost", port: server.port, socket: { data(socket, data) { console.log(data.toString()); socket.end(); }, open(socket) { socket.write("POST / HTTP/1.1\\r\\nHost: localhost\\r\\nTransfer-Encoding: chunked\\r\\n\\r\\n4\\r\\nTestX"); socket.flush(); setTimeout(() => { socket.write("\\n0\\r\\n\\r\\n"); socket.flush(); }, 50); }, error() {}, close() { resolve(); }, }, }); await promise; server.stop(); `; await using proc = Bun.spawn({ cmd: [bunExe(), "-e", script], env: bunEnv, stdout: "pipe", stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([ new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited, ]); expect(stdout).toContain("400"); expect(exitCode).toBe(0); }); }); describe.if(isPosix)("HTTP server handles fragmented requests", () => { test("handles requests with tiny send buffer (regression test)", async () => { using server = Bun.serve({ hostname: "localhost", port: 0, async fetch(req) { const body = await req.text(); const headers: Record = {}; req.headers.forEach((value, key) => { headers[key] = value; }); return new Response( JSON.stringify({ method: req.method, url: req.url, headers, body, }), { headers: { "Content-Type": "application/json" }, }, ); }, }); const { port } = server; let remaining = 100; const batchSize = 10; for (let i = 0; i < remaining; i += batchSize) { const promises: Promise[] = []; for (let j = 0; j < batchSize; j++) { promises.push( (async i => { const { resolve: resolveClose, reject: rejectClose, promise: closePromise } = Promise.withResolvers(); let buffer: Buffer; function actuallyWrite(socket) { while (buffer.length > 0) { const written = socket.write(buffer.slice(0, 1)); if (written == 0) break; if (written > 1) { throw new Error(`Written ${written} bytes, expected 1`); } socket.flush(); buffer = buffer.slice(written); } } let remainingRequests = 20; const socket = await Bun.connect({ hostname: server.hostname, port: server.port!, socket: { open(socket: Socket) { // Set a very small send buffer to force fragmentation // This simulates the condition that triggered the bug setSocketOptions(socket, 1, 1); // 1 = send buffer, 1 = size const input = `GET /test-${i} HTTP/1.1\r\nHost: ${server.hostname}:${port}\r\nUser-Agent: Bun-Test\r\nAccept: */*\r\n\r\n`; const repeated = Buffer.alloc(input.length * remainingRequests, input); buffer = repeated; actuallyWrite(socket); }, data(socket: Socket, data: Buffer) { // Mini HTTP parser to count complete responses const dataStr = data.toString(); const responses = dataStr.split("\r\n\r\n"); // Count complete responses (those that have both headers and body) for (let k = 0; k < responses.length - 1; k++) { if (responses[k].includes("HTTP/1.1 200 OK")) { remainingRequests--; } } if (remainingRequests == 0) { socket.end(); } }, close() { if (remainingRequests > 0) { throw new Error(`Expected 20 responses, got ${20 - remainingRequests}`); } resolveClose(); }, drain(socket: Socket) { actuallyWrite(socket); }, error(_socket: Socket, error: Error) { rejectClose(error); }, }, }); // Wait for the socket to close await closePromise; })(i), ); } await Promise.all(promises); } server.stop(); }); });