mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
607 lines
19 KiB
JavaScript
607 lines
19 KiB
JavaScript
import { expect, it } from "bun:test";
|
|
import net from "node:net";
|
|
|
|
it("handles trailing headers split across packets", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
socket.write("5\r\nHello\r\n");
|
|
socket.write("7\r\n, world\r\n");
|
|
socket.write("0\r\n");
|
|
socket.uncork();
|
|
setTimeout(() => {
|
|
socket.write("X-Trail: ok\r\n");
|
|
socket.write('X-Quoted: "quoted value with \\"escapes\\""\r\n\r\n');
|
|
socket.end();
|
|
}, 10);
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello, world");
|
|
});
|
|
|
|
it("handles trailing headers in a single packet", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
socket.write("5\r\nHello\r\n");
|
|
socket.write("0\r\n");
|
|
socket.write("X-Trail: ok\r\n\r\n");
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello");
|
|
});
|
|
|
|
it("handles trailing headers with empty body", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
socket.write("0\r\n");
|
|
socket.write("X-Trail: ok\r\n\r\n");
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("");
|
|
});
|
|
|
|
it("handles multiple trailing headers", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
socket.write("5\r\nHello\r\n");
|
|
socket.write("0\r\n");
|
|
socket.write("X-Trail1: value1\r\n");
|
|
socket.write("X-Trail2: value2\r\n");
|
|
socket.write("X-Trail3: value3\r\n\r\n");
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello");
|
|
});
|
|
|
|
it("handles trailing headers with very long delay", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
socket.write("5\r\nHello\r\n");
|
|
socket.write("0\r\n");
|
|
socket.uncork();
|
|
setTimeout(() => {
|
|
socket.write("X-Trail: ok\r\n\r\n");
|
|
socket.end();
|
|
}, 100);
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello");
|
|
});
|
|
|
|
it("handles trailing headers with byte-by-byte transmission", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
socket.write("5\r\nHello\r\n");
|
|
socket.write("0\r\n");
|
|
socket.uncork();
|
|
|
|
const trailer = "X-Trail: ok\r\n\r\n";
|
|
let i = 0;
|
|
|
|
function writeNextByte() {
|
|
if (i < trailer.length) {
|
|
socket.write(trailer[i]);
|
|
i++;
|
|
setTimeout(writeNextByte, 5);
|
|
} else {
|
|
socket.end();
|
|
}
|
|
}
|
|
|
|
setTimeout(writeNextByte, 10);
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello");
|
|
});
|
|
|
|
it("handles trailing headers with malformed format (missing final CRLF)", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
socket.write("5\r\nHello\r\n");
|
|
socket.write("0\r\n");
|
|
socket.write("X-Trail: ok\r\n"); // Missing final CRLF
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello");
|
|
});
|
|
|
|
it("handles trailing headers with extremely large values", async () => {
|
|
const largeValue = "x".repeat(16384); // 16KB value
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
socket.write("5\r\nHello\r\n");
|
|
socket.write("0\r\n");
|
|
socket.write(`X-Large-Trail: ${largeValue}\r\n\r\n`);
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello");
|
|
});
|
|
|
|
it("handles connection close during trailing headers", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
socket.write("5\r\nHello\r\n");
|
|
socket.write("0\r\n");
|
|
socket.write("X-Trail: partial\r\n");
|
|
socket.end(); // Close connection abruptly
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello");
|
|
});
|
|
|
|
it("handles trailing headers with multiple header lines", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
socket.write("5\r\nHello\r\n");
|
|
socket.write("0\r\n");
|
|
socket.write("X-Trail-1: value1\r\n");
|
|
socket.write("X-Trail-2: value2\r\n");
|
|
socket.write("X-Trail-3: value3\r\n\r\n");
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello");
|
|
});
|
|
|
|
it("handles trailing headers with empty values", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
socket.write("5\r\nHello\r\n");
|
|
socket.write("0\r\n");
|
|
socket.write("X-Empty-Trail: \r\n\r\n");
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello");
|
|
});
|
|
|
|
it("handles delayed trailing headers", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
socket.write("5\r\nHello\r\n");
|
|
socket.write("0\r\n");
|
|
|
|
// Simulate delay before sending trailing headers
|
|
setTimeout(() => {
|
|
socket.write("X-Delayed-Trail: value\r\n\r\n");
|
|
socket.end();
|
|
}, 100);
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello");
|
|
});
|
|
|
|
it("handles trailing headers after the final chunk only", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
|
|
// First chunk
|
|
socket.write("5\r\nHello\r\n");
|
|
|
|
// Second chunk
|
|
socket.write("5\r\nWorld\r\n");
|
|
|
|
// Final chunk with trailing headers
|
|
socket.write("0\r\n");
|
|
socket.write("X-Final-Trail: final\r\n\r\n");
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("HelloWorld");
|
|
});
|
|
|
|
it("handles chunked extensions with empty extension", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
|
|
// Chunk with empty extension
|
|
socket.write("5;\r\nHello\r\n");
|
|
socket.write("0\r\n\r\n");
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello");
|
|
});
|
|
|
|
it("handles chunked extensions with simple key", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
|
|
// Chunk with simple extension
|
|
socket.write("5;foo\r\nHello\r\n");
|
|
socket.write("0\r\n\r\n");
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello");
|
|
});
|
|
|
|
it("handles chunked extensions with key-value pair", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
|
|
// Chunk with key-value extension
|
|
socket.write("5;foo=bar\r\nHello\r\n");
|
|
socket.write("0\r\n\r\n");
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello");
|
|
});
|
|
|
|
it("handles chunked extensions with quoted value", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
|
|
// Chunk with quoted value extension
|
|
socket.write('5;foo="bar baz"\r\nHello\r\n');
|
|
socket.write("0\r\n\r\n");
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello");
|
|
});
|
|
|
|
it("handles chunked extensions on multiple chunks", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
|
|
// First chunk with extension
|
|
socket.write("5;ext=1\r\nHello\r\n");
|
|
|
|
// Second chunk with different extension
|
|
socket.write("5;ext=2\r\nWorld\r\n");
|
|
|
|
// Final chunk with extension
|
|
socket.write("0;ext=final\r\n\r\n");
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("HelloWorld");
|
|
});
|
|
|
|
it("handles chunked extensions with trailing headers", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
|
|
// Chunks with extensions
|
|
socket.write("5;ext=first\r\nHello\r\n");
|
|
socket.write("5;ext=second\r\nWorld\r\n");
|
|
|
|
// Final chunk with extension and trailing headers
|
|
socket.write("0;ext=final\r\n");
|
|
socket.write("X-Trailer: value\r\n\r\n");
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("HelloWorld");
|
|
});
|
|
|
|
it("handles chunked extensions with special characters", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
|
|
// Extension with special characters in quoted value
|
|
socket.write('5;ext="!@#$%^&*()"\r\nHello\r\n');
|
|
socket.write("0\r\n\r\n");
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
const address = await promise;
|
|
const res = await fetch(`http://localhost:${address.port}`);
|
|
expect(res.status).toBe(200);
|
|
expect(await res.text()).toBe("Hello");
|
|
});
|
|
|
|
it("proper error if missing zero-length chunk", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
|
|
// Valid chunk
|
|
socket.write("5\r\nHello\r\n");
|
|
|
|
// End the connection abruptly
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
try {
|
|
const address = await promise;
|
|
const response = await fetch(`http://localhost:${address.port}`);
|
|
expect(response.status).toBe(200);
|
|
await response.text();
|
|
expect.unreachable();
|
|
} catch (e) {
|
|
expect(e?.code).toBe("ECONNRESET");
|
|
}
|
|
});
|
|
it("proper error if missing data in middle of chunk extension", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
|
|
// Valid chunk
|
|
socket.write("5\r\nHello\r\n");
|
|
|
|
// Malformed chunk - missing CRLF after extension
|
|
socket.write("5;ext=foo");
|
|
|
|
// End the connection abruptly
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
try {
|
|
const address = await promise;
|
|
await fetch(`http://localhost:${address.port}`).then(res => res.text());
|
|
expect.unreachable();
|
|
} catch (e) {
|
|
expect(e?.code).toBe("ECONNRESET");
|
|
}
|
|
});
|
|
|
|
it("proper error if missing CRLF after chunk data", async () => {
|
|
const { promise, resolve } = Promise.withResolvers();
|
|
await using server = net
|
|
.createServer(socket => {
|
|
socket.write("HTTP/1.1 200 OK\r\n");
|
|
socket.write("Content-Type: text/plain\r\n");
|
|
socket.write("Transfer-Encoding: chunked\r\n");
|
|
socket.write("\r\n");
|
|
|
|
// Valid chunk
|
|
socket.write("5\r\nHello\r\n");
|
|
|
|
// Malformed chunk - missing CRLF after chunk data
|
|
socket.write("5\r\nWorldX");
|
|
|
|
// End the connection abruptly
|
|
socket.end();
|
|
})
|
|
.listen(0, "localhost", () => {
|
|
resolve(server.address());
|
|
});
|
|
|
|
try {
|
|
const address = await promise;
|
|
await fetch(`http://localhost:${address.port}`).then(res => res.text());
|
|
expect.unreachable();
|
|
} catch (e) {
|
|
expect(e?.code).toBe("InvalidHTTPResponse");
|
|
}
|
|
});
|