Files
bun.sh/test/js/bun/http/http-server-chunking.test.ts
robobun 8f61adf494 Harden chunked encoding parser (#26594)
## Summary
- Improve handling of fragmented chunk data in the HTTP parser
- Add test coverage for edge cases

## Test plan
- [x] New tests pass
- [x] Existing tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 01:18:39 -08:00

224 lines
6.6 KiB
TypeScript

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<string, string> = {};
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<void>[] = [];
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();
});
});