import { file, gc, Serve, serve, Server } from "bun"; import { afterAll, afterEach, describe, expect, it, mock } from "bun:test"; import { readFileSync, writeFileSync } from "fs"; import { bunEnv, bunExe, dumpStats, isBroken, isIntelMacOS, isIPv4, isIPv6, isPosix, tls, tmpdirSync } from "harness"; import { join, resolve } from "path"; // import { renderToReadableStream } from "react-dom/server"; // import app_jsx from "./app.jsx"; import { heapStats } from "bun:jsc"; import { spawn } from "child_process"; import net from "node:net"; import { tmpdir } from "os"; let renderToReadableStream: any = null; let app_jsx: any = null; type Handler = (req: Request) => Response; afterEach(() => { gc(true); }); const count = 200; let server: Server | undefined; async function runTest({ port, ...serverOptions }: Serve, test: (server: Server) => Promise | void) { if (server) { server.reload({ ...serverOptions, port: 0 }); } else { while (!server) { try { server = serve({ ...serverOptions, port: 0 }); break; } catch (e: any) { console.log("catch:", e); if (e?.message !== `Failed to start server `) { throw e; } } } } await test(server); } afterAll(() => { if (server) { server.stop(true); server = undefined; } }); it("should be able to abruptly stop the server many times", async () => { async function run() { const stopped = Promise.withResolvers(); const server = Bun.serve({ port: 0, error() { return new Response("Error", { status: 500 }); }, async fetch(req, server) { await Bun.sleep(50); server.stop(true); await Bun.sleep(50); server = undefined; if (stopped.resolve) { stopped.resolve(); stopped.resolve = undefined; } return new Response("Hello, World!"); }, }); const url = server.url; async function request() { try { await fetch(url, { keepalive: true }).then(res => res.text()); expect.unreachable(); } catch (e) { expect(["ECONNRESET", "ConnectionRefused"]).toContain(e.code); } } const requests = new Array(20); for (let i = 0; i < 20; i++) { requests[i] = request(); } await Promise.all(requests); await stopped.promise; Bun.gc(true); } const runs = new Array(10); for (let i = 0; i < 10; i++) { runs[i] = run(); } await Promise.all(runs); Bun.gc(true); }); // This test reproduces a crash in Bun v1.1.18 and earlier it("should be able to abruptly stop the server", async () => { for (let i = 0; i < 2; i++) { const controller = new AbortController(); using server = Bun.serve({ port: 0, error() { return new Response("Error", { status: 500 }); }, async fetch(req, server) { server.stop(true); await Bun.sleep(10); return new Response(); }, }); await fetch(server.url, { signal: controller.signal, }) .then(res => { return res.blob(); }) .catch(() => {}); } }); // https://github.com/oven-sh/bun/issues/6758 // https://github.com/oven-sh/bun/issues/4517 it("should call cancel() on ReadableStream when the Request is aborted", async () => { let waitForCancel = Promise.withResolvers(); const abortedFn = mock(() => { console.log("'abort' event fired", new Date()); }); const cancelledFn = mock(() => { console.log("'cancel' function called", new Date()); waitForCancel.resolve(); }); let onIncomingRequest = Promise.withResolvers(); await runTest( { async fetch(req) { req.signal.addEventListener("abort", abortedFn); // Give it a chance to start the stream so that the cancel function can be called. setTimeout(() => { console.log("'onIncomingRequest' function called", new Date()); onIncomingRequest.resolve(); }, 0); return new Response( new ReadableStream({ async pull(controller) { await waitForCancel.promise; }, cancel: cancelledFn, }), ); }, }, async server => { const controller = new AbortController(); const signal = controller.signal; const request = fetch(server.url, { signal }); await onIncomingRequest.promise; controller.abort(); expect(async () => await request).toThrow(); // Delay for one run of the event loop. await Bun.sleep(1); expect(abortedFn).toHaveBeenCalled(); expect(cancelledFn).toHaveBeenCalled(); }, ); }); for (let withDelay of [true, false]) { for (let connectionHeader of ["keepalive", "not keepalive"] as const) { it(`should NOT call cancel() on ReadableStream that finished normally for ${connectionHeader} request and ${withDelay ? "with" : "without"} delay`, async () => { const cancelledFn = mock(() => { console.log("'cancel' function called", new Date()); }); let onIncomingRequest = Promise.withResolvers(); await runTest( { async fetch(req) { return new Response( new ReadableStream({ async pull(controller) { controller.enqueue(new Uint8Array([1, 2, 3])); if (withDelay) await Bun.sleep(1); controller.close(); }, cancel: cancelledFn, }), ); }, }, async server => { const resp = await fetch( server.url, connectionHeader === "keepalive" ? {} : { headers: { "Connection": "close", }, keepalive: false, }, ); await resp.blob(); // Delay for one run of the event loop. await Bun.sleep(1); expect(cancelledFn).not.toHaveBeenCalled(); }, ); }); } } describe.todoIf(isBroken && isIntelMacOS)( "1000 uploads & downloads in batches of 64 do not leak ReadableStream", () => { for (let isDirect of [true, false] as const) { it( isDirect ? "direct" : "default", async () => { const blob = new Blob([new Uint8Array(1024 * 768).fill(123)]); Bun.gc(true); const expected = Bun.CryptoHasher.hash("sha256", blob, "base64"); const initialCount = heapStats().objectTypeCounts.ReadableStream || 0; await runTest( { async fetch(req) { var hasher = new Bun.SHA256(); for await (const chunk of req.body) { await Bun.sleep(0); hasher.update(chunk); } return new Response( isDirect ? new ReadableStream({ type: "direct", async pull(controller) { await Bun.sleep(0); controller.write(Buffer.from(hasher.digest("base64"))); await controller.flush(); controller.close(); }, }) : new ReadableStream({ async pull(controller) { await Bun.sleep(0); controller.enqueue(Buffer.from(hasher.digest("base64"))); controller.close(); }, }), ); }, }, async server => { const count = 1000; async function callback() { const response = await fetch(server.url, { body: blob, method: "POST", }); // We are testing for ReadableStream leaks, so we use the ReadableStream here. const chunks = []; for await (const chunk of response.body) { chunks.push(chunk); } const digest = Buffer.from(Bun.concatArrayBuffers(chunks)).toString(); expect(digest).toBe(expected); Bun.gc(false); } { let remaining = count; const batchSize = 64; while (remaining > 0) { const promises = new Array(count); for (let i = 0; i < batchSize && remaining > 0; i++) { promises[i] = callback(); } await Promise.all(promises); remaining -= batchSize; } } Bun.gc(true); dumpStats(); expect(heapStats().objectTypeCounts.ReadableStream).toBeWithin( Math.max(initialCount - count / 2, 0), initialCount + count / 2, ); }, ); }, 100000, ); } }, ); [200, 200n, 303, 418, 599, 599n].forEach(statusCode => { it(`should response with HTTP status code (${statusCode})`, async () => { await runTest( { fetch() { return new Response("Foo Bar", { status: statusCode }); }, }, async server => { const response = await fetch(server.url.origin); expect(response.status).toBe(Number(statusCode)); expect(await response.text()).toBe("Foo Bar"); }, ); }); }); [-200, 42, 100, 102, 12345, Math.PI, 999, 600, 199, 199n, 600n, 100n, 102n].forEach(statusCode => { it(`should error on invalid HTTP status code (${statusCode})`, async () => { await runTest( { fetch() { try { return new Response("Foo Bar", { status: statusCode }); } catch (err) { expect(err).toBeInstanceOf(RangeError); return new Response("Error!", { status: 500 }); } }, }, async server => { const response = await fetch(server.url.origin); expect(response.status).toBe(500); expect(await response.text()).toBe("Error!"); }, ); }); }); it("should display a welcome message when the response value type is incorrect", async () => { await runTest( { // @ts-ignore fetch(req) { return Symbol("invalid response type"); }, }, async server => { const response = await fetch(server.url.origin); const text = await response.text(); expect(text).toContain("Welcome to Bun!"); }, ); }); it("request.signal works in trivial case", async () => { var aborty = new AbortController(); var signaler = Promise.withResolvers(); await runTest( { async fetch(req) { req.signal.addEventListener("abort", () => { signaler.resolve(); }); aborty.abort(); await Bun.sleep(2); return new Response("Test failed!"); }, }, async server => { expect(async () => { const response = await fetch(server.url.origin, { signal: aborty.signal, }); await signaler.promise; await response.blob(); }).toThrow("The operation was aborted."); }, ); }); it("request.signal works in leaky case", async () => { var aborty = new AbortController(); var signaler = Promise.withResolvers(); await runTest( { async fetch(req) { req.signal.addEventListener("abort", () => { signaler.resolve(); }); aborty.abort(); await Bun.sleep(20); return new Response("Test failed!"); }, }, async server => { await expect(async () => { const resp = await fetch(server.url.origin, { signal: aborty.signal }); await signaler.promise; await Bun.sleep(10); resp.body?.getReader(); }).toThrow("The operation was aborted."); }, ); }); it("should work for a file", async () => { const fixture = resolve(import.meta.dir, "./fetch.js.txt"); const textToExpect = readFileSync(fixture, "utf-8"); await runTest( { fetch(req) { return new Response(file(fixture)); }, }, async server => { const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); }); it("request.url should log successfully", async () => { const fixture = resolve(import.meta.dir, "./fetch.js.txt"); const textToExpect = readFileSync(fixture, "utf-8"); var expected: string; await runTest( { fetch(req) { expect(Bun.inspect(req).includes(expected)).toBe(true); return new Response(file(fixture)); }, }, async server => { expected = `http://localhost:${server.port}/helloooo`; const response = await fetch(expected); expect(response.url).toBe(expected); expect(await response.text()).toBe(textToExpect); }, ); }); it("request.url should be based on the Host header", async () => { const fixture = resolve(import.meta.dir, "./fetch.js.txt"); const textToExpect = readFileSync(fixture, "utf-8"); await runTest( { fetch(req) { expect(req.url).toBe("http://example.com/helloooo"); return new Response(file(fixture)); }, }, async server => { const expected = `${server.url.origin}/helloooo`; const response = await fetch(expected, { headers: { Host: "example.com", }, }); expect(response.url).toBe(expected); expect(await response.text()).toBe(textToExpect); }, ); }); describe("streaming", () => { describe("error handler", () => { it("throw on pull renders headers, does not call error handler", async () => { const onMessage = mock(async url => { const response = await fetch(url); expect(response.status).toBe(402); expect(response.headers.get("X-Hey")).toBe("123"); expect(response.text()).resolves.toBe(""); subprocess.kill(); }); await using subprocess = Bun.spawn({ cwd: import.meta.dirname, cmd: [bunExe(), "readable-stream-throws.fixture.js"], env: bunEnv, stdout: "ignore", stderr: "pipe", ipc: onMessage, }); let [exitCode, stderr] = await Promise.all([subprocess.exited, subprocess.stderr.text()]); expect(exitCode).toBeInteger(); expect(stderr).toContain("error: Oops"); expect(onMessage).toHaveBeenCalled(); }); it("throw on pull after writing should not call the error handler", async () => { const onMessage = mock(async href => { const url = new URL("write", href); const response = await fetch(url); expect(response.status).toBe(402); expect(response.headers.get("X-Hey")).toBe("123"); expect(response.text()).resolves.toBe(""); subprocess.kill(); }); await using subprocess = Bun.spawn({ cwd: import.meta.dirname, cmd: [bunExe(), "readable-stream-throws.fixture.js"], env: bunEnv, stdout: "ignore", stderr: "pipe", ipc: onMessage, }); let [exitCode, stderr] = await Promise.all([subprocess.exited, subprocess.stderr.text()]); expect(exitCode).toBeInteger(); expect(stderr).toContain("error: Oops"); expect(onMessage).toHaveBeenCalled(); }); }); it("text from JS, one chunk", async () => { const relative = new URL("./fetch.js.txt", import.meta.url); const textToExpect = readFileSync(relative, "utf-8"); await runTest( { fetch(req) { return new Response( new ReadableStream({ start(controller) { controller.enqueue(textToExpect); controller.close(); }, }), ); }, }, async server => { const response = await fetch(server.url.origin); const text = await response.text(); expect(text.length).toBe(textToExpect.length); expect(text).toBe(textToExpect); }, ); }); it("text from JS, two chunks", async () => { const fixture = resolve(import.meta.dir, "./fetch.js.txt"); const textToExpect = readFileSync(fixture, "utf-8"); await runTest( { fetch(req) { return new Response( new ReadableStream({ start(controller) { controller.enqueue(textToExpect.substring(0, 100)); controller.enqueue(textToExpect.substring(100)); controller.close(); }, }), ); }, }, async server => { const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); }); it("Error handler is called when a throwing stream hasn't written anything", async () => { await runTest( { error(e) { return new Response("Test Passed", { status: 200 }); }, fetch(req) { return new Response( new ReadableStream({ start(controller) { throw new Error("Test Passed"); }, }), { status: 404, }, ); }, }, async server => { const response = await fetch(server.url.origin); expect(response.status).toBe(200); expect(await response.text()).toBe("Test Passed"); }, ); }); // Also verifies error handler reset in `.reload()` due to test above // TODO: rewrite test so uncaught error does not create an annotation in CI it.skip("text from JS throws on start with no error handler", async () => { await runTest( { error: undefined, fetch(req) { return new Response( new ReadableStream({ start(controller) { throw new Error("Test Passed"); }, }), { status: 420, headers: { "x-what": "123", }, }, ); }, }, async server => { const response = await fetch(server.url.origin); expect(response.status).toBe(500); }, ); }); it("text from JS throws on start has error handler", async () => { var pass = false; var err: Error; await runTest( { error(e) { pass = true; err = e; return new Response("Fail", { status: 500 }); }, fetch(req) { return new Response( new ReadableStream({ start(controller) { throw new TypeError("error"); }, }), ); }, }, async server => { const response = await fetch(server.url.origin); expect(response.status).toBe(500); expect(await response.text()).toBe("Fail"); expect(pass).toBe(true); expect(err?.name).toBe("TypeError"); expect(err?.message).toBe("error"); }, ); }); it("text from JS, 2 chunks, with delay", async () => { const fixture = resolve(import.meta.dir, "./fetch.js.txt"); const textToExpect = readFileSync(fixture, "utf-8"); await runTest( { async fetch(req) { return new Response( new ReadableStream({ async start(controller) { controller.enqueue(textToExpect.substring(0, 100)); await Bun.sleep(0); queueMicrotask(() => { controller.enqueue(textToExpect.substring(100)); controller.close(); }); }, }), ); }, }, async server => { const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); }); it("text from JS, 1 chunk via pull()", async () => { const fixture = resolve(import.meta.dir, "./fetch.js.txt"); const textToExpect = readFileSync(fixture, "utf-8"); await runTest( { fetch(req) { return new Response( new ReadableStream({ pull(controller) { controller.enqueue(textToExpect); controller.close(); }, }), ); }, }, async server => { const response = await fetch(server.url.origin); const text = await response.text(); expect(text).toBe(textToExpect); }, ); }); it("text from JS, 2 chunks, with delay in pull", async () => { const fixture = resolve(import.meta.dir, "./fetch.js.txt"); const textToExpect = readFileSync(fixture, "utf-8"); await runTest( { fetch(req) { return new Response( new ReadableStream({ async pull(controller) { controller.enqueue(textToExpect.substring(0, 100)); await Bun.sleep(0); queueMicrotask(() => { controller.enqueue(textToExpect.substring(100)); controller.close(); }); }, }), ); }, }, async server => { const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); }); it("text from JS, 3 chunks, 1 empty, with delay in pull", async () => { const textToExpect = "hello world"; const groups = [ ["hello", "", " world"], ["", "hello ", "world"], ["hello ", "world", ""], ["hello world", "", ""], ["", "", "hello world"], ]; var count = 0; for (const chunks of groups) { await runTest( { fetch(req) { return new Response( new ReadableStream({ async pull(controller) { for (let chunk of chunks) { controller.enqueue(Buffer.from(chunk)); await Bun.sleep(0); } await Bun.sleep(0); controller.close(); }, }), ); }, }, async server => { const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); count++; }, ); } expect(count).toBe(groups.length); }); it("text from JS, 2 chunks, with async pull", async () => { const fixture = resolve(import.meta.dir, "./fetch.js.txt"); const textToExpect = readFileSync(fixture, "utf-8"); await runTest( { fetch(req) { return new Response( new ReadableStream({ async pull(controller) { controller.enqueue(textToExpect.substring(0, 100)); await Bun.sleep(0); controller.enqueue(textToExpect.substring(100)); await Bun.sleep(0); controller.close(); }, }), ); }, }, async server => { const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); }); it("text from JS, 10 chunks, with async pull", async () => { const fixture = resolve(import.meta.dir, "./fetch.js.txt"); const textToExpect = readFileSync(fixture, "utf-8"); await runTest( { fetch(req) { return new Response( new ReadableStream({ async pull(controller) { var remain = textToExpect; for (let i = 0; i < 10 && remain.length > 0; i++) { controller.enqueue(remain.substring(0, 100)); remain = remain.substring(100); await Bun.sleep(0); } controller.enqueue(remain); controller.close(); }, }), ); }, }, async server => { const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); }); }); it("should work for a hello world", async () => { await runTest( { fetch(req) { return new Response(`Hello, world!`); }, }, async server => { const response = await fetch(server.url.origin); expect(await response.text()).toBe("Hello, world!"); }, ); }); it("should work for a blob", async () => { const fixture = resolve(import.meta.dir, "./fetch.js.txt"); const textToExpect = readFileSync(fixture, "utf-8"); await runTest( { fetch(req) { return new Response(new Blob([textToExpect])); }, }, async server => { const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); }); it("should work for a blob stream", async () => { const fixture = resolve(import.meta.dir, "./fetch.js.txt"); const textToExpect = readFileSync(fixture, "utf-8"); await runTest( { fetch(req) { return new Response(new Blob([textToExpect]).stream()); }, }, async server => { const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); }); it("should work for a file stream", async () => { const fixture = resolve(import.meta.dir, "./fetch.js.txt"); const textToExpect = readFileSync(fixture, "utf-8"); await runTest( { fetch(req) { return new Response(file(fixture).stream()); }, }, async server => { const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); }, ); }); it("fetch should work with headers", async () => { const fixture = resolve(import.meta.dir, "./fetch.js.txt"); await runTest( { fetch(req) { if (req.headers.get("X-Foo") !== "bar") { return new Response("X-Foo header not set", { status: 500 }); } return new Response(file(fixture), { headers: { "X-Both-Ways": "1" }, }); }, }, async server => { const response = await fetch(server.url.origin, { headers: { "X-Foo": "bar", }, }); expect(response.status).toBe(200); expect(response.headers.get("X-Both-Ways")).toBe("1"); }, ); }); it(`should work for a file ${count} times serial`, async () => { const fixture = resolve(import.meta.dir, "./fetch.js.txt"); const textToExpect = readFileSync(fixture, "utf-8"); await runTest( { async fetch(req) { return new Response(file(fixture)); }, }, async server => { for (let i = 0; i < count; i++) { const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); } }, ); }); it(`should work for ArrayBuffer ${count} times serial`, async () => { const textToExpect = "hello"; await runTest( { fetch(req) { return new Response(new TextEncoder().encode(textToExpect)); }, }, async server => { for (let i = 0; i < count; i++) { const response = await fetch(server.url.origin); expect(await response.text()).toBe(textToExpect); } }, ); }); describe("parallel", () => { it(`should work for text ${count} times in batches of 5`, async () => { const textToExpect = "hello"; await runTest( { fetch(req) { return new Response(textToExpect); }, }, async server => { for (let i = 0; i < count; ) { let responses = await Promise.all([ fetch(server.url.origin), fetch(server.url.origin), fetch(server.url.origin), fetch(server.url.origin), fetch(server.url.origin), ]); for (let response of responses) { expect(await response.text()).toBe(textToExpect); } i += responses.length; } }, ); }); it(`should work for Uint8Array ${count} times in batches of 5`, async () => { const textToExpect = "hello"; await runTest( { fetch(req) { return new Response(new TextEncoder().encode(textToExpect)); }, }, async server => { for (let i = 0; i < count; ) { let responses = await Promise.all([ fetch(server.url.origin), fetch(server.url.origin), fetch(server.url.origin), fetch(server.url.origin), fetch(server.url.origin), ]); for (let response of responses) { expect(await response.text()).toBe(textToExpect); } i += responses.length; } }, ); }); }); it("should support reloading", async () => { const first: Handler = req => new Response("first"); const second: Handler = req => new Response("second"); await runTest( { fetch: first, }, async server => { const response = await fetch(server.url.origin); expect(await response.text()).toBe("first"); server.reload({ fetch: second }); const response2 = await fetch(server.url.origin); expect(await response2.text()).toBe("second"); }, ); }); describe("status code text", () => { const fixture = { 200: "OK", 201: "Created", 202: "Accepted", 203: "Non-Authoritative Information", 204: "No Content", 205: "Reset Content", 206: "Partial Content", 207: "Multi-Status", 208: "Already Reported", 226: "IM Used", 300: "Multiple Choices", 301: "Moved Permanently", 302: "Found", 303: "See Other", 304: "Not Modified", 305: "Use Proxy", 306: "Switch Proxy", 307: "Temporary Redirect", 308: "Permanent Redirect", 400: "Bad Request", 401: "Unauthorized", 402: "Payment Required", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable", 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict", 410: "Gone", 411: "Length Required", 412: "Precondition Failed", 413: "Payload Too Large", 414: "URI Too Long", 415: "Unsupported Media Type", 416: "Range Not Satisfiable", 417: "Expectation Failed", 418: "I'm a Teapot", 421: "Misdirected Request", 422: "Unprocessable Entity", 423: "Locked", 424: "Failed Dependency", 425: "Too Early", 426: "Upgrade Required", 428: "Precondition Required", 429: "Too Many Requests", 431: "Request Header Fields Too Large", 451: "Unavailable For Legal Reasons", 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported", 506: "Variant Also Negotiates", 507: "Insufficient Storage", 508: "Loop Detected", 510: "Not Extended", 511: "Network Authentication Required", } as Record; for (let code in fixture) { it(`should return ${code} ${fixture[code]}`, async () => { await runTest( { fetch(req) { return new Response("hey", { status: +code }); }, }, async server => { const response = await fetch(server.url.origin); expect(response.status).toBe(parseInt(code)); expect(response.statusText).toBe(fixture[code]); }, ); }); } }); it("should support multiple Set-Cookie headers", async () => { await runTest( { fetch(req) { return new Response("hello", { headers: [ ["Another-Header", "1"], ["Set-Cookie", "foo=bar"], ["Set-Cookie", "baz=qux"], ], }); }, }, async server => { const response = await fetch(server.url.origin); expect(response.headers.getAll("Set-Cookie")).toEqual(["foo=bar", "baz=qux"]); expect(response.headers.get("Set-Cookie")).toEqual("foo=bar, baz=qux"); const cloned = response.clone().headers; expect(response.headers.getAll("Set-Cookie")).toEqual(["foo=bar", "baz=qux"]); response.headers.delete("Set-Cookie"); expect(response.headers.getAll("Set-Cookie")).toEqual([]); response.headers.delete("Set-Cookie"); expect(cloned.getAll("Set-Cookie")).toEqual(["foo=bar", "baz=qux"]); expect(new Headers(cloned).getAll("Set-Cookie")).toEqual(["foo=bar", "baz=qux"]); }, ); }); describe("should support Content-Range with Bun.file()", () => { // this must be a big file so we can test potentially multiple chunks // more than 65 KB const full = (function () { const fixture = resolve(import.meta.dir + "/fetch.js.txt"); const chunk = readFileSync(fixture); var whole = new Uint8Array(chunk.byteLength * 128); for (var i = 0; i < 128; i++) { whole.set(chunk, i * chunk.byteLength); } writeFileSync(fixture + ".big", whole); return whole; })(); const fixture = resolve(import.meta.dir + "/fetch.js.txt") + ".big"; const getServer = runTest.bind(null, { fetch(req) { const { searchParams } = new URL(req.url); const start = Number(searchParams.get("start")); const end = Number(searchParams.get("end")); return new Response(Bun.file(fixture).slice(start, end)); }, }); const getServerWithSize = runTest.bind(null, { fetch(req) { const { searchParams } = new URL(req.url); const start = Number(searchParams.get("start")); const end = Number(searchParams.get("end")); const file = Bun.file(fixture); return new Response(file.slice(start, end), { headers: { "Content-Range": "bytes " + start + "-" + end + "/" + file.size, }, }); }, }); const good = [ [0, 1], [1, 2], [0, 10], [10, 20], [0, Infinity], [10, Infinity], [NaN, Infinity], [full.byteLength - 10, full.byteLength], [full.byteLength - 10, full.byteLength - 1], [full.byteLength - 1, full.byteLength], [0, full.byteLength], ] as const; for (const [start, end] of good) { it(`good range: ${start} - ${end}`, async () => { await getServer(async server => { const response = await fetch(`${server.url.origin}/?start=${start}&end=${end}`, { verbose: true, }); expect(await response.arrayBuffer()).toEqual(full.buffer.slice(start, end)); expect(response.status).toBe(start > 0 || end < full.byteLength ? 206 : 200); }); }); } for (const [start, end] of good) { it(`good range with size: ${start} - ${end}`, async () => { await getServerWithSize(async server => { const response = await fetch(`${server.url.origin}/?start=${start}&end=${end}`, { verbose: true, }); expect(parseInt(response.headers.get("Content-Range")?.split("/")[1])).toEqual(full.byteLength); expect(await response.arrayBuffer()).toEqual(full.buffer.slice(start, end)); expect(response.status).toBe(start > 0 || end < full.byteLength ? 206 : 200); }); }); } const emptyRanges = [ [0, 0], [1, 1], [10, 10], [-Infinity, -Infinity], [Infinity, Infinity], [NaN, NaN], [(full.byteLength / 2) | 0, (full.byteLength / 2) | 0], [full.byteLength, full.byteLength], [full.byteLength - 1, full.byteLength - 1], ]; for (const [start, end] of emptyRanges) { it(`empty range: ${start} - ${end}`, async () => { await getServer(async server => { const response = await fetch(`${server.url.origin}/?start=${start}&end=${end}`); const out = await response.arrayBuffer(); expect(out).toEqual(new ArrayBuffer(0)); expect(response.status).toBe(206); }); }); } const badRanges = [ [10, NaN], [10, -Infinity], [-(full.byteLength / 2) | 0, Infinity], [-(full.byteLength / 2) | 0, -Infinity], [full.byteLength + 100, full.byteLength], [full.byteLength + 100, full.byteLength + 100], [full.byteLength + 100, full.byteLength + 1], [full.byteLength + 100, -full.byteLength], ]; for (const [start, end] of badRanges) { it(`bad range: ${start} - ${end}`, async () => { await getServer(async server => { const response = await fetch(`${server.url.origin}/?start=${start}&end=${end}`); const out = await response.arrayBuffer(); expect(out).toEqual(new ArrayBuffer(0)); expect(response.status).toBe(206); }); }); } }); it("formats error responses correctly", async () => { const { promise, resolve, reject } = Promise.withResolvers(); const c = spawn(bunExe(), ["./error-response.js"], { cwd: import.meta.dir, env: bunEnv, }); var output = ""; c.stderr.on("data", chunk => { output += chunk.toString(); }); c.stderr.on("end", () => { try { expect(output).toContain('throw new Error("1");'); resolve(); } catch (e) { reject(e); } finally { c.kill(); } }); await promise; }); it("request body and signal life cycle", async () => { renderToReadableStream = (await import("react-dom/server.browser")).renderToReadableStream; app_jsx = (await import("./app")).default; { const headers = { headers: { "Content-Type": "text/html", }, }; using server = Bun.serve({ port: 0, async fetch(req) { return new Response(await renderToReadableStream(app_jsx), headers); }, }); for (let j = 0; j < 10; j++) { const batchSize = 64; const requests = []; for (let i = 0; i < batchSize; i++) { requests.push(fetch(server.url.origin)); } await Promise.all(requests); Bun.gc(true); } await Bun.sleep(10); expect().pass(); } }, 30_000); it("propagates content-type from a Bun.file()'s file path in fetch()", async () => { const body = Bun.file(import.meta.dir + "/fetch.js.txt"); const bodyText = await body.text(); using server = Bun.serve({ port: 0, development: false, async fetch(req) { expect(req.headers.get("Content-Type")).toBe("text/plain;charset=utf-8"); const text = await req.text(); expect(text).toBe(bodyText); return new Response(Bun.file(import.meta.dir + "/fetch.js.txt")); }, }); // @ts-ignore const reqBody = new Request(server.url.origin, { body, method: "POST", }); const res = await fetch(reqBody); expect(res.status).toBe(200); // but it does for Response expect(res.headers.get("Content-Type")).toBe("text/plain;charset=utf-8"); }); it("does propagate type for Blob", async () => { using server = Bun.serve({ port: 0, development: false, async fetch(req) { expect(req.headers.get("Content-Type")).toBe("text/plain;charset=utf-8"); return new Response(new Blob(["hey"], { type: "text/plain;charset=utf-8" })); }, }); const body = new Blob(["hey"], { type: "text/plain;charset=utf-8" }); // @ts-ignore const res = await fetch(server.url.origin, { body, method: "POST", }); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toBe("text/plain;charset=utf-8"); }); it("unix socket connection in Bun.serve", async () => { const unix = join(tmpdir(), "bun." + Date.now() + ((Math.random() * 32) | 0).toString(16) + ".sock"); using server = Bun.serve({ unix, async fetch(req) { expect(req.headers.get("Content-Type")).toBeNull(); return new Response(new Blob(["hey"], { type: "text/plain;charset=utf-8" })); }, }); const requestText = `GET / HTTP/1.1\r\nHost: localhost\r\n\r\n`; const received: Buffer[] = []; const { resolve, promise } = Promise.withResolvers(); await using connection = await Bun.connect({ unix, socket: { data(socket, data) { received.push(data); resolve(); }, }, }); connection.write(requestText); connection.flush(); await promise; expect(Buffer.concat(received).toString()).toEndWith("\r\n\r\nhey"); connection.end(); }); it("unix socket connection throws an error on a bad domain without crashing", async () => { const unix = "/i/don/tevent/exist/because/the/directory/is/invalid/yes.sock"; expect(() => { using server = Bun.serve({ port: 0, unix, async fetch(req) { expect(req.headers.get("Content-Type")).toBeNull(); return new Response(new Blob(["hey"], { type: "text/plain;charset=utf-8" })); }, }); }).toThrow(); }); it("#5859 text", async () => { using server = Bun.serve({ port: 0, development: false, async fetch(req) { return new Response(await req.text(), {}); }, }); const response = await fetch(server.url.origin, { method: "POST", body: new Uint8Array([0xfd]), }); expect(await response.text()).toBe("�"); }); it("#5859 json", async () => { using server = Bun.serve({ port: 0, async fetch(req) { try { const json = await req.json(); console.log({ json }); } catch (e) { return new Response(e?.message!, { status: 500 }); } return new Response("SHOULD'VE FAILED", {}); }, }); const response = await fetch(server.url.origin, { method: "POST", body: new Uint8Array([0xfd]), }); expect(await response.text()).toBe("Failed to parse JSON"); expect(response.ok).toBeFalse(); }); it("#5859 arrayBuffer", async () => { const tmp = join(tmpdirSync(), "bad"); await Bun.write(tmp, new Uint8Array([0xfd])); expect(async () => await Bun.file(tmp).json()).toThrow(); }); describe("server.requestIP", () => { it.if(isIPv4())("v4", async () => { using server = Bun.serve({ port: 0, fetch(req, server) { const ip = server.requestIP(req); console.log(ip); return Response.json(ip); }, hostname: "127.0.0.1", }); const response = await fetch(server.url.origin).then(x => x.json()); expect(response).toMatchObject({ address: "127.0.0.1", family: "IPv4", port: expect.any(Number), }); }); it.if(isIPv6())("v6", async () => { using server = Bun.serve({ port: 0, fetch(req, server) { return Response.json(server.requestIP(req)); }, hostname: "::1", }); const response = await fetch(`http://localhost:${server.port}`).then(x => x.json()); expect(response).toMatchObject({ address: "::1", family: "IPv6", port: expect.any(Number), }); }); it.if(isPosix)("server.requestIP (unix)", async () => { const unix = join(tmpdirSync(), "serve.sock"); using server = Bun.serve({ unix, fetch(req, server) { return Response.json(server.requestIP(req)); }, }); const requestText = `GET / HTTP/1.1\r\nHost: localhost\r\n\r\n`; const received: Buffer[] = []; const { resolve, promise } = Promise.withResolvers(); const connection = await Bun.connect({ unix, socket: { data(socket, data) { received.push(data); resolve(); }, }, }); connection.write(requestText); connection.flush(); await promise; expect(Buffer.concat(received).toString()).toEndWith("\r\n\r\nnull"); connection.end(); }); }); it("should response with HTTP 413 when request body is larger than maxRequestBodySize, issue#6031", async () => { using server = Bun.serve({ port: 0, maxRequestBodySize: 10, fetch(req, server) { return new Response("OK"); }, }); { const resp = await fetch(server.url.origin, { method: "POST", body: "A".repeat(10), }); expect(resp.status).toBe(200); expect(await resp.text()).toBe("OK"); } { const resp = await fetch(server.url.origin, { method: "POST", body: "A".repeat(11), }); expect(resp.status).toBe(413); } }); it("should support promise returned from error", async () => { const { promise, resolve } = Promise.withResolvers(); await using subprocess = Bun.spawn({ cwd: import.meta.dirname, cmd: [bunExe(), "bun-serve.fixture.js"], env: bunEnv, stdout: "ignore", stderr: "pipe", ipc(message) { resolve(message); }, }); const url = new URL(await promise); { const resp = await fetch(new URL("async-fulfilled", url)); expect(resp.status).toBe(200); expect(resp.text()).resolves.toBe("Async fulfilled"); } { const resp = await fetch(new URL("async-rejected", url)); expect(resp.status).toBe(500); } { const resp = await fetch(new URL("async-pending", url)); expect(resp.status).toBe(200); expect(resp.text()).resolves.toBe("Async pending"); } { const resp = await fetch(new URL("async-rejected-pending", url)); expect(resp.status).toBe(500); } subprocess.kill(); }); if (process.platform === "linux") it("should use correct error when using a root range port(#7187)", () => { expect(() => { using server = Bun.serve({ port: 1003, fetch(req) { return new Response("request answered"); }, }); }).toThrow("permission denied 0.0.0.0:1003"); }); describe.concurrent("should error with invalid options", async () => { it("requestCert", () => { expect(() => { Bun.serve({ port: 0, fetch(req) { return new Response("hi"); }, tls: { requestCert: "invalid", }, }); }).toThrow("TLSOptions.requestCert must be a boolean"); }); it("rejectUnauthorized", () => { expect(() => { Bun.serve({ port: 0, fetch(req) { return new Response("hi"); }, tls: { rejectUnauthorized: "invalid", }, }); }).toThrow("TLSOptions.rejectUnauthorized must be a boolean"); }); it("lowMemoryMode", () => { expect(() => { Bun.serve({ port: 0, fetch(req) { return new Response("hi"); }, tls: { rejectUnauthorized: true, lowMemoryMode: "invalid", }, }); }).toThrow("TLSOptions.lowMemoryMode must be a boolean"); }); it("multiple missing server name", () => { expect(() => { Bun.serve({ port: 0, fetch(req) { return new Response("hi"); }, tls: [ { key: "lkwejflkwjeflkj", }, { key: "lkwjefhwlkejfklwj", }, ], }); }).toThrow("SNI tls object must have a serverName"); }); }); it.concurrent("should resolve pending promise if requested ended with pending read", async () => { let error: Error; function shouldError(e: Error) { error = e; } let is_done = false; function shouldMarkDone(result: { done: boolean; value: any }) { is_done = result.done; } await runTest( { fetch(req) { // @ts-ignore req.body?.getReader().read().then(shouldMarkDone).catch(shouldError); return new Response("OK"); }, }, async server => { const response = await fetch(server.url.origin, { method: "POST", body: "1".repeat(64 * 1024), }); const text = await response.text(); expect(text).toContain("OK"); expect(is_done).toBe(false); expect(error).toBeDefined(); expect(error.name).toContain("AbortError"); }, ); }); it.concurrent("should work with dispose keyword", async () => { let url: string; { using server = Bun.serve({ port: 0, fetch() { return new Response("OK"); }, }); url = server.url; expect((await fetch(url)).status).toBe(200); } expect(fetch(url)).rejects.toThrow(); }); // prettier-ignore it("should be able to stop in the middle of a file response", async () => { async function doRequest(url: string) { try { const response = await fetch(url, { signal: AbortSignal.timeout(10) }); const read = (response.body as ReadableStream).getReader(); while (true) { const { value, done } = await read.read(); if (done) break; } expect(response.status).toBe(200); } catch {} } const fixture = join(import.meta.dir, "server-bigfile-send.fixture.js"); for (let i = 0; i < 3; i++) { const process = Bun.spawn([bunExe(), fixture], { env: bunEnv, stderr: "inherit", stdout: "pipe", stdin: "ignore", }); const { value } = await process.stdout.getReader().read(); const url = new TextDecoder().decode(value).trim(); const requests = []; for (let j = 0; j < 5_000; j++) { requests.push(doRequest(url)); } // only await for 1k requests (and kill the process) await Promise.all(requests.slice(0, 1_000)); expect(process.exitCode || 0).toBe(0); process.kill(); } }, 60_000); it("should be able to abrupt stop the server", async () => { for (let i = 0; i < 10; i++) { using server = Bun.serve({ port: 0, error() { return new Response("Error", { status: 500 }); }, async fetch(req, server) { server.stop(true); await Bun.sleep(100); return new Response("Hello, World!"); }, }); try { await fetch(server.url).then(res => res.text()); expect.unreachable(); } catch (e) { expect(e.code).toBe("ECONNRESET"); } } }); it.concurrent("should not instanciate error instances in each request", async () => { const startErrorCount = heapStats().objectTypeCounts.Error || 0; using server = Bun.serve({ port: 0, async fetch(req, server) { return new Response("bun"); }, }); const batchSize = 100; const batch = new Array(batchSize); for (let i = 0; i < 1000; i++) { batch[i % batchSize] = await fetch(server.url, { method: "POST", body: "bun", }); if (i % batchSize === batchSize - 1) { await Promise.all(batch); } } expect(heapStats().objectTypeCounts.Error || 0).toBeLessThanOrEqual(startErrorCount); }); it("should be able to abort a sendfile response and streams", async () => { const bigfile = join(import.meta.dir, "../../web/encoding/utf8-encoding-fixture.bin"); using server = serve({ port: 0, tls, hostname: "localhost", async fetch() { return new Response(file(bigfile), { headers: { "Content-Type": "text/html" }, }); }, }); async function doRequest() { try { const controller = new AbortController(); const res = await fetch(server.url, { signal: controller.signal, tls: { rejectUnauthorized: false }, }); res.body ?.getReader() .read() .catch(() => {}); controller.abort(); } catch {} } const batchSize = 20; const batch = []; for (let i = 0; i < 500; i++) { batch.push(doRequest()); if (batch.length === batchSize) { await Promise.all(batch); batch.length = 0; } } await Promise.all(batch); expect().pass(); }, 10_000); it.concurrent("should not send extra bytes when using sendfile", async () => { const payload = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); const tmpFile = join(tmpdirSync(), "test.bin"); await Bun.write(tmpFile, payload); using serve = Bun.serve({ port: 0, fetch(req) { const pathname = new URL(req.url).pathname; if (pathname === "/file") { return new Response(Bun.file(tmpFile), { headers: { "Content-Type": "plain/text", }, }); } return new Response("Not Found", { status: 404, }); }, }); // manually fetch the file using sockets, and get the whole content const { promise, resolve, reject } = Promise.withResolvers(); const socket = net.connect(serve.port, "localhost", () => { socket.write("GET /file HTTP/1.1\r\nHost: localhost\r\n\r\n"); setTimeout(() => { socket.end(); // wait a bit before closing the connection so we get the whole content }, 100); }); let body: Buffer | null = null; let content_length = 0; let headers = ""; socket.on("data", data => { if (body) { body = Buffer.concat([body as Buffer, data]); return; } // parse headers const str = data.toString("utf8"); const index = str.indexOf("\r\n\r\n"); if (index === -1) { headers += str; return; } headers += str.slice(0, index); const lines = headers.split("\r\n"); for (const line of lines) { const [key, value] = line.split(": "); if (key.toLowerCase() === "content-length") { content_length = Number.parseInt(value, 10); } } body = data.subarray(index + 4); }); socket.on("error", reject); socket.on("close", () => { resolve(body); }); expect(await promise).toEqual(Buffer.from(payload)); expect(content_length).toBe(payload.byteLength); }); it.concurrent("we should always send date", async () => { const payload = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); const tmpFile = join(tmpdirSync(), "test.bin"); await Bun.write(tmpFile, payload); using serve = Bun.serve({ port: 0, fetch(req) { const pathname = new URL(req.url).pathname; if (pathname === "/file") { return new Response(Bun.file(tmpFile), { headers: { "Content-Type": "plain/text", }, }); } if (pathname === "/file2") { return new Response(Bun.file(tmpFile)); } if (pathname === "/stream") { return new Response( new ReadableStream({ async pull(controller) { await Bun.sleep(10); controller.enqueue(payload); await Bun.sleep(10); controller.close(); }, }), ); } return new Response("Hello, World!"); }, }); { const res = await fetch(new URL("/file", serve.url.origin)); expect(res.headers.has("Date")).toBeTrue(); } { const res = await fetch(new URL("/file2", serve.url.origin)); expect(res.headers.has("Date")).toBeTrue(); } { const res = await fetch(new URL("/", serve.url.origin)); expect(res.headers.has("Date")).toBeTrue(); } { const res = await fetch(new URL("/stream", serve.url.origin)); expect(res.headers.has("Date")).toBeTrue(); } }); it.concurrent( "should allow use of custom timeout", async () => { using server = Bun.serve({ port: 0, idleTimeout: 8, // uws precision is in seconds, and lower than 4 seconds is not reliable its timer is not that accurate async fetch(req) { const url = new URL(req.url); return new Response( new ReadableStream({ async pull(controller) { controller.enqueue("Hello,"); if (url.pathname === "/timeout") { await Bun.sleep(10000); } else { await Bun.sleep(10); } controller.enqueue(" World!"); controller.close(); }, }), { headers: { "Content-Type": "text/plain" } }, ); }, }); async function testTimeout(pathname: string, success: boolean) { const res = await fetch(new URL(pathname, server.url.origin)); expect(res.status).toBe(200); if (success) { expect(res.text()).resolves.toBe("Hello, World!"); } else { expect(res.text()).rejects.toThrow(/The socket connection was closed unexpectedly./); } } await Promise.all([testTimeout("/ok", true), testTimeout("/timeout", false)]); }, 15_000, ); it.concurrent( "should reset timeout after writes", async () => { // the default is 10s so we send 15 // this test should take 15s at most const CHUNKS = 15; const payload = Buffer.from(`data: ${Date.now()}\n\n`); using server = Bun.serve({ idleTimeout: 5, port: 0, fetch(request, server) { let controller!: ReadableStreamDefaultController; let count = CHUNKS; let interval = setInterval(() => { controller.enqueue(payload); count--; if (count == 0) { clearInterval(interval); interval = null; controller.close(); return; } }, 1000); return new Response( new ReadableStream({ start(_controller) { controller = _controller; }, cancel(controller) { if (interval) clearInterval(interval); }, }), { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", }, }, ); }, }); let received = 0; const response = await fetch(server.url); const stream = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await stream.read(); received += value?.length || 0; if (done) break; } expect(received).toBe(CHUNKS * payload.byteLength); }, 20_000, ); it.concurrent("allow requestIP after async operation", async () => { using server = Bun.serve({ port: 0, async fetch(req, server) { await Bun.sleep(1); return new Response(JSON.stringify(server.requestIP(req))); }, }); const ip = await fetch(server.url).then(res => res.json()); expect(ip).not.toBeNull(); expect(ip.port).toBeInteger(); expect(ip.address).toBeString(); expect(ip.family).toBeString(); }); it.concurrent( "allow custom timeout per request", async () => { using server = Bun.serve({ idleTimeout: 1, port: 0, async fetch(req, server) { server.timeout(req, 60); await Bun.sleep(10000); //uWS precision is not great return new Response("Hello, World!"); }, }); expect(server.timeout).toBeFunction(); const res = await fetch(new URL("/long-timeout", server.url.origin)); expect(res.status).toBe(200); expect(res.text()).resolves.toBe("Hello, World!"); }, 20_000, ); it.concurrent("#6462", async () => { let headers: string[] = []; using server = Bun.serve({ port: 0, async fetch(request) { for (const key of request.headers.keys()) { headers = headers.concat([[key, request.headers.get(key)]]); } return new Response( JSON.stringify({ "headers": headers, }), { status: 200 }, ); }, }); const bytes = Buffer.from(`GET / HTTP/1.1\r\nConnection: close\r\nHost: ${server.hostname}\r\nTest!: test\r\n\r\n`); const { promise, resolve } = Promise.withResolvers(); await Bun.connect({ port: server.port, hostname: server.hostname, socket: { open(socket) { const wrote = socket.write(bytes); console.log("wrote", wrote); }, data(socket, data) { console.log(data.toString("utf8")); }, close(socket) { resolve(); }, }, }); await promise; expect(headers).toStrictEqual([ ["connection", "close"], ["host", "localhost"], ["test!", "test"], ]); }); it.concurrent("#6583", async () => { const callback = mock(); using server = Bun.serve({ fetch: callback, port: 0, hostname: "localhost", }); const { promise, resolve } = Promise.withResolvers(); await Bun.connect({ port: server.port, hostname: server.hostname, tls: true, socket: { open(socket) { socket.write("GET / HTTP/1.1\r\nConnection: close\r\nHost: localhost\r\n\r\n"); }, data(socket, data) { console.log(data.toString("utf8")); }, close(socket) { resolve(); }, }, }); await promise; expect(callback).not.toHaveBeenCalled(); }); it.concurrent("do the best effort to flush everything", async () => { using server = Bun.serve({ port: 0, async fetch(req) { return new Response( new ReadableStream({ type: "direct", async pull(ctrl) { ctrl.write("b"); await Bun.sleep(10); ctrl.write("un"); }, }), ); }, }); let response = await fetch(server.url); expect(await response.text()).toBe("bun"); }); it.concurrent("#20283", async () => { using server = Bun.serve({ routes: { "/": async req => { // calling clone() with no cookies should not crash const cloned = req.clone(); return Response.json({ cookies: req.cookies, clonedCookies: cloned.cookies, }); }, }, port: 0, }); const response = await fetch(server.url); const json = await response.json(); // there should be no cookies and the clone should have succeeded expect(json).toEqual({ cookies: {}, clonedCookies: {} }); });