/** * 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 { 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(" { 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; 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; 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(); 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(0); await once(server, "listening"); const socket = connect(server.address().port, () => { socket.write(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\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(0); await once(server, "listening"); const socket = connect(server.address().port, () => { socket.write(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\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(0); await once(server, "listening"); const socket = connect(server.address().port, () => { socket.write(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\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:${server.address().port}\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:${server.address().port}\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:${server.address().port}\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(); }); }); test("Server should not crash in clientError is emitted when calling destroy", async () => { await using server = http.createServer(async (req, res) => { res.end("Hello World"); }); const clientErrors: Promise[] = []; server.on("clientError", (err, socket) => { clientErrors.push( Bun.sleep(10).then(() => { socket.destroy(); }), ); }); await once(server.listen(), "listening"); const address = server.address() as AddressInfo; async function doRequests(address: AddressInfo) { const client = connect(address.port, address.address, () => { client.write("GET / HTTP/1.1\r\nHost: localhost:3000\r\nContent-Length: 0\r\n\r\n"); }); { const { promise, resolve, reject } = Promise.withResolvers(); client.on("data", resolve); client.on("error", reject); client.on("end", resolve); await promise; } { const { promise, resolve, reject } = Promise.withResolvers(); client.write("GET / HTTP/1.1\r\nContent-Length: 0\r\n\r\n"); client.on("error", reject); client.on("end", resolve); await promise; } } async function doInvalidRequests(address: AddressInfo) { const client = connect(address.port, address.address, () => { client.write("GET / HTTP/1.1\r\nContent-Length: 0\r\n\r\n"); }); const { promise, resolve, reject } = Promise.withResolvers(); client.on("error", reject); client.on("close", resolve); await promise; } await doRequests(address); await Promise.all(clientErrors); clientErrors.length = 0; await doInvalidRequests(address); await Promise.all(clientErrors); }); });