Files
bun.sh/test/js/node/http/node-http.test.ts
2025-07-16 23:07:23 -07:00

3353 lines
98 KiB
TypeScript

/**
* All new tests in this file should also run in Node.js.
*
* Do not add any tests that only run in Bun.
*
* A handful of older tests do not run in Node in this file. These tests should be updated to run in Node, or deleted.
*/
import { bunEnv, bunExe, tls as COMMON_TLS_CERT, randomPort } from "harness";
import { createTest } from "node-harness";
import { spawnSync } from "node:child_process";
import { EventEmitter, once } from "node:events";
import nodefs, { unlinkSync } from "node:fs";
import http, {
Agent,
createServer,
get,
globalAgent,
IncomingMessage,
OutgoingMessage,
request,
Server,
ServerResponse,
validateHeaderName,
validateHeaderValue,
} from "node:http";
import https, { createServer as createHttpsServer } from "node:https";
import type { AddressInfo } from "node:net";
import { connect, createConnection } from "node:net";
import { tmpdir } from "node:os";
import * as path from "node:path";
import * as stream from "node:stream";
import { PassThrough } from "node:stream";
import * as zlib from "node:zlib";
import { run as runHTTPProxyTest } from "./node-http-proxy.js";
const { describe, expect, it, beforeAll, afterAll, createDoneDotAll, mock, test } = createTest(import.meta.path);
function listen(server: Server, protocol: string = "http"): Promise<URL> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject("Timed out"), 5000).unref();
server.listen({ port: 0 }, (err, hostname, port) => {
clearTimeout(timeout);
if (err) {
reject(err);
} else {
resolve(new URL(`${protocol}://${hostname}:${port}`));
}
});
});
}
describe("node:http", () => {
describe("createServer", async () => {
it("hello world", async () => {
try {
var server = createServer((req, res) => {
expect(req.url).toBe("/hello?world");
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello World");
});
const url = await listen(server);
const res = await fetch(new URL("/hello?world", url));
expect(await res.text()).toBe("Hello World");
} catch (e) {
throw e;
} finally {
server.close();
}
});
it("is not marked encrypted (#5867)", async () => {
try {
var server = createServer((req, res) => {
expect(req.connection.encrypted).toBe(false);
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello World");
});
const url = await listen(server);
const res = await fetch(new URL("", url));
expect(await res.text()).toBe("Hello World");
} catch (e) {
throw e;
} finally {
server.close();
}
});
it("request & response body streaming (large)", async () => {
const input = Buffer.alloc("hello world, hello world".length * 9000, "hello world, hello world");
try {
var server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
req.on("data", chunk => {
res.write(chunk);
});
req.on("end", () => {
res.end();
});
});
const url = await listen(server);
const res = await fetch(url, {
method: "POST",
body: input,
});
const out = await res.text();
expect(out).toBe(input.toString());
} finally {
server.close();
}
});
it("request & response body streaming (small)", async () => {
try {
const bodyBlob = new Blob(["hello world", "hello world".repeat(4)]);
const input = await bodyBlob.text();
var server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
req.on("data", chunk => {
res.write(chunk);
});
req.on("end", () => {
res.end();
});
});
const url = await listen(server);
const res = await fetch(url, {
method: "POST",
body: bodyBlob,
});
const out = await res.text();
expect(out).toBe(input);
} finally {
server.close();
}
});
it("listen should return server", async () => {
const server = createServer();
const listenResponse = server.listen(0);
expect(listenResponse instanceof Server).toBe(true);
expect(listenResponse).toBe(server);
listenResponse.close();
});
it("listen callback should be bound to server", async () => {
const server = createServer();
const { resolve, reject, promise } = Promise.withResolvers();
server.listen(0, function () {
try {
expect(this === server).toBeTrue();
resolve();
} catch (e) {
reject();
}
});
await promise;
server.close();
});
it("should use the provided port", async () => {
while (true) {
try {
const server = http.createServer(() => {});
const random_port = randomPort();
server.listen(random_port);
await once(server, "listening");
const { port } = server.address();
expect(port).toEqual(random_port);
server.close();
break;
} catch (err) {
// Address in use try another port
if (err.code === "EADDRINUSE") {
continue;
}
throw err;
}
}
});
it("should assign a random port when undefined", async () => {
const server1 = http.createServer(() => {});
const server2 = http.createServer(() => {});
server1.listen(undefined);
server2.listen(undefined);
const { port: port1 } = server1.address();
const { port: port2 } = server2.address();
expect(port1).not.toEqual(port2);
expect(port1).toBeWithin(1024, 65535);
server1.close();
server2.close();
});
it("option method should be uppercase (#7250)", async () => {
try {
var server = createServer((req, res) => {
expect(req.method).toBe("OPTIONS");
res.writeHead(204, {});
res.end();
});
const url = await listen(server);
const res = await fetch(url, {
method: "OPTIONS",
});
expect(res.status).toBe(204);
} finally {
server.close();
}
});
});
describe("response", () => {
test("set-cookie works with getHeader", () => {
const res = new ServerResponse({});
res.setHeader("Set-Cookie", ["swag=true", "yolo=true"]);
expect(res.getHeader("Set-Cookie")).toEqual(["swag=true", "yolo=true"]);
});
test("set-cookie works with getHeaders", () => {
const res = new ServerResponse({});
res.setHeader("Set-Cookie", ["swag=true", "yolo=true"]);
res.setHeader("test", "test");
expect(res.getHeaders()).toEqual({
"set-cookie": ["swag=true", "yolo=true"],
"test": "test",
});
});
});
describe("request", () => {
function runTest(done: Function, callback: (server: Server, port: number, done: (err?: Error) => void) => void) {
let timer;
const server = createServer((req, res) => {
if (req.headers.__proto__ !== {}.__proto__) {
throw new Error("Headers should inherit from Object.prototype");
}
const reqUrl = new URL(req.url!, `http://${req.headers.host}`);
if (reqUrl.pathname) {
if (reqUrl.pathname === "/redirect") {
// Temporary redirect
res.writeHead(301, {
Location: `http://localhost:${server.port}/redirected`,
});
res.end("Got redirect!\n");
return;
}
if (reqUrl.pathname === "/multi-chunk-response") {
res.writeHead(200, { "Content-Type": "text/plain" });
const toWrite = "a".repeat(512);
for (let i = 0; i < 4; i++) {
res.write(toWrite);
}
res.end();
return;
}
if (reqUrl.pathname === "/multiple-set-cookie") {
expect(req.headers.cookie).toBe("foo=bar; bar=baz");
res.setHeader("Set-Cookie", ["foo=bar", "bar=baz"]);
res.end("OK");
return;
}
if (reqUrl.pathname === "/redirected") {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not Found");
return;
}
if (reqUrl.pathname === "/lowerCaseHeaders") {
res.writeHead(200, { "content-type": "text/plain", "X-Custom-Header": "custom_value" });
res.end("Hello World");
return;
}
if (reqUrl.pathname.includes("timeout")) {
if (timer) clearTimeout(timer);
req.on("timeout", () => {
console.log("req timeout");
});
res.on("timeout", () => {
console.log("res timeout");
});
timer = setTimeout(() => {
if (res.closed) {
return;
}
res.end("Hello World");
timer = null;
}, 3000).unref();
return;
}
if (reqUrl.pathname === "/pathTest") {
res.end("Path correct!\n");
return;
}
if (reqUrl.pathname === "/customWriteHead") {
function createWriteHead(prevWriteHead, listener) {
let fired = false;
return function writeHead() {
if (!fired) {
fired = true;
listener.call(this);
}
return prevWriteHead.apply(this, arguments);
};
}
function addPoweredBy() {
if (!this.getHeader("X-Powered-By")) {
this.setHeader("X-Powered-By", "Bun");
}
}
res.writeHead = createWriteHead(res.writeHead, addPoweredBy);
res.setHeader("Content-Type", "text/plain");
res.end("Hello World");
return;
}
if (reqUrl.pathname === "/uploadFile") {
let requestData = Buffer.alloc(0);
req.on("data", chunk => {
requestData = Buffer.concat([requestData, chunk]);
});
req.on("end", () => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.write(requestData);
res.end();
});
return;
}
}
res.writeHead(200, { "Content-Type": "text/plain" });
if (req.headers["x-test"]) {
res.write(`x-test: ${req.headers["x-test"]}\n`);
}
// Check for body
if (req.method === "OPTIONS") {
req.on("data", chunk => {
res.write(chunk);
});
req.on("end", () => {
res.write("OPTIONS\n");
res.end("Hello World");
});
} else if (req.method === "POST") {
req.on("data", chunk => {
res.write(chunk);
});
req.on("end", () => {
res.write("POST\n");
res.end("Hello World");
});
} else {
if (req.headers["X-Test"] !== undefined) {
res.write(`X-Test: test\n`);
}
res.write("Maybe GET maybe not\n");
res.end("Hello World");
}
});
server.listen({ port: 0 }, (_, __, port) => {
var _done = (...args) => {
server.close();
done(...args);
};
callback(server, port, _done);
});
}
// test("check for expected fields", done => {
// runTest((server, port) => {
// const req = request({ host: "localhost", port, method: "GET" }, res => {
// console.log("called");
// res.on("end", () => {
// console.log("here");
// server.close();
// done();
// });
// res.on("error", err => {
// server.close();
// done(err);
// });
// });
// expect(req.path).toEqual("/");
// expect(req.method).toEqual("GET");
// expect(req.host).toEqual("localhost");
// expect(req.protocol).toEqual("http:");
// req.end();
// });
// });
it("should not insert extraneous accept-encoding header", async done => {
try {
let headers;
var server = createServer((req, res) => {
headers = req.headers;
req.on("data", () => {});
req.on("end", () => {
res.end();
});
});
const url = await listen(server);
await fetch(url, { decompress: false });
expect(headers["accept-encoding"]).toBeFalsy();
done();
} catch (e) {
done(e);
} finally {
server.close();
}
});
it("multiple Set-Cookie headers works #6810", done => {
runTest(done, (server, port, done) => {
const req = request(`http://localhost:${port}/multiple-set-cookie`, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(res.headers["set-cookie"]).toEqual(["foo=bar", "bar=baz"]);
done();
});
res.on("error", err => done(err));
});
req.setHeader("Cookie", ["foo=bar; bar=baz"]);
req.end();
});
});
it("should make a standard GET request when passed string as first arg", done => {
runTest(done, (server, port, done) => {
const req = request(`http://localhost:${port}`, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(data).toBe("Maybe GET maybe not\nHello World");
done();
});
res.on("error", err => done(err));
});
req.end();
});
});
it("should make a https:// GET request when passed string as first arg", done => {
const req = https.request("https://example.com", { headers: { "accept-encoding": "identity" } }, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(data).toContain("This domain is for use in illustrative examples in documents");
done();
});
res.on("error", err => done(err));
});
req.end();
});
it("should make a POST request when provided POST method, even without a body", done => {
runTest(done, (server, serverPort, done) => {
const req = request({ host: "localhost", port: serverPort, method: "POST" }, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(data).toBe("POST\nHello World");
done();
});
res.on("error", err => done(err));
});
req.end();
});
});
it("should correctly handle a POST request with a body", done => {
runTest(done, (server, port, done) => {
const req = request({ host: "localhost", port, method: "POST" }, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(data).toBe("Posting\nPOST\nHello World");
done();
});
res.on("error", err => done(err));
});
req.write("Posting\n");
req.end();
});
});
it("should noop request.setSocketKeepAlive without error", done => {
runTest(done, (server, port, done) => {
const req = request(`http://localhost:${port}`);
req.setSocketKeepAlive(true, 1000);
req.end();
expect(true).toBe(true);
// Neglecting to close this will cause a future test to fail.
req.on("close", () => done());
});
});
it("should allow us to set timeout with request.setTimeout or `timeout` in options", done => {
runTest(done, (server, serverPort, done) => {
const createDone = createDoneDotAll(done);
const req1Done = createDone();
const req2Done = createDone();
const req1 = request(
{
host: "localhost",
port: serverPort,
path: "/timeout",
timeout: 500,
},
res => {
req1Done(new Error("Should not have received response"));
},
);
req1.on("timeout", () => req1Done());
const req2 = request(
{
host: "localhost",
port: serverPort,
path: "/timeout",
},
res => {
req2Done(new Error("Should not have received response"));
},
);
req2.setTimeout(500, () => {
req2Done();
});
req1.end();
req2.end();
});
});
it("should correctly set path when path provided", done => {
runTest(done, (server, serverPort, done) => {
const createDone = createDoneDotAll(done);
const req1Done = createDone();
const req2Done = createDone();
const req1 = request(`http://localhost:${serverPort}/pathTest`, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(data).toBe("Path correct!\n");
req1Done();
});
res.on("error", err => req1Done(err));
});
const req2 = request(`http://localhost:${serverPort}`, { path: "/pathTest" }, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(data).toBe("Path correct!\n");
req2Done();
});
res.on("error", err => req2Done(err));
});
req1.end();
req2.end();
expect(req1.path).toBe("/pathTest");
expect(req2.path).toBe("/pathTest");
});
});
it("should emit response when response received", done => {
runTest(done, (server, serverPort, done) => {
const req = request(`http://localhost:${serverPort}`);
req.on("response", res => {
expect(res.statusCode).toBe(200);
done();
});
req.end();
});
});
// NOTE: Node http.request doesn't follow redirects by default
it("should handle redirects properly", done => {
runTest(done, (server, serverPort, done) => {
const req = request(`http://localhost:${serverPort}/redirect`, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(data).toBe("Got redirect!\n");
done();
});
res.on("error", err => done(err));
});
req.end();
});
});
it("should correctly attach headers to request", done => {
runTest(done, (server, serverPort, done) => {
const req = request({ host: "localhost", port: serverPort, headers: { "X-Test": "test" } }, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(data).toBe("x-test: test\nMaybe GET maybe not\nHello World");
done();
});
res.on("error", err => done(err));
});
expect(req.getHeader("X-Test")).toBe("test");
// node returns undefined
// Headers returns null
expect(req.getHeader("X-Not-Exists")).toBe(undefined);
req.end();
});
});
it("should correct casing of method param", done => {
runTest(done, (server, serverPort, done) => {
const req = request({ host: "localhost", port: serverPort, method: "get" }, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(data).toBe("Maybe GET maybe not\nHello World");
done();
});
res.on("error", err => done(err));
});
req.end();
});
});
it("should allow for port as a string", done => {
runTest(done, (server, serverPort, done) => {
const req = request({ host: "localhost", port: `${serverPort}`, method: "GET" }, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(data).toBe("Maybe GET maybe not\nHello World");
done();
});
res.on("error", err => done(err));
});
req.end();
});
});
it("should allow us to pass a URL object", done => {
runTest(done, (server, serverPort, done) => {
const req = request(new URL(`http://localhost:${serverPort}`), { method: "POST" }, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(data).toBe("Hello WorldPOST\nHello World");
done();
});
res.on("error", err => done(err));
});
req.write("Hello World");
req.end();
});
});
it("should ignore body when method is GET/HEAD", done => {
runTest(done, (server, serverPort, done) => {
const createDone = createDoneDotAll(done);
const methods = ["GET", "HEAD"];
const dones = {};
for (const method of methods) {
dones[method] = createDone();
}
for (const method of methods) {
const req = request(`http://localhost:${serverPort}`, { method }, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(data).toBe(method === "GET" ? "Maybe GET maybe not\nHello World" : "");
dones[method]();
});
res.on("error", err => dones[method](err));
});
req.write("BODY");
req.end();
}
});
});
it("should have a response body when method is OPTIONS", done => {
runTest(done, (server, serverPort, done) => {
const createDone = createDoneDotAll(done);
const methods = ["OPTIONS"]; //keep this logic to add more methods in future
const dones = {};
for (const method of methods) {
dones[method] = createDone();
}
for (const method of methods) {
const req = request(`http://localhost:${serverPort}`, { method }, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(data).toBe(method + "\nHello World");
dones[method]();
});
res.on("error", err => dones[method](err));
});
req.end();
}
});
});
it("should return response with lowercase headers", done => {
runTest(done, (server, serverPort, done) => {
const req = request(`http://localhost:${serverPort}/lowerCaseHeaders`, res => {
expect(res.headers["content-type"]).toBe("text/plain");
expect(res.headers["x-custom-header"]).toBe("custom_value");
done();
});
req.end();
});
});
it("reassign writeHead method, issue#3585", done => {
runTest(done, (server, serverPort, done) => {
const req = request(`http://localhost:${serverPort}/customWriteHead`, res => {
expect(res.headers["content-type"]).toBe("text/plain");
expect(res.headers["x-powered-by"]).toBe("Bun");
done();
});
req.end();
});
});
it("uploading file by 'formdata/multipart', issue#3116", done => {
runTest(done, (server, serverPort, done) => {
const boundary = "----FormBoundary" + Date.now();
const formDataBegin = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="myfile.txt"\r\nContent-Type: application/octet-stream\r\n\r\n`;
const fileData = Buffer.from("80818283", "hex");
const formDataEnd = `\r\n--${boundary}--`;
const requestOptions = {
hostname: "localhost",
port: serverPort,
path: "/uploadFile",
method: "POST",
headers: {
"Content-Type": `multipart/form-data; boundary=${boundary}`,
},
};
const req = request(requestOptions, res => {
let responseData = Buffer.alloc(0);
res.on("data", chunk => {
responseData = Buffer.concat([responseData, chunk]);
});
res.on("end", () => {
try {
expect(responseData).toEqual(
Buffer.concat([Buffer.from(formDataBegin), fileData, Buffer.from(formDataEnd)]),
);
} catch (e) {
return done(e);
}
done();
});
});
req.on("error", err => {
done(err);
});
req.write(formDataBegin); // string
req.write(fileData); // Buffer
req.write(formDataEnd); // string
req.end();
});
});
it("request via http proxy, issue#4295", async () => {
await runHTTPProxyTest();
});
it("should correctly stream a multi-chunk response #5320", async done => {
runTest(done, (server, serverPort, done) => {
const req = request({ host: "localhost", port: `${serverPort}`, path: "/multi-chunk-response", method: "GET" });
req.on("error", err => done(err));
req.on("response", async res => {
const body = res.pipe(new PassThrough({ highWaterMark: 512 }));
const response = new Response(body);
const text = await response.text();
expect(text.length).toBe(2048);
done();
});
req.end();
});
});
it("should emit a socket event when connecting", async done => {
runTest(done, async (server, serverPort, done) => {
const req = request(`http://localhost:${serverPort}`, {});
req.on("socket", function onRequestSocket(socket) {
req.destroy();
done();
});
req.end();
});
});
});
describe("https.request with custom tls options", () => {
const createServer = () =>
new Promise(resolve => {
const server = createHttpsServer(
{
key: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.key")),
cert: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.crt")),
rejectUnauthorized: true,
},
(req, res) => {
res.writeHead(200);
res.end("hello world");
},
);
listen(server, "https").then(url => {
resolve({
server,
close: () => server.close(),
url,
});
});
});
it("supports custom tls args", async done => {
const { url, close } = await createServer();
try {
const options: https.RequestOptions = {
method: "GET",
url,
port: url.port,
ca: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost_ca.pem")),
};
const req = https.request(options, res => {
res.on("data", () => null);
res.on("end", () => {
close();
done();
});
});
req.on("error", error => {
close();
done(error);
});
req.end();
} catch (e) {
close();
throw e;
}
});
});
describe("signal", () => {
it("should abort and close the server", done => {
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello World");
});
const interval = setTimeout(() => {
server.close();
done();
}, 100);
const signal = AbortSignal.timeout(30);
signal.addEventListener("abort", () => {
clearTimeout(interval);
expect(true).toBe(true);
done();
});
server.listen({ signal, port: 0 });
});
});
describe("get", () => {
it("should make a standard GET request, like request", async done => {
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello World");
});
const url = await listen(server);
get(url, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(data).toBe("Hello World");
server.close();
done();
});
res.on("error", err => {
server.close();
done(err);
});
});
});
});
describe("Agent", () => {
let dummyAgent;
beforeAll(() => {
dummyAgent = new Agent();
});
it("should be a class", () => {
expect(Agent instanceof Function).toBe(true);
});
it("can be constructed with new", () => {
expect(new Agent().protocol).toBe("http:");
});
it("can be constructed with apply", () => {
expect(Agent.apply({}).protocol).toBe("http:");
});
it("should have a default maxSockets of Infinity", () => {
expect(dummyAgent.maxSockets).toBe(Infinity);
});
it("should have a keepAlive value", () => {
expect(dummyAgent.keepAlive).toBe(false);
});
it("should noop keepSocketAlive", () => {
const agent = new Agent({ keepAlive: true });
// @ts-ignore
expect(agent.keepAlive).toBe(true);
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello World");
agent.keepSocketAlive(request({ host: "localhost", port: server.address().port, method: "GET" }));
server.end();
});
});
it("should provide globalAgent", () => {
expect(globalAgent instanceof Agent).toBe(true);
});
});
describe("ClientRequest.signal", () => {
it("should attempt to make a standard GET request and abort", async () => {
let server_port;
let server_host;
const {
resolve: resolveClientAbort,
reject: rejectClientAbort,
promise: promiseClientAbort,
} = Promise.withResolvers();
const server = createServer((req, res) => {});
server.listen({ port: 0 }, (_err, host, port) => {
server_port = port;
server_host = host;
const signal = AbortSignal.timeout(5);
get(`http://${server_host}:${server_port}`, { signal }, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
server.close();
});
}).once("abort", () => {
resolveClientAbort();
});
});
await promiseClientAbort;
server.close();
});
});
test("validateHeaderName", () => {
validateHeaderName("Foo");
expect(() => validateHeaderName("foo:")).toThrow();
expect(() => validateHeaderName("foo:bar")).toThrow();
});
test("validateHeaderValue", () => {
validateHeaderValue("Foo", "Bar");
expect(() => validateHeaderValue("Foo", undefined as any)).toThrow();
expect(() => validateHeaderValue("Foo", "Bar\r")).toThrow();
});
test("req.req = req", done => {
const server = createServer((req, res) => {
req.req = req;
res.write(req.req === req ? "ok" : "fail");
res.end();
});
server.listen({ port: 0 }, async (_err, host, port) => {
try {
const x = await fetch(`http://${host}:${port}`).then(res => res.text());
expect(x).toBe("ok");
done();
} catch (error) {
done(error);
} finally {
server.close();
}
});
});
test("test unix socket server", done => {
const socketPath = `${tmpdir()}/bun-server-${Math.random().toString(32)}.sock`;
const server = createServer((req, res) => {
expect(req.method).toStrictEqual("GET");
expect(req.url).toStrictEqual("/bun?a=1");
res.writeHead(200, {
"Content-Type": "text/plain",
"Connection": "close",
});
res.write("Bun\n");
res.end();
});
server.listen(socketPath, () => {
// TODO: unix socket is not implemented in fetch.
const output = spawnSync("curl", ["--unix-socket", socketPath, "http://localhost/bun?a=1"]);
try {
expect(output.stdout.toString()).toStrictEqual("Bun\n");
done();
} catch (err) {
done(err);
} finally {
server.close();
}
});
});
test("should not decompress gzip, issue#4397", async () => {
const { promise, resolve } = Promise.withResolvers();
https
.request("https://bun.sh/", { headers: { "accept-encoding": "gzip" } }, res => {
res.on("data", function cb(chunk) {
resolve(chunk);
res.off("data", cb);
});
})
.end();
const chunk = await promise;
expect(chunk.toString()).not.toContain("<html");
});
test("should listen on port if string, issue#4582", done => {
const server = createServer((req, res) => {
res.end();
});
server.listen({ port: "0" }, async (_err, host, port) => {
try {
await fetch(`http://${host}:${port}`).then(res => {
expect(res.status).toBe(200);
done();
});
} catch (err) {
done(err);
} finally {
server.close();
}
});
});
test("error event not fired, issue#4651", async () => {
const { promise, resolve } = Promise.withResolvers();
const server = createServer((req, res) => {
res.end();
});
server.listen({ port: 0 }, () => {
const server2 = createServer((_, res) => {
res.end();
});
server2.on("error", err => {
resolve(err);
});
server2.listen({ port: server.address().port }, () => {});
});
const err = await promise;
expect(err.code).toBe("EADDRINUSE");
});
});
describe("node https server", async () => {
const httpsOptions = {
key: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "cert.key")),
cert: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "cert.pem")),
};
const createServer = onRequest => {
return new Promise(resolve => {
const server = createHttpsServer(httpsOptions, (req, res) => {
onRequest(req, res);
});
listen(server, "https").then(url => {
resolve({
server,
done: () => server.close(),
url,
});
});
});
};
it("is marked encrypted (#5867)", async () => {
const { server, url, done } = await createServer(async (req, res) => {
expect(req.connection.encrypted).toBe(true);
res.end();
});
try {
await fetch(url, { tls: { rejectUnauthorized: false } });
} catch (e) {
throw e;
} finally {
done();
}
});
});
describe("server.address should be valid IP", () => {
it("should return null before listening", done => {
const server = createServer((req, res) => {});
try {
expect(server.address()).toBeNull();
done();
} catch (err) {
done(err);
}
});
it("should return null after close", done => {
const server = createServer((req, res) => {});
server.listen(0, async (_err, host, port) => {
try {
expect(server.address()).not.toBeNull();
server.close();
expect(server.address()).toBeNull();
done();
} catch (err) {
done(err);
}
});
});
it("test default hostname, issue#5850", done => {
const server = createServer((req, res) => {});
server.listen(0, async (_err, host, port) => {
try {
const { address, family, port } = server.address();
expect(port).toBeInteger();
expect(port).toBeGreaterThan(0);
expect(port).toBeLessThan(65536);
expect(["::", "0.0.0.0"]).toContain(address);
if (address === "0.0.0.0") {
expect(family).toStrictEqual("IPv4");
} else {
expect(family).toStrictEqual("IPv6");
}
done();
} catch (err) {
done(err);
} finally {
server.close();
}
});
});
it.each([["localhost"], ["127.0.0.1"]])("test %s", (hostname, done) => {
const server = createServer((req, res) => {});
server.listen(0, hostname, async (_err, host, port) => {
try {
const { address, family } = server.address();
expect(port).toBeInteger();
expect(port).toBeGreaterThan(0);
expect(port).toBeLessThan(65536);
expect(["IPv4", "IPv6"]).toContain(family);
if (family === "IPv4") {
expect(address).toStrictEqual("127.0.0.1");
} else {
expect(address).toStrictEqual("::1");
}
done();
} catch (err) {
done(err);
} finally {
server.close();
}
});
});
it("test unix socket, issue#6413", done => {
const socketPath = `${tmpdir()}/bun-server-${Math.random().toString(32)}.sock`;
const server = createServer((req, res) => {});
server.listen(socketPath, async (_err, host, port) => {
try {
expect(server.address()).toStrictEqual(socketPath);
done();
} catch (err) {
done(err);
} finally {
server.close();
unlinkSync(socketPath);
}
});
});
test("ServerResponse init", done => {
try {
const req = {};
const res = new ServerResponse(req);
expect(res.req).toBe(req);
done();
} catch (err) {
done(err);
}
});
test("ServerResponse instanceof OutgoingMessage", () => {
expect(new ServerResponse({}) instanceof OutgoingMessage).toBe(true);
});
test("ServerResponse assign assignSocket", async done => {
const createDone = createDoneDotAll(done);
const doneRequest = createDone();
const waitSocket = createDone();
const doneSocket = createDone();
try {
const socket = new EventEmitter();
const res = new ServerResponse({});
res.once("socket", socket => {
expect(socket).toBe(socket);
waitSocket();
});
res.once("close", () => {
doneRequest();
});
res.assignSocket(socket);
await Bun.sleep(10);
expect(res.socket).toBe(socket);
expect(socket._httpMessage).toBe(res);
expect(() => res.assignSocket(socket)).toThrow("Socket already assigned");
socket.emit("close");
doneSocket();
} catch (err) {
doneRequest(err);
}
});
});
it("should not accept untrusted certificates", async () => {
const server = https.createServer(
{
key: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl.key")),
cert: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl.crt")),
passphrase: "123123123",
},
(req, res) => {
res.write("Hello from https server");
res.end();
},
);
server.listen(0, "127.0.0.1");
const address = server.address();
try {
let url_address = address.address;
if (address.family === "IPv6") {
url_address = `[${url_address}]`;
}
const res = await fetch(`https://${url_address}:${address.port}`, {
tls: {
rejectUnauthorized: true,
},
});
await res.text();
expect(true).toBe("unreacheable");
} catch (err) {
expect(err.code).toBe("UNABLE_TO_VERIFY_LEAF_SIGNATURE");
expect(err.message).toBe("unable to verify the first certificate");
}
server.close();
});
it("#4415.1 ServerResponse es6", () => {
class Response extends ServerResponse {
constructor(req) {
super(req);
}
}
const req = {};
const res = new Response(req);
expect(res.req).toBe(req);
});
it("#4415.2 ServerResponse es5", () => {
function Response(req) {
ServerResponse.call(this, req);
}
Response.prototype = Object.create(ServerResponse.prototype);
const req = {};
const res = new Response(req);
expect(res.req).toBe(req);
});
it("#4415.3 Server es5", done => {
const server = Server((req, res) => {
res.end();
});
server.listen(0, async (_err, host, port) => {
try {
const res = await fetch(`http://localhost:${port}`);
expect(res.status).toBe(200);
done();
} catch (err) {
done(err);
} finally {
server.close();
}
});
});
it("#4415.4 IncomingMessage es5", done => {
// This matches Node.js:
const im = Object.create(IncomingMessage.prototype);
IncomingMessage.call(im, { url: "/foo" });
expect(im.url).toBe("");
let didCall = false;
function Subclass(...args) {
IncomingMessage.apply(this, args);
didCall = true;
}
Object.setPrototypeOf(Subclass.prototype, IncomingMessage.prototype);
Object.setPrototypeOf(Subclass, IncomingMessage);
const server = new Server(
{
IncomingMessage: Subclass,
},
(req, res) => {
if (req instanceof Subclass && didCall) {
expect(req.url).toBe("/foo");
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("hello");
} else {
res.writeHead(500, { "Content-Type": "text/plain" });
res.end("bye");
}
},
);
server.listen(0, () => {
fetch(`http://localhost:${server.address().port}/foo`, {
method: "GET",
}).then(response => {
expect(response.status).toBe(200);
server.close(done);
});
});
});
it("#9242.1 Server has constructor", () => {
const s = new Server();
expect(s.constructor).toBe(Server);
});
it("#9242.2 IncomingMessage has constructor", () => {
const im = new IncomingMessage("http://localhost");
expect(im.constructor).toBe(IncomingMessage);
});
it("#9242.3 OutgoingMessage has constructor", () => {
const om = new OutgoingMessage();
expect(om.constructor).toBe(OutgoingMessage);
});
it("#9242.4 ServerResponse has constructor", () => {
const sr = new ServerResponse({});
expect(sr.constructor).toBe(ServerResponse);
});
// Windows doesnt support SIGUSR1
if (process.platform !== "win32") {
// By not timing out, this test passes.
test(".unref() works", async () => {
expect([path.join(import.meta.dir, "node-http-ref-fixture.js")]).toRun();
});
}
it("#10177 response.write with non-ascii latin1 should not cause duplicated character or segfault", () => {
// this can cause a segfault so we run it in a separate process
const { exitCode } = Bun.spawnSync({
cmd: [bunExe(), "run", path.join(import.meta.dir, "node-http-response-write-encode-fixture.js")],
env: bunEnv,
stdout: "inherit",
stderr: "inherit",
});
expect(exitCode).toBe(0);
}, 60_000);
it("#11425 http no payload limit", done => {
const server = Server((req, res) => {
res.end();
});
server.listen(0, async (_err, host, port) => {
try {
const res = await fetch(`http://localhost:${port}`, {
method: "POST",
body: new Uint8Array(1024 * 1024 * 200),
});
expect(res.status).toBe(200);
done();
} catch (err) {
done(err);
} finally {
server.close();
}
});
});
it("should emit events in the right order", async () => {
const { stdout, exited } = Bun.spawn({
cmd: [bunExe(), "run", path.join(import.meta.dir, "fixtures/log-events.mjs")],
stdout: "pipe",
stdin: "ignore",
stderr: "inherit",
env: bunEnv,
});
const out = await stdout.text();
// TODO prefinish and socket are not emitted in the right order
expect(
out
.split("\n")
.filter(Boolean)
.map(x => JSON.parse(x)),
).toStrictEqual([
["req", "socket"],
["req", "prefinish"],
["req", "finish"],
["req", "response"],
"STATUS: 200",
// TODO: not totally right:
["res", "resume"],
["req", "close"],
["res", "readable"],
["res", "end"],
["res", "close"],
]);
expect(await exited).toBe(0);
});
it("destroy should end download", async () => {
// just simulate some file that will take forever to download
const payload = Buffer.alloc(128 * 1024, "X");
for (let i = 0; i < 5; i++) {
let sendedByteLength = 0;
using server = Bun.serve({
port: 0,
async fetch(req) {
let running = true;
req.signal.onabort = () => (running = false);
return new Response(async function* () {
while (running) {
sendedByteLength += payload.byteLength;
yield payload;
await Bun.sleep(10);
}
});
},
});
async function run() {
let receivedByteLength = 0;
let { promise, resolve } = Promise.withResolvers();
const req = request(server.url, res => {
res.on("data", data => {
receivedByteLength += data.length;
if (resolve) {
resolve();
resolve = null;
}
});
});
req.end();
await promise;
req.destroy();
await Bun.sleep(10);
const initialByteLength = receivedByteLength;
// we should receive the same amount of data we sent
expect(initialByteLength).toBeLessThanOrEqual(sendedByteLength);
await Bun.sleep(10);
// we should not receive more data after destroy
expect(initialByteLength).toBe(receivedByteLength);
await Bun.sleep(10);
}
const runCount = 50;
const runs = Array.from({ length: runCount }, run);
await Promise.all(runs);
Bun.gc(true);
await Bun.sleep(10);
}
});
it("can send brotli from Server and receive with fetch", async () => {
try {
var server = createServer((req, res) => {
expect(req.url).toBe("/hello");
res.writeHead(200);
res.setHeader("content-encoding", "br");
const inputStream = new stream.Readable();
inputStream.push("Hello World");
inputStream.push(null);
inputStream.pipe(zlib.createBrotliCompress()).pipe(res);
});
const url = await listen(server);
const res = await fetch(new URL("/hello", url));
expect(await res.text()).toBe("Hello World");
} catch (e) {
throw e;
} finally {
server.close();
}
});
it("can send gzip from Server and receive with fetch", async () => {
try {
var server = createServer((req, res) => {
expect(req.url).toBe("/hello");
res.writeHead(200);
res.setHeader("content-encoding", "gzip");
const inputStream = new stream.Readable();
inputStream.push("Hello World");
inputStream.push(null);
inputStream.pipe(zlib.createGzip()).pipe(res);
});
const url = await listen(server);
const res = await fetch(new URL("/hello", url));
expect(await res.text()).toBe("Hello World");
} catch (e) {
throw e;
} finally {
server.close();
}
});
it("can send deflate from Server and receive with fetch", async () => {
try {
var server = createServer((req, res) => {
expect(req.url).toBe("/hello");
res.writeHead(200);
res.setHeader("content-encoding", "deflate");
const inputStream = new stream.Readable();
inputStream.push("Hello World");
inputStream.push(null);
inputStream.pipe(zlib.createDeflate()).pipe(res);
});
const url = await listen(server);
const res = await fetch(new URL("/hello", url));
expect(await res.text()).toBe("Hello World");
} catch (e) {
throw e;
} finally {
server.close();
}
});
it("can send brotli from Server and receive with Client", async () => {
try {
var server = createServer((req, res) => {
expect(req.url).toBe("/hello");
res.writeHead(200);
res.setHeader("content-encoding", "br");
const inputStream = new stream.Readable();
inputStream.push("Hello World");
inputStream.push(null);
const passthrough = new stream.PassThrough();
passthrough.on("data", data => res.write(data));
passthrough.on("end", () => res.end());
inputStream.pipe(zlib.createBrotliCompress()).pipe(passthrough);
});
const url = await listen(server);
const { resolve, reject, promise } = Promise.withResolvers();
http.get(new URL("/hello", url), res => {
let rawData = "";
const passthrough = stream.PassThrough();
passthrough.on("data", chunk => {
rawData += chunk;
});
passthrough.on("end", () => {
try {
expect(Buffer.from(rawData)).toEqual(Buffer.from("Hello World"));
resolve();
} catch (e) {
reject(e);
}
});
res.pipe(zlib.createBrotliDecompress()).pipe(passthrough);
});
await promise;
} catch (e) {
throw e;
} finally {
server.close();
}
});
it("ServerResponse ClientRequest field exposes agent getter", async () => {
try {
var server = createServer((req, res) => {
expect(req.url).toBe("/hello");
res.writeHead(200);
res.end("world");
});
const url = await listen(server);
const { resolve, reject, promise } = Promise.withResolvers();
http.get(new URL("/hello", url), res => {
try {
expect(res.req.agent.protocol).toBe("http:");
resolve();
} catch (e) {
reject(e);
}
});
await promise;
} catch (e) {
throw e;
} finally {
server.close();
}
});
it("should accept custom certs when provided", async () => {
const server = https.createServer(
{
key: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.key")),
cert: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.crt")),
passphrase: "123123123",
},
(req, res) => {
res.write("Hello from https server");
res.end();
},
);
server.listen(0, "localhost");
const address = server.address();
let url_address = address.address;
const res = await fetch(`https://localhost:${address.port}`, {
tls: {
rejectUnauthorized: true,
ca: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost_ca.pem")),
},
});
const t = await res.text();
expect(t).toEqual("Hello from https server");
server.close();
});
it("should error with faulty args", async () => {
const server = https.createServer(
{
key: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.key")),
cert: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.crt")),
passphrase: "123123123",
},
(req, res) => {
res.write("Hello from https server");
res.end();
},
);
server.listen(0, "localhost");
const address = server.address();
try {
let url_address = address.address;
const res = await fetch(`https://localhost:${address.port}`, {
tls: {
rejectUnauthorized: true,
ca: "some invalid value for a ca",
},
});
await res.text();
expect(true).toBe("unreacheable");
} catch (err) {
expect(err.code).toBe("FailedToOpenSocket");
expect(err.message).toBe("Was there a typo in the url or port?");
}
server.close();
});
it("should propagate exception in sync data handler", async () => {
const { exitCode, stdout } = Bun.spawnSync({
cmd: [bunExe(), "run", path.join(import.meta.dir, "node-http-error-in-data-handler-fixture.1.js")],
stdout: "pipe",
stderr: "inherit",
env: bunEnv,
});
expect(stdout.toString()).toContain("Test passed");
expect(exitCode).toBe(0);
});
it("should propagate exception in async data handler", async () => {
const { exitCode, stdout } = Bun.spawnSync({
cmd: [bunExe(), "run", path.join(import.meta.dir, "node-http-error-in-data-handler-fixture.2.js")],
stdout: "pipe",
stderr: "inherit",
env: bunEnv,
});
expect(stdout.toString()).toContain("Test passed");
expect(exitCode).toBe(0);
});
// This test is disabled because it can OOM the CI
it.skip("should be able to stream huge amounts of data", async () => {
const buf = Buffer.alloc(1024 * 1024 * 256);
const CONTENT_LENGTH = 3 * 1024 * 1024 * 1024;
let received = 0;
let written = 0;
const { promise: listen, resolve: resolveListen } = Promise.withResolvers();
const server = http
.createServer((req, res) => {
res.writeHead(200, {
"Content-Type": "text/plain",
"Content-Length": CONTENT_LENGTH,
});
function commit() {
if (written < CONTENT_LENGTH) {
written += buf.byteLength;
res.write(buf, commit);
} else {
res.end();
}
}
commit();
})
.listen(0, "localhost", resolveListen);
await listen;
try {
const response = await fetch(`http://localhost:${server.address().port}`);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("text/plain");
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
received += value ? value.byteLength : 0;
if (done) {
break;
}
}
expect(written).toBe(CONTENT_LENGTH);
expect(received).toBe(CONTENT_LENGTH);
} finally {
server.close();
}
}, 30_000);
// TODO: today we use a workaround to continue event, we need to fix it in the future.
it("should emit continue event #7480", done => {
let receivedContinue = false;
const req = https.request(
"https://example.com",
{ headers: { "accept-encoding": "identity", "expect": "100-continue" } },
res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(receivedContinue).toBe(true);
expect(data).toContain("This domain is for use in illustrative examples in documents");
done();
});
res.on("error", err => done(err));
},
);
req.on("continue", () => {
receivedContinue = true;
});
req.end();
});
it("should not emit continue event #7480", done => {
let receivedContinue = false;
const req = https.request("https://example.com", { headers: { "accept-encoding": "identity" } }, res => {
let data = "";
res.setEncoding("utf8");
res.on("data", chunk => {
data += chunk;
});
res.on("end", () => {
expect(receivedContinue).toBe(false);
expect(data).toContain("This domain is for use in illustrative examples in documents");
done();
});
res.on("error", err => done(err));
});
req.on("continue", () => {
receivedContinue = true;
});
req.end();
});
it("http.Agent is configured correctly", () => {
const agent = new http.Agent();
expect(agent.defaultPort).toBe(80);
expect(agent.protocol).toBe("http:");
});
it("https.Agent is configured correctly", () => {
const agent = new https.Agent();
expect(agent.defaultPort).toBe(443);
expect(agent.protocol).toBe("https:");
});
it("http.get can use http.Agent", async () => {
const agent = new http.Agent();
const { promise, resolve } = Promise.withResolvers();
http.get({ agent, hostname: "google.com" }, resolve);
const response = await promise;
expect(response.req.port).toBe(80);
expect(response.req.protocol).toBe("http:");
});
it("https.get can use https.Agent", async () => {
const agent = new https.Agent();
const { promise, resolve } = Promise.withResolvers();
https.get({ agent, hostname: "google.com" }, resolve);
const response = await promise;
expect(response.req.port).toBe(443);
expect(response.req.protocol).toBe("https:");
});
it("http.request has the correct options", async () => {
const { promise, resolve } = Promise.withResolvers();
http.request("http://google.com/", resolve).end();
const response = await promise;
expect(response.req.port).toBe(80);
expect(response.req.protocol).toBe("http:");
});
it("https.request has the correct options", async () => {
const { promise, resolve } = Promise.withResolvers();
https.request("https://google.com/", resolve).end();
const response = await promise;
expect(response.req.port).toBe(443);
expect(response.req.protocol).toBe("https:");
});
it("using node:http to do https: request fails", () => {
expect(() => http.request("https://example.com")).toThrow(TypeError);
expect(() => http.request("https://example.com")).toThrow({
code: "ERR_INVALID_PROTOCOL",
message: `Protocol "https:" not supported. Expected "http:"`,
});
});
it("should emit close, and complete should be true only after close #13373", async () => {
const server = http.createServer().listen(0);
try {
await once(server, "listening");
fetch(`http://localhost:${server.address().port}`)
.then(res => res.text())
.catch(() => {});
const [req, res] = await once(server, "request");
expect(req.complete).toBe(false);
console.log("ok 1");
const closeEvent = once(req, "close");
res.end("hi");
await closeEvent;
expect(req.complete).toBe(true);
} finally {
server.closeAllConnections();
}
});
it("should emit close when connection is aborted", async () => {
const server = http.createServer().listen(0);
server.unref();
try {
await once(server, "listening");
const controller = new AbortController();
fetch(`http://localhost:${server.address().port}`, { signal: controller.signal })
.then(res => res.text())
.catch(() => {});
const [req, res] = await once(server, "request");
const closeEvent = Promise.withResolvers();
req.once("close", () => {
closeEvent.resolve();
});
controller.abort();
await closeEvent.promise;
expect(req.aborted).toBe(true);
} finally {
server.close();
}
});
it("should emit timeout event", async () => {
const server = http.createServer().listen(0);
try {
await once(server, "listening");
fetch(`http://localhost:${server.address().port}`)
.then(res => res.text())
.catch(() => {});
const [req, res] = await once(server, "request");
expect(req.complete).toBe(false);
let callBackCalled = false;
req.setTimeout(100, () => {
callBackCalled = true;
});
await once(req, "timeout");
expect(callBackCalled).toBe(true);
} finally {
server.closeAllConnections();
}
}, 12_000);
it("should emit timeout event when using server.setTimeout", async () => {
const server = http.createServer().listen(0);
try {
await once(server, "listening");
let callBackCalled = false;
server.setTimeout(100, () => {
callBackCalled = true;
console.log("Called timeout");
});
fetch(`http://localhost:${server.address().port}`, { verbose: true })
.then(res => res.text())
.catch(err => {
console.log(err);
});
const [req, res] = await once(server, "request");
expect(req.complete).toBe(false);
await once(server, "timeout");
expect(callBackCalled).toBe(true);
} finally {
server.closeAllConnections();
}
}, 12_000);
it("must set headersSent to true after headers are sent #3458", async () => {
const server = createServer().listen(0);
try {
await once(server, "listening");
fetch(`http://localhost:${server.address().port}`).then(res => res.text());
const [req, res] = await once(server, "request");
expect(res.headersSent).toBe(false);
const { promise, resolve } = Promise.withResolvers();
res.end("OK", resolve);
await promise;
expect(res.headersSent).toBe(true);
} finally {
server.close();
}
});
it("must set headersSent to true after headers are sent when using chunk encoded", async () => {
const server = createServer().listen(0);
try {
await once(server, "listening");
fetch(`http://localhost:${server.address().port}`).then(res => res.text());
const [req, res] = await once(server, "request");
expect(res.headersSent).toBe(false);
const { promise, resolve } = Promise.withResolvers();
res.write("first", () => {
res.write("second", () => {
res.end("OK", resolve);
});
});
await promise;
expect(res.headersSent).toBe(true);
} finally {
server.close();
}
});
it("should work when sending https.request with agent:false", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const client = https.request("https://example.com/", { agent: false });
client.on("error", reject);
client.on("close", resolve);
client.end();
await promise;
});
it("client should use chunked encoding if more than one write is called", async () => {
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Bun.serve is used here until #15576 or similar fix is merged
using server = Bun.serve({
port: 0,
hostname: "127.0.0.1",
fetch(req) {
if (req.headers.get("transfer-encoding") !== "chunked") {
return new Response("should be chunked encoding", { status: 500 });
}
return new Response(req.body);
},
});
// Options for the HTTP request
const options = {
hostname: "127.0.0.1", // Replace with the target server
port: server.port,
path: "/api/data",
method: "POST",
headers: {
"Content-Type": "application/json",
},
};
const { promise, resolve, reject } = Promise.withResolvers();
// Create the request
const req = http.request(options, res => {
if (res.statusCode !== 200) {
reject(new Error("Body should be chunked"));
}
const chunks = [];
// Collect the response data
res.on("data", chunk => {
chunks.push(chunk);
});
res.on("end", () => {
resolve(chunks);
});
});
// Handle errors
req.on("error", reject);
// Write chunks to the request body
for (let i = 0; i < 4; i++) {
req.write("chunk");
await sleep(50);
req.write(" ");
await sleep(50);
}
req.write("BUN!");
// End the request and signal no more data will be sent
req.end();
const chunks = await promise;
expect(chunks.length).toBeGreaterThan(1);
expect(chunks[chunks.length - 1]?.toString()).toEndWith("BUN!");
expect(Buffer.concat(chunks).toString()).toBe("chunk ".repeat(4) + "BUN!");
});
it("client should use content-length if only one write is called", async () => {
await using server = http.createServer((req, res) => {
if (req.headers["transfer-encoding"] === "chunked") {
return res.writeHead(500).end();
}
res.writeHead(200);
req.on("data", data => {
res.write(data);
});
req.on("end", () => {
res.end();
});
});
await once(server.listen(0, "127.0.0.1"), "listening");
// Options for the HTTP request
const options = {
hostname: "127.0.0.1", // Replace with the target server
port: server.address().port,
path: "/api/data",
method: "POST",
headers: {
"Content-Type": "application/json",
},
};
const { promise, resolve, reject } = Promise.withResolvers();
// Create the request
const req = http.request(options, res => {
if (res.statusCode !== 200) {
reject(new Error("Body should not be chunked"));
}
const chunks = [];
// Collect the response data
res.on("data", chunk => {
chunks.push(chunk);
});
res.on("end", () => {
resolve(chunks);
});
});
// Handle errors
req.on("error", reject);
// Write chunks to the request body
req.write("Hello World BUN!");
// End the request and signal no more data will be sent
req.end();
const chunks = await promise;
expect(chunks.length).toBe(1);
expect(chunks[0]?.toString()).toBe("Hello World BUN!");
expect(Buffer.concat(chunks).toString()).toBe("Hello World BUN!");
});
it("should allow numbers headers to be set in node:http server and client", async () => {
let server_headers;
await using server = http.createServer((req, res) => {
server_headers = req.headers;
res.setHeader("x-number", 10);
res.appendHeader("x-number-2", 20);
res.end();
});
await once(server.listen(0, "localhost"), "listening");
const { promise, resolve } = Promise.withResolvers();
{
const response = http.request(`http://localhost:${server.address().port}`, resolve);
response.setHeader("x-number", 30);
response.appendHeader("x-number-2", 40);
response.end();
}
const response = (await promise) as Record<string, string>;
expect(response.headers["x-number"]).toBe("10");
expect(response.headers["x-number-2"]).toBe("20");
expect(server_headers["x-number"]).toBe("30");
expect(server_headers["x-number-2"]).toBe("40");
});
it("should allow Strict-Transport-Security when using node:http", async () => {
await using server = http.createServer((req, res) => {
res.writeHead(200, { "Strict-Transport-Security": "max-age=31536000" });
res.end();
});
server.listen(0, "localhost");
await once(server, "listening");
const response = await fetch(`http://localhost:${server.address().port}`);
expect(response.status).toBe(200);
expect(response.headers.get("strict-transport-security")).toBe("max-age=31536000");
});
it("should support localAddress", async () => {
await new Promise(resolve => {
const server = http.createServer((req, res) => {
const { localAddress, localFamily, localPort } = req.socket;
res.end();
server.close();
expect(localAddress).toStartWith("127.");
expect(localFamily).toBe("IPv4");
expect(localPort).toBeGreaterThan(0);
resolve();
});
server.listen(0, "127.0.0.1", () => {
http.request(`http://localhost:${server.address().port}`).end();
});
});
await new Promise(resolve => {
const server = http.createServer((req, res) => {
const { localAddress, localFamily, localPort } = req.socket;
res.end();
server.close();
expect(localAddress).toStartWith("::");
expect(localFamily).toBe("IPv6");
expect(localPort).toBeGreaterThan(0);
resolve();
});
server.listen(0, "::1", () => {
http.request(`http://[::1]:${server.address().port}`).end();
});
});
});
it("should not emit/throw error when writing after socket.end", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
const server = http.createServer((req, res) => {
res.writeHead(200, { "Connection": "close" });
res.socket.end();
res.on("error", reject);
try {
const result = res.write("Hello, world!");
resolve(result);
} catch (err) {
reject(err);
}
});
try {
await once(server.listen(0), "listening");
const url = `http://localhost:${server.address().port}`;
await fetch(url, {
method: "POST",
body: Buffer.allocUnsafe(1024 * 1024 * 10),
})
.then(res => res.bytes())
.catch(err => {});
expect(await promise).toBeTrue();
} finally {
server.close();
}
});
it("should handle data if not immediately handled", async () => {
// Create a local server to receive data from
const server = http.createServer();
// Listen to the request event
server.on("request", (request, res) => {
setTimeout(() => {
const body: Uint8Array[] = [];
request.on("data", chunk => {
body.push(chunk);
});
request.on("end", () => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(Buffer.concat(body));
});
}, 100);
});
try {
await once(server.listen(0), "listening");
const url = `http://localhost:${server.address().port}`;
const payload = "Hello, world!".repeat(10).toString();
const res = await fetch(url, {
method: "POST",
body: payload,
});
expect(res.status).toBe(200);
expect(await res.text()).toBe(payload);
} finally {
server.close();
}
});
it("Empty requests should not be Transfer-Encoding: chunked", async () => {
const server = http.createServer((req, res) => {
res.end(JSON.stringify(req.headers));
});
await once(server.listen(0), "listening");
const url = `http://localhost:${server.address().port}`;
try {
for (let method of ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"]) {
const { promise, resolve, reject } = Promise.withResolvers();
http
.request(
url,
{
method,
},
res => {
const body: Uint8Array[] = [];
res.on("data", chunk => {
body.push(chunk);
});
res.on("end", () => {
try {
resolve(JSON.parse(Buffer.concat(body).toString()));
} catch (e) {
reject(e);
}
});
},
)
.on("error", reject)
.end();
const headers = (await promise) as Record<string, string | undefined>;
expect(headers).toBeDefined();
expect(headers["transfer-encoding"]).toBeUndefined();
switch (method) {
case "GET":
case "DELETE":
case "OPTIONS":
// Content-Length will not be present for GET, DELETE, and OPTIONS
// aka DELETE in node.js will be undefined and in bun it will be 0
// this is not outside the spec but is different between node.js and bun
expect(headers["content-length"]).toBeOneOf(["0", undefined]);
break;
default:
expect(headers["content-length"]).toBeDefined();
break;
}
}
} finally {
server.close();
}
});
it("should reject non-standard body writes when rejectNonStandardBodyWrites is true", async () => {
{
let body_not_allowed_on_write;
let body_not_allowed_on_end;
for (const rejectNonStandardBodyWrites of [true, false, undefined]) {
await using server = http.createServer({
rejectNonStandardBodyWrites,
});
server.on("request", (req, res) => {
body_not_allowed_on_write = false;
body_not_allowed_on_end = false;
res.writeHead(204);
try {
res.write("bun");
} catch (e: any) {
expect(e?.code).toBe("ERR_HTTP_BODY_NOT_ALLOWED");
body_not_allowed_on_write = true;
}
try {
res.end("bun");
} catch (e: any) {
expect(e?.code).toBe("ERR_HTTP_BODY_NOT_ALLOWED");
body_not_allowed_on_end = true;
// if we throw here, we need to call end() to actually end the request
res.end();
}
});
await once(server.listen(0), "listening");
const url = `http://localhost:${server.address().port}`;
{
await fetch(url, {
method: "GET",
}).then(res => res.text());
expect(body_not_allowed_on_write).toBe(rejectNonStandardBodyWrites || false);
expect(body_not_allowed_on_end).toBe(rejectNonStandardBodyWrites || false);
}
}
}
});
test("should emit clientError when Content-Length is invalid", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
await using server = http.createServer(reject);
server.on("clientError", (err, socket) => {
resolve(err);
socket.destroy();
});
server.listen(0);
await once(server, "listening");
const client = connect(server.address().port, () => {
// HTTP request with invalid Content-Length
// The Content-Length says 10 but the actual body is 20 bytes
// Send the request
client.write(
`POST /test HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nContent-Type: text/plain\r\nContent-Length: invalid\r\n\r\n`,
);
});
const err = (await promise) as Error;
expect(err.code).toBe("HPE_UNEXPECTED_CONTENT_LENGTH");
});
test("should emit clientError when mixing Content-Length and Transfer-Encoding", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
await using server = http.createServer(reject);
server.on("clientError", (err, socket) => {
resolve(err);
socket.destroy();
});
await once(server.listen(0), "listening");
const client = connect(server.address().port, () => {
// HTTP request with invalid Content-Length
// The Content-Length says 10 but the actual body is 20 bytes
// Send the request
client.write(
`POST /test HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nContent-Type: text/plain\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\nHello`,
);
});
const err = (await promise) as Error;
expect(err.code).toBe("HPE_INVALID_TRANSFER_ENCODING");
});
test("should be able to flush headers socket._httpMessage must be set", async () => {
let server: Server | undefined;
try {
server = http.createServer((req, res) => {
res.flushHeaders();
});
await once(server.listen(0), "listening");
const { promise, resolve } = Promise.withResolvers();
const address = server.address() as AddressInfo;
const req = http.get(
{
hostname: address.address,
port: address.port,
},
resolve,
);
const { socket } = req;
await promise;
expect(socket._httpMessage).toBe(req);
socket.destroy();
} finally {
server?.closeAllConnections();
}
});
test("req.connection.bytesWritten must be supported on the server", async () => {
let httpServer: Server;
try {
const { promise, resolve } = Promise.withResolvers();
httpServer = http.createServer(function (req, res) {
res.on("finish", () => resolve(req.connection.bytesWritten));
res.writeHead(200, { "Content-Type": "text/plain" });
const chunk = "7".repeat(1024);
const bchunk = Buffer.from(chunk);
res.write(chunk);
res.write(bchunk);
expect(res.connection.bytesWritten).toBe(1024 * 2);
res.end("bunbunbun");
});
await once(httpServer.listen(0), "listening");
const address = httpServer.address() as AddressInfo;
const req = http.get({ port: address.port });
await once(req, "response");
const bytesWritten = await promise;
expect(typeof bytesWritten).toBe("number");
expect(bytesWritten).toBe(1024 * 2 + 9);
req.destroy();
} finally {
httpServer?.closeAllConnections();
}
});
test("req.connection.bytesWritten must be supported on the https server", async () => {
let httpServer: Server;
try {
const { promise, resolve } = Promise.withResolvers();
httpServer = createHttpsServer(COMMON_TLS_CERT, function (req, res) {
res.on("finish", () => resolve(req.connection.bytesWritten));
res.writeHead(200, { "Content-Type": "text/plain" });
// Write 1.5mb to cause some requests to buffer
// Also, mix up the encodings a bit.
const chunk = "7".repeat(1024);
const bchunk = Buffer.from(chunk);
res.write(chunk);
res.write(bchunk);
// Get .bytesWritten while buffer is not empty
expect(res.connection.bytesWritten).toBe(1024 * 2);
res.end("bunbunbun");
});
await once(httpServer.listen(0), "listening");
const address = httpServer.address() as AddressInfo;
const req = https.get({ port: address.port, rejectUnauthorized: false });
await once(req, "response");
const bytesWritten = await promise;
expect(typeof bytesWritten).toBe("number");
expect(bytesWritten).toBe(1024 * 2 + 9);
req.destroy();
} finally {
httpServer?.closeAllConnections();
}
});
test("host array should throw in http.request", () => {
expect(() =>
http.request({
host: [1, 2, 3],
}),
).toThrow('The "options.host" property must be of type string, undefined, or null. Received an instance of Array');
});
test("strictContentLength should work on server", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
await using server = http.createServer((req, res) => {
try {
res.strictContentLength = true;
res.writeHead(200, { "Content-Length": 10 });
res.write("123456789");
// Too much data
try {
res.write("123456789");
expect.unreachable();
} catch (e: any) {
expect(e).toBeInstanceOf(Error);
expect(e.code).toBe("ERR_HTTP_CONTENT_LENGTH_MISMATCH");
}
// Too little data
try {
res.end();
expect.unreachable();
} catch (e: any) {
expect(e).toBeInstanceOf(Error);
expect(e.code).toBe("ERR_HTTP_CONTENT_LENGTH_MISMATCH");
}
// Just right
res.end("0");
resolve();
} catch (e: any) {
reject(e);
} finally {
}
});
await once(server.listen(0), "listening");
const url = `http://localhost:${server.address().port}`;
await fetch(url, {
method: "GET",
}).catch(() => {});
await promise;
});
test("client side flushHeaders should work", async () => {
const { promise, resolve } = Promise.withResolvers();
await using server = http.createServer((req, res) => {
resolve(req.headers);
res.end();
});
await once(server.listen(0), "listening");
const address = server.address() as AddressInfo;
const req = http.request({
method: "GET",
host: "127.0.0.1",
port: address.port,
});
req.setHeader("foo", "bar");
req.flushHeaders();
const headers = await promise;
expect(headers).toBeDefined();
expect(headers.foo).toEqual("bar");
});
test("flushHeaders should not drop request body", async () => {
const { promise, resolve } = Promise.withResolvers<string>();
await using server = http.createServer((req, res) => {
let body = "";
req.setEncoding("utf8");
req.on("data", chunk => (body += chunk));
req.on("end", () => {
resolve(body);
res.end();
});
});
await once(server.listen(0), "listening");
const address = server.address() as AddressInfo;
const req = http.request({
method: "POST",
host: "127.0.0.1",
port: address.port,
headers: { "content-type": "text/plain" },
});
req.flushHeaders();
req.write("bun");
req.end("rocks");
const body = await promise;
expect(body).toBe("bunrocks");
});
test("server.listening should work", async () => {
const server = http.createServer();
await once(server.listen(0), "listening");
expect(server.listening).toBe(true);
server.closeAllConnections();
expect(server.listening).toBe(false);
});
test("asyncDispose should work in http.Server", async () => {
const server = http.createServer();
await once(server.listen(0), "listening");
expect(server.listening).toBe(true);
await server[Symbol.asyncDispose]();
expect(server.listening).toBe(false);
});
test("timeout destruction should be visible using kConnectionsCheckingInterval", async () => {
const { kConnectionsCheckingInterval } = require("_http_server");
const server = http.createServer();
await once(server.listen(0), "listening");
server.closeAllConnections();
expect(server[kConnectionsCheckingInterval]._destroyed).toBe(true);
});
test("client should be able to send a array of [key, value] as headers", async () => {
const { promise, resolve } = Promise.withResolvers();
await using server = http.createServer((req, res) => {
resolve([req, res]);
});
await once(server.listen(0), "listening");
const address = server.address() as AddressInfo;
http.get({
host: "127.0.0.1",
port: address.port,
headers: [
["foo", "bar"],
["foo", "baz"],
["host", "127.0.0.1"],
["host", "127.0.0.2"],
["host", "127.0.0.3"],
],
});
const [req, res] = await promise;
expect(req.headers.foo).toBe("bar, baz");
expect(req.headers.host).toBe("127.0.0.1");
res.end();
});
test("clientError should fire when receiving invalid method", async () => {
await using server = http.createServer((req, res) => {
res.end();
});
let socket;
server.on("clientError", err => {
expect(err.code).toBe("HPE_INVALID_METHOD");
expect(err.rawPacket.toString()).toBe("*");
socket.end();
});
await once(server.listen(0), "listening");
const address = server.address() as AddressInfo;
socket = createConnection({ port: address.port });
await once(socket, "connect");
socket.write("*");
await once(socket, "close");
});
test("throw inside clientError should be propagated to uncaughtException", async () => {
const testFile = path.join(import.meta.dir, "node-http-clientError-uncaughtException-fixture.js");
expect([testFile]).toRun("", 0);
});
test("chunked encoding must be valid after flushHeaders", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
await using server = http.createServer(async (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain", "Transfer-Encoding": "chunked" });
res.flushHeaders();
// make sure headers are flushed
await Bun.sleep(10);
// send some chunks at once
res.write("chunk 1");
res.write("chunk 2");
res.write("chunk 3");
res.write("chunk 4");
res.write("chunk 5");
await Bun.sleep(10);
// send some more chunk
res.write("chunk 6");
res.write("chunk 7");
await Bun.sleep(10);
// send the last chunk
res.end();
});
server.listen(3000);
await once(server, "listening");
const socket = connect(3000, () => {
socket.write("GET / HTTP/1.1\r\nHost: localhost:3000\r\nConnection: close\r\n\r\n");
});
const chunks = [];
let received_headers = false;
socket.on("data", data => {
if (!received_headers) {
received_headers = true;
const headers = data.toString("utf-8").split("\r\n");
expect(headers[0]).toBe("HTTP/1.1 200 OK");
expect(headers[1]).toBe("Content-Type: text/plain");
expect(headers[2]).toBe("Transfer-Encoding: chunked");
expect(headers[3].startsWith("Date:")).toBe(true);
// empty line for end of headers aka flushHeaders works
expect(headers[headers.length - 1]).toBe("");
expect(headers[headers.length - 2]).toBe("");
} else {
chunks.push(data);
}
});
function parseChunkedData(buffer) {
let offset = 0;
let result = Buffer.alloc(0);
while (offset < buffer.length) {
// Find the CRLF that terminates the chunk size line
let lineEnd = buffer.indexOf("\r\n", offset);
if (lineEnd === -1) break;
// Parse the chunk size (in hex)
const chunkSizeHex = buffer.toString("ascii", offset, lineEnd);
const chunkSize = parseInt(chunkSizeHex, 16);
expect(isNaN(chunkSize)).toBe(false);
// If chunk size is 0, we've reached the end
if (chunkSize === 0) {
// Skip the final CRLF after the 0-size chunk
offset = lineEnd + 4;
break;
}
// Move past the chunk size line's CRLF
offset = lineEnd + 2;
// Extract the chunk data
const chunkData = buffer.slice(offset, offset + chunkSize);
// Concatenate this chunk to our result
result = Buffer.concat([result, chunkData]);
// Move past this chunk's data and its terminating CRLF
offset += chunkSize + 2;
}
return result;
}
socket.on("end", () => {
try {
const body = parseChunkedData(Buffer.concat(chunks));
expect(body.toString("utf-8")).toBe("chunk 1chunk 2chunk 3chunk 4chunk 5chunk 6chunk 7");
resolve();
} catch (e) {
reject(e);
} finally {
socket.end();
}
});
await promise;
});
test("chunked encoding must be valid using minimal code", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
await using server = http.createServer(async (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain", "Transfer-Encoding": "chunked" });
res.write("chunk 1");
res.end("chunk 2");
});
server.listen(3000);
await once(server, "listening");
const socket = connect(3000, () => {
socket.write("GET / HTTP/1.1\r\nHost: localhost:3000\r\nConnection: close\r\n\r\n");
});
const chunks = [];
socket.on("data", data => {
chunks.push(data);
});
function parseChunkedData(buffer) {
let offset = 0;
let result = Buffer.alloc(0);
while (offset < buffer.length) {
// Find the CRLF that terminates the chunk size line
let lineEnd = buffer.indexOf("\r\n", offset);
if (lineEnd === -1) break;
// Parse the chunk size (in hex)
const chunkSizeHex = buffer.toString("ascii", offset, lineEnd);
const chunkSize = parseInt(chunkSizeHex, 16);
expect(isNaN(chunkSize)).toBe(false);
// If chunk size is 0, we've reached the end
if (chunkSize === 0) {
// Skip the final CRLF after the 0-size chunk
offset = lineEnd + 4;
break;
}
// Move past the chunk size line's CRLF
offset = lineEnd + 2;
// Extract the chunk data
const chunkData = buffer.slice(offset, offset + chunkSize);
// Concatenate this chunk to our result
result = Buffer.concat([result, chunkData]);
// Move past this chunk's data and its terminating CRLF
offset += chunkSize + 2;
}
return result;
}
socket.on("end", () => {
try {
const data = Buffer.concat(chunks);
const headersEnd = data.indexOf("\r\n\r\n");
const headers = data.toString("utf-8", 0, headersEnd).split("\r\n");
expect(headers[0]).toBe("HTTP/1.1 200 OK");
expect(headers[1]).toBe("Content-Type: text/plain");
expect(headers[2]).toBe("Transfer-Encoding: chunked");
expect(headers[3].startsWith("Date:")).toBe(true);
const body = parseChunkedData(data.slice(headersEnd + 4));
expect(body.toString("utf-8")).toBe("chunk 1chunk 2");
resolve();
} catch (e) {
reject(e);
} finally {
socket.end();
}
});
await promise;
});
test("chunked encoding must be valid after without flushHeaders", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
await using server = http.createServer(async (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain", "Transfer-Encoding": "chunked" });
// send some chunks at once
res.write("chunk 1");
res.write("chunk 2");
res.write("chunk 3");
res.write("chunk 4");
res.write("chunk 5");
await Bun.sleep(10);
// send some more chunk
res.write("chunk 6");
res.write("chunk 7");
await Bun.sleep(10);
// send the last chunk
res.end();
});
server.listen(3000);
await once(server, "listening");
const socket = connect(3000, () => {
socket.write("GET / HTTP/1.1\r\nHost: localhost:3000\r\nConnection: close\r\n\r\n");
});
const chunks = [];
socket.on("data", data => {
chunks.push(data);
});
function parseChunkedData(buffer) {
let offset = 0;
let result = Buffer.alloc(0);
while (offset < buffer.length) {
// Find the CRLF that terminates the chunk size line
let lineEnd = buffer.indexOf("\r\n", offset);
if (lineEnd === -1) break;
// Parse the chunk size (in hex)
const chunkSizeHex = buffer.toString("ascii", offset, lineEnd);
const chunkSize = parseInt(chunkSizeHex, 16);
expect(isNaN(chunkSize)).toBe(false);
// If chunk size is 0, we've reached the end
if (chunkSize === 0) {
// Skip the final CRLF after the 0-size chunk
offset = lineEnd + 4;
break;
}
// Move past the chunk size line's CRLF
offset = lineEnd + 2;
// Extract the chunk data
const chunkData = buffer.slice(offset, offset + chunkSize);
// Concatenate this chunk to our result
result = Buffer.concat([result, chunkData]);
// Move past this chunk's data and its terminating CRLF
offset += chunkSize + 2;
}
return result;
}
socket.on("end", () => {
try {
const data = Buffer.concat(chunks);
const headersEnd = data.indexOf("\r\n\r\n");
const headers = data.toString("utf-8", 0, headersEnd).split("\r\n");
expect(headers[0]).toBe("HTTP/1.1 200 OK");
expect(headers[1]).toBe("Content-Type: text/plain");
expect(headers[2]).toBe("Transfer-Encoding: chunked");
expect(headers[3].startsWith("Date:")).toBe(true);
const body = parseChunkedData(data.slice(headersEnd + 4));
expect(body.toString("utf-8")).toBe("chunk 1chunk 2chunk 3chunk 4chunk 5chunk 6chunk 7");
resolve();
} catch (e) {
reject(e);
} finally {
socket.end();
}
});
await promise;
});
test("should accept received and send blank headers", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
await using server = http.createServer(async (req, res) => {
expect(req.headers["empty-header"]).toBe("");
res.writeHead(200, { "x-test": "test", "empty-header": "" });
res.end();
});
server.listen(0);
await once(server, "listening");
const socket = createConnection((server.address() as AddressInfo).port, "localhost", () => {
socket.write("GET / HTTP/1.1\r\nHost: localhost:3000\r\nConnection: close\r\nEmpty-Header:\r\n\r\n");
});
socket.on("data", data => {
const headers = data.toString("utf-8").split("\r\n");
expect(headers[0]).toBe("HTTP/1.1 200 OK");
expect(headers[1]).toBe("x-test: test");
expect(headers[2]).toBe("empty-header: ");
socket.end();
resolve();
});
socket.on("error", reject);
await promise;
});
test("should handle header overflow", async () => {
await using server = http.createServer(async (req, res) => {
expect.unreachable();
});
const { promise, resolve, reject } = Promise.withResolvers();
server.on("connection", socket => {
socket.on("error", (err: any) => {
expect(err.code).toBe("HPE_HEADER_OVERFLOW");
resolve();
});
});
server.listen(0);
await once(server, "listening");
const socket = createConnection((server.address() as AddressInfo).port, "localhost", () => {
socket.write(
"GET / HTTP/1.1\r\nHost: localhost:3000\r\nConnection: close\r\nBig-Header: " +
"a".repeat(http.maxHeaderSize) + // will overflow because of host and connection headers
"\r\n\r\n",
);
});
socket.on("error", reject);
await promise;
});
test("should handle invalid method", async () => {
await using server = http.createServer(async (req, res) => {
expect.unreachable();
});
const { promise, resolve, reject } = Promise.withResolvers();
server.on("connection", socket => {
socket.on("error", (err: any) => {
expect(err.code).toBe("HPE_INVALID_METHOD");
resolve();
});
});
server.listen(0);
await once(server, "listening");
const socket = createConnection((server.address() as AddressInfo).port, "localhost", () => {
socket.write(
"BUN / HTTP/1.1\r\nHost: localhost:3000\r\nConnection: close\r\nBig-Header: " +
"a".repeat(http.maxHeaderSize) + // will overflow because of host and connection headers
"\r\n\r\n",
);
});
socket.on("error", reject);
await promise;
});
describe("HTTP Server Security Tests - Advanced", () => {
// Setup and teardown utilities
let server;
let port;
beforeEach(async () => {
server = new Server();
server.listen(0, () => {
port = server.address().port;
});
await once(server, "listening");
});
afterEach(async () => {
// Close the server if it's still running
if (server.listening) {
server.closeAllConnections();
}
});
// Helper that returns a promise with the server response
const sendRequest = message => {
return new Promise((resolve, reject) => {
const client = connect(port, "localhost");
let response = "";
client.setEncoding("utf8");
client.on("data", chunk => {
response += chunk;
});
client.on("error", reject);
client.on("end", () => {
resolve(response.toString("utf8"));
});
client.write(message);
});
};
// Mock request handler that simulates security-sensitive operations
const createMockHandler = () => {
const mockHandler = jest.fn().mockImplementation((req, res) => {
// In a real app, this might be a security-sensitive operation
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Request processed successfully");
});
return mockHandler;
};
// Test Suites
describe("Header Injection Protection", () => {
test("rejects requests with CR in header field name", async () => {
const mockHandler = createMockHandler();
server.on("request", mockHandler);
const { promise, resolve, reject } = Promise.withResolvers();
server.on("clientError", (err: any) => {
try {
expect(err.code).toBe("HPE_INVALID_HEADER_TOKEN");
resolve();
} catch (err) {
reject(err);
}
});
const msg = ["GET / HTTP/1.1", "Host: localhost", "Bad\rHeader: value", "", ""].join("\r\n");
const response = await sendRequest(msg);
expect(response).toInclude("400 Bad Request");
await promise;
expect(mockHandler).not.toHaveBeenCalled();
});
test("rejects requests with CR in header field value", async () => {
const mockHandler = createMockHandler();
server.on("request", mockHandler);
const { promise, resolve, reject } = Promise.withResolvers();
server.on("clientError", (err: any) => {
try {
expect(err.code).toBe("HPE_INTERNAL");
resolve();
} catch (err) {
reject(err);
}
});
const msg = ["GET / HTTP/1.1", "Host: localhost", "X-Custom: bad\rvalue", "", ""].join("\r\n");
const response = await sendRequest(msg);
expect(response).toInclude("400 Bad Request");
await promise;
expect(mockHandler).not.toHaveBeenCalled();
});
});
describe("Transfer-Encoding Attacks", () => {
test("rejects chunked requests with malformed chunk size", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
server.on("clientError", (err: any) => {
try {
expect(err.code).toBe("HPE_INTERNAL");
resolve();
} catch (err) {
reject(err);
}
});
const msg = [
"POST / HTTP/1.1",
"Host: localhost",
"Transfer-Encoding: chunked",
"",
"XYZ\r\n", // Not a valid hex number
"data",
"0",
"",
"",
].join("\r\n");
const response = await sendRequest(msg);
expect(response).toInclude("400 Bad Request");
await promise;
});
test("rejects chunked requests with invalid chunk ending", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
server.on("clientError", (err: any) => {
try {
expect(err.code).toBe("HPE_INTERNAL");
resolve();
} catch (err) {
reject(err);
}
});
const msg = [
"POST / HTTP/1.1",
"Host: localhost",
"Transfer-Encoding: chunked",
"",
"4",
"dataXXXX", // Should be "data\r\n"
"0",
"",
"",
].join("\r\n");
const response = await sendRequest(msg);
expect(response).toInclude("400 Bad Request");
await promise;
});
});
describe("HTTP Request Smuggling", () => {
test("rejects requests with both Content-Length and Transfer-Encoding", async () => {
const mockHandler = createMockHandler();
server.on("request", mockHandler);
const { promise, resolve, reject } = Promise.withResolvers();
server.on("clientError", (err: any) => {
try {
expect(err.code).toBe("HPE_INVALID_TRANSFER_ENCODING");
resolve();
} catch (err) {
reject(err);
}
});
const msg = [
"POST / HTTP/1.1",
"Host: localhost",
"Content-Length: 10",
"Transfer-Encoding: chunked",
"",
"5",
"hello",
"0",
"",
"",
].join("\r\n");
const response = await sendRequest(msg);
expect(response).toInclude("400 Bad Request");
await promise;
expect(mockHandler).not.toHaveBeenCalled();
});
test("rejects requests with obfuscated Transfer-Encoding header", async () => {
const mockHandler = createMockHandler();
server.on("request", mockHandler);
const { promise, resolve, reject } = Promise.withResolvers();
server.on("clientError", (err: any) => {
try {
expect(err.code).toBe("HPE_INVALID_HEADER_TOKEN");
resolve();
} catch (err) {
reject(err);
}
});
const msg = [
"POST / HTTP/1.1",
"Host: localhost",
"Content-Length: 11",
"Transfer-Encoding : chunked", // Note the space before colon
"",
"5",
"hello",
"0",
"",
"",
].join("\r\n");
const response = await sendRequest(msg);
expect(response).toInclude("400 Bad Request");
await promise;
expect(mockHandler).not.toHaveBeenCalled();
});
});
describe("HTTP Protocol Violations", () => {
test("rejects requests with invalid HTTP version", async () => {
const mockHandler = createMockHandler();
server.on("request", mockHandler);
const { promise, resolve, reject } = Promise.withResolvers();
server.on("clientError", (err: any) => {
try {
expect(err.code).toBe("HPE_INTERNAL");
resolve();
} catch (err) {
reject(err);
}
});
const msg = [
"GET / HTTP/9.9", // Invalid HTTP version
"Host: localhost",
"",
"",
].join("\r\n");
const response = await sendRequest(msg);
expect(response).toInclude("505 HTTP Version Not Supported");
await promise;
expect(mockHandler).not.toHaveBeenCalled();
});
test("rejects requests with missing Host header in HTTP/1.1", async () => {
const mockHandler = createMockHandler();
server.on("request", mockHandler);
const { promise, resolve, reject } = Promise.withResolvers();
server.on("clientError", (err: any) => {
try {
expect(err.code).toBe("HPE_INTERNAL");
resolve();
} catch (err) {
reject(err);
}
});
const msg = [
"GET / HTTP/1.1",
// Missing Host header
"",
"",
].join("\r\n");
const response = await sendRequest(msg);
expect(response).toInclude("400 Bad Request");
await promise;
expect(mockHandler).not.toHaveBeenCalled();
});
});
});