Files
bun.sh/test/js/bun/http/serve.test.ts
taylor.fish f14f3b03bb Add new bindings generator; port SSLConfig (#23169)
Add a new generator for JS → Zig bindings. The bulk of the conversion is
done in C++, after which the data is transformed into an FFI-safe
representation, passed to Zig, and then finally transformed into
idiomatic Zig types.

In its current form, the new bindings generator supports:

* Signed and unsigned integers
* Floats (plus a “finite” variant that disallows NaN and infinities)
* Strings
* ArrayBuffer (accepts ArrayBuffer, TypedArray, or DataView)
* Blob
* Optional types
* Nullable types (allows null, whereas Optional only allows undefined)
* Arrays
* User-defined string enumerations
* User-defined unions (fields can optionally be named to provide a
better experience in Zig)
* Null and undefined, for use in unions (can more efficiently represent
optional/nullable unions than wrapping a union in an optional)
* User-defined dictionaries (arbitrary key-value pairs; expects a JS
object and parses it into a struct)
* Default values for dictionary members
* Alternative names for dictionary members (e.g., to support both
`serverName` and `servername` without taking up twice the space)
* Descriptive error messages
* Automatic `fromJS` functions in Zig for dictionaries
* Automatic `deinit` functions for the generated Zig types

Although this bindings generator has many features not present in
`bindgen.ts`, it does not yet implement all of `bindgen.ts`'s
functionality, so for the time being, it has been named `bindgenv2`, and
its configuration is specified in `.bindv2.ts` files. Once all
`bindgen.ts`'s functionality has been incorporated, it will be renamed.

This PR ports `SSLConfig` to use the new bindings generator; see
`SSLConfig.bindv2.ts`.

(For internal tracking: fixes STAB-1319, STAB-1322, STAB-1323,
STAB-1324)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Alistair Smith <hi@alistair.sh>
2025-10-03 17:10:28 -07:00

2192 lines
60 KiB
TypeScript
Raw Permalink Blame History

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<any>, test: (server: Server) => Promise<void> | 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<string, string>;
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("<22>");
});
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<void>();
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<string>();
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<any>).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: {} });
});