import { AnyFunction, serve, ServeOptions, Server, sleep } from "bun";
import { afterAll, afterEach, beforeAll, describe, expect, it, beforeEach } from "bun:test";
import { chmodSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "fs";
import { mkfifo } from "mkfifo";
import { tmpdir } from "os";
import { join } from "path";
import { gc, withoutAggressiveGC, gcTick } from "harness";
import net from "net";
const tmp_dir = mkdtempSync(join(realpathSync(tmpdir()), "fetch.test"));
const fixture = readFileSync(join(import.meta.dir, "fetch.js.txt"), "utf8");
let server: Server;
function startServer({ fetch, ...options }: ServeOptions) {
server = serve({
...options,
fetch,
port: 0,
});
}
afterEach(() => {
server?.stop?.(true);
});
afterAll(() => {
rmSync(tmp_dir, { force: true, recursive: true });
});
const payload = new Uint8Array(1024 * 1024 * 2);
crypto.getRandomValues(payload);
it("new Request(invalid url) throws", () => {
expect(() => new Request("http")).toThrow();
expect(() => new Request("")).toThrow();
expect(() => new Request("http://[::1")).toThrow();
expect(() => new Request("https://[::1")).toThrow();
expect(() => new Request("!")).toThrow();
});
describe("fetch data urls", () => {
it("basic", async () => {
var url =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
var res = await fetch(url);
expect(res.status).toBe(200);
expect(res.statusText).toBe("OK");
expect(res.ok).toBe(true);
var blob = await res.blob();
expect(blob.size).toBe(85);
expect(blob.type).toBe("image/png");
});
it("percent encoded", async () => {
var url = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D";
var res = await fetch(url);
expect(res.status).toBe(200);
expect(res.statusText).toBe("OK");
expect(res.ok).toBe(true);
var blob = await res.blob();
expect(blob.size).toBe(13);
expect(blob.type).toBe("text/plain;charset=utf-8");
expect(blob.text()).resolves.toBe("Hello, World!");
});
it("percent encoded (invalid)", async () => {
var url = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3";
expect(async () => {
await fetch(url);
}).toThrow("failed to fetch the data URL");
});
it("plain text", async () => {
var url = "data:,Hello%2C%20World!";
var res = await fetch(url);
expect(res.status).toBe(200);
expect(res.statusText).toBe("OK");
expect(res.ok).toBe(true);
var blob = await res.blob();
expect(blob.size).toBe(13);
expect(blob.type).toBe("text/plain;charset=utf-8");
expect(blob.text()).resolves.toBe("Hello, World!");
url = "data:,helloworld!";
res = await fetch(url);
expect(res.status).toBe(200);
expect(res.statusText).toBe("OK");
expect(res.ok).toBe(true);
blob = await res.blob();
expect(blob.size).toBe(11);
expect(blob.type).toBe("text/plain;charset=utf-8");
expect(blob.text()).resolves.toBe("helloworld!");
});
it("unstrict parsing of invalid URL characters", async () => {
var url = "data:application/json,{%7B%7D}";
var res = await fetch(url);
expect(res.status).toBe(200);
expect(res.statusText).toBe("OK");
expect(res.ok).toBe(true);
var blob = await res.blob();
expect(blob.size).toBe(4);
expect(blob.type).toBe("application/json;charset=utf-8");
expect(blob.text()).resolves.toBe("{{}}");
});
it("unstrict parsing of double percent characters", async () => {
var url = "data:application/json,{%%7B%7D%%}%%";
var res = await fetch(url);
expect(res.status).toBe(200);
expect(res.statusText).toBe("OK");
expect(res.ok).toBe(true);
var blob = await res.blob();
expect(blob.size).toBe(9);
expect(blob.type).toBe("application/json;charset=utf-8");
expect(blob.text()).resolves.toBe("{%{}%%}%%");
});
it("data url (invalid)", async () => {
var url = "data:Hello%2C%20World!";
expect(async () => {
await fetch(url);
}).toThrow("failed to fetch the data URL");
});
it("emoji", async () => {
var url = "data:,π";
var res = await fetch(url);
expect(res.status).toBe(200);
expect(res.statusText).toBe("OK");
expect(res.ok).toBe(true);
var blob = await res.blob();
expect(blob.size).toBe(4);
expect(blob.type).toBe("text/plain;charset=utf-8");
expect(blob.text()).resolves.toBe("π");
});
it("should work with Request", async () => {
var req = new Request("data:,Hello%2C%20World!");
var res = await fetch(req);
expect(res.status).toBe(200);
expect(res.statusText).toBe("OK");
expect(res.ok).toBe(true);
var blob = await res.blob();
expect(blob.size).toBe(13);
expect(blob.type).toBe("text/plain;charset=utf-8");
expect(blob.text()).resolves.toBe("Hello, World!");
req = new Request("data:,π");
res = await fetch(req);
expect(res.status).toBe(200);
expect(res.statusText).toBe("OK");
expect(res.ok).toBe(true);
blob = await res.blob();
expect(blob.size).toBe(4);
expect(blob.type).toBe("text/plain;charset=utf-8");
expect(blob.text()).resolves.toBe("π");
});
it("should work with Request (invalid)", async () => {
var req = new Request("data:Hello%2C%20World!");
expect(async () => {
await fetch(req);
}).toThrow("failed to fetch the data URL");
req = new Request("data:Hello%345632");
expect(async () => {
await fetch(req);
}).toThrow("failed to fetch the data URL");
});
});
describe("AbortSignal", () => {
beforeEach(() => {
startServer({
async fetch(request) {
if (request.url.endsWith("/nodelay")) {
return new Response("Hello");
}
if (request.url.endsWith("/stream")) {
const reader = request.body!.getReader();
const body = new ReadableStream({
async pull(controller) {
if (!reader) controller.close();
const { done, value } = await reader.read();
// When no more data needs to be consumed, close the stream
if (done) {
controller.close();
return;
}
// Enqueue the next data chunk into our target stream
controller.enqueue(value);
},
});
return new Response(body);
}
if (request.method.toUpperCase() === "POST") {
const body = await request.text();
return new Response(body);
}
await sleep(15);
return new Response("Hello");
},
});
});
afterEach(() => {
server?.stop?.(true);
});
it("AbortError", async () => {
const controller = new AbortController();
const signal = controller.signal;
expect(async () => {
async function manualAbort() {
await sleep(1);
controller.abort();
}
await Promise.all([
fetch(`http://127.0.0.1:${server.port}`, { signal: signal }).then(res => res.text()),
manualAbort(),
]);
}).toThrow(new DOMException("The operation was aborted."));
});
it("AbortAfterFinish", async () => {
const controller = new AbortController();
const signal = controller.signal;
await fetch(`http://127.0.0.1:${server.port}/nodelay`, { signal: signal }).then(async res =>
expect(await res.text()).toBe("Hello"),
);
controller.abort();
});
it("AbortErrorWithReason", async () => {
const controller = new AbortController();
const signal = controller.signal;
expect(async () => {
async function manualAbort() {
await sleep(10);
controller.abort(new Error("My Reason"));
}
await Promise.all([
fetch(`http://127.0.0.1:${server.port}`, { signal: signal }).then(res => res.text()),
manualAbort(),
]);
}).toThrow("My Reason");
});
it("AbortErrorEventListener", async () => {
const controller = new AbortController();
const signal = controller.signal;
signal.addEventListener("abort", ev => {
const target = ev.currentTarget!;
expect(target).toBeDefined();
expect(target.aborted).toBe(true);
expect(target.reason).toBeDefined();
expect(target.reason!.name).toBe("AbortError");
});
expect(async () => {
async function manualAbort() {
await sleep(10);
controller.abort();
}
await Promise.all([
fetch(`http://127.0.0.1:${server.port}`, { signal: signal }).then(res => res.text()),
manualAbort(),
]);
}).toThrow(new DOMException("The operation was aborted."));
});
it("AbortErrorWhileUploading", async () => {
const controller = new AbortController();
expect(async () => {
await fetch(`http://localhost:${server.port}`, {
method: "POST",
body: new ReadableStream({
pull(event_controller) {
event_controller.enqueue(new Uint8Array([1, 2, 3, 4]));
//this will abort immediately should abort before connected
controller.abort();
},
}),
signal: controller.signal,
});
}).toThrow(new DOMException("The operation was aborted."));
});
it("TimeoutError", async () => {
const signal = AbortSignal.timeout(10);
try {
await fetch(`http://127.0.0.1:${server.port}`, { signal: signal }).then(res => res.text());
expect(() => {}).toThrow();
} catch (ex: any) {
expect(ex.name).toBe("TimeoutError");
}
});
it("Request", async () => {
const controller = new AbortController();
const signal = controller.signal;
async function manualAbort() {
await sleep(10);
controller.abort();
}
try {
const request = new Request(`http://127.0.0.1:${server.port}`, { signal });
await Promise.all([fetch(request).then(res => res.text()), manualAbort()]);
expect(() => {}).toThrow();
} catch (ex: any) {
expect(ex.name).toBe("AbortError");
}
});
});
describe("Headers", () => {
it(".toJSON", () => {
const headers = new Headers({
"content-length": "123",
"content-type": "text/plain",
"x-another-custom-header": "Hello World",
"x-custom-header": "Hello World",
});
expect(JSON.stringify(headers.toJSON(), null, 2)).toBe(
JSON.stringify(Object.fromEntries(headers.entries()), null, 2),
);
});
it(".getSetCookie() with object", () => {
const headers = new Headers({
"content-length": "123",
"content-type": "text/plain",
"x-another-custom-header": "Hello World",
"x-custom-header": "Hello World",
"Set-Cookie": "foo=bar; Path=/; HttpOnly",
});
expect(headers.count).toBe(5);
expect(headers.getAll("set-cookie")).toEqual(["foo=bar; Path=/; HttpOnly"]);
});
it(".getSetCookie() with array", () => {
const headers = new Headers([
["content-length", "123"],
["content-type", "text/plain"],
["x-another-custom-header", "Hello World"],
["x-custom-header", "Hello World"],
["Set-Cookie", "foo=bar; Path=/; HttpOnly"],
["Set-Cookie", "foo2=bar2; Path=/; HttpOnly"],
]);
expect(headers.count).toBe(6);
expect(headers.getAll("set-cookie")).toEqual(["foo=bar; Path=/; HttpOnly", "foo2=bar2; Path=/; HttpOnly"]);
});
it("Set-Cookies init", () => {
const headers = new Headers([
["Set-Cookie", "foo=bar"],
["Set-Cookie", "bar=baz"],
["X-bun", "abc"],
["X-bun", "def"],
]);
const actual = [...headers];
expect(actual).toEqual([
["x-bun", "abc, def"],
["set-cookie", "foo=bar"],
["set-cookie", "bar=baz"],
]);
expect([...headers.values()]).toEqual(["abc, def", "foo=bar", "bar=baz"]);
});
it("Headers append multiple", () => {
const headers = new Headers([
["Set-Cookie", "foo=bar"],
["X-bun", "foo"],
]);
headers.append("Set-Cookie", "bar=baz");
headers.append("x-bun", "bar");
const actual = [...headers];
// we do not preserve the order
// which is kind of bad
expect(actual).toEqual([
["x-bun", "foo, bar"],
["set-cookie", "foo=bar"],
["set-cookie", "bar=baz"],
]);
});
it("append duplicate set cookie key", () => {
const headers = new Headers([["Set-Cookie", "foo=bar"]]);
headers.append("set-Cookie", "foo=baz");
headers.append("Set-cookie", "baz=bar");
const actual = [...headers];
expect(actual).toEqual([
["set-cookie", "foo=bar"],
["set-cookie", "foo=baz"],
["set-cookie", "baz=bar"],
]);
});
it("set duplicate cookie key", () => {
const headers = new Headers([["Set-Cookie", "foo=bar"]]);
headers.set("set-Cookie", "foo=baz");
headers.set("set-cookie", "bar=qat");
const actual = [...headers];
expect(actual).toEqual([["set-cookie", "bar=qat"]]);
});
it("should include set-cookie headers in array", () => {
const headers = new Headers();
headers.append("Set-Cookie", "foo=bar");
headers.append("Content-Type", "text/plain");
const actual = [...headers];
expect(actual).toEqual([
["content-type", "text/plain"],
["set-cookie", "foo=bar"],
]);
});
});
describe("fetch", () => {
const urls = [
"https://example.com",
"http://example.com",
new URL("https://example.com"),
new Request({ url: "https://example.com" }),
{ toString: () => "https://example.com" } as string,
];
for (let url of urls) {
gc();
let name: string;
if (url instanceof URL) {
name = "URL: " + url;
} else if (url instanceof Request) {
name = "Request: " + url.url;
} else if (url.hasOwnProperty("toString")) {
name = "Object: " + url.toString();
} else {
name = url as string;
}
it(name, async () => {
gc();
const response = await fetch(url, { verbose: true });
gc();
const text = await response.text();
gc();
expect(fixture).toBe(text);
});
}
it('redirect: "manual"', async () => {
startServer({
fetch(req) {
return new Response(null, {
status: 302,
headers: {
Location: "https://example.com",
},
});
},
});
const response = await fetch(`http://${server.hostname}:${server.port}`, {
redirect: "manual",
});
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("https://example.com");
expect(response.redirected).toBe(true);
});
it('redirect: "follow"', async () => {
startServer({
fetch(req) {
return new Response(null, {
status: 302,
headers: {
Location: "https://example.com",
},
});
},
});
const response = await fetch(`http://${server.hostname}:${server.port}`, {
redirect: "follow",
});
expect(response.status).toBe(200);
expect(response.headers.get("location")).toBe(null);
expect(response.redirected).toBe(true);
});
it('redirect: "error" #2819', async () => {
startServer({
fetch(req) {
return new Response(null, {
status: 302,
headers: {
Location: "https://example.com",
},
});
},
});
try {
const response = await fetch(`http://${server.hostname}:${server.port}`, {
redirect: "error",
});
expect(response).toBeUndefined();
} catch (err: any) {
expect(err.code).toBe("UnexpectedRedirect");
}
});
it("provide body", async () => {
startServer({
fetch(req) {
return new Response(req.body);
},
hostname: "localhost",
});
// POST with body
const url = `http://${server.hostname}:${server.port}`;
const response = await fetch(url, { method: "POST", body: "buntastic" });
expect(response.status).toBe(200);
expect(await response.text()).toBe("buntastic");
});
["GET", "HEAD", "OPTIONS"].forEach(method =>
it(`fail on ${method} with body`, async () => {
const url = `http://${server.hostname}:${server.port}`;
expect(async () => {
await fetch(url, { body: "buntastic" });
}).toThrow("fetch() request with GET/HEAD/OPTIONS method cannot have body.");
}),
);
it("content length is inferred", async () => {
startServer({
fetch(req) {
return new Response(req.headers.get("content-length"));
},
hostname: "localhost",
});
// POST with body
const url = `http://${server.hostname}:${server.port}`;
const response = await fetch(url, { method: "POST", body: "buntastic" });
expect(response.status).toBe(200);
expect(await response.text()).toBe("9");
const response2 = await fetch(url, { method: "POST", body: "" });
expect(response2.status).toBe(200);
expect(await response2.text()).toBe("0");
});
it("should work with ipv6 localhost", async () => {
const server = Bun.serve({
port: 0,
fetch(req) {
return new Response("Pass!");
},
});
let res = await fetch(`http://[::1]:${server.port}`);
expect(await res.text()).toBe("Pass!");
res = await fetch(`http://[::]:${server.port}/`);
expect(await res.text()).toBe("Pass!");
res = await fetch(`http://[0:0:0:0:0:0:0:1]:${server.port}/`);
expect(await res.text()).toBe("Pass!");
res = await fetch(`http://[0000:0000:0000:0000:0000:0000:0000:0001]:${server.port}/`);
expect(await res.text()).toBe("Pass!");
server.stop();
});
});
it("simultaneous HTTPS fetch", async () => {
const urls = ["https://example.com", "https://www.example.com"];
for (let batch = 0; batch < 4; batch++) {
const promises = new Array(20);
for (let i = 0; i < 20; i++) {
promises[i] = fetch(urls[i % 2]);
}
const result = await Promise.all(promises);
expect(result.length).toBe(20);
for (let i = 0; i < 20; i++) {
expect(result[i].status).toBe(200);
expect(await result[i].text()).toBe(fixture);
}
}
});
it("website with tlsextname", async () => {
// irony
await fetch("https://bun.sh", { method: "HEAD" });
});
function testBlobInterface(blobbyConstructor: { (..._: any[]): any }, hasBlobFn?: boolean) {
for (let withGC of [false, true]) {
for (let jsonObject of [
{ hello: true },
{
hello: "π π π π π π
π π€£ π₯² βΊοΈ π π π π π π π π₯° π π π π π π π π π€ͺ π€¨ π§ π€ π π₯Έ π€© π₯³",
},
]) {
it(`${jsonObject.hello === true ? "latin1" : "utf16"} json${withGC ? " (with gc) " : ""}`, async () => {
if (withGC) gc();
var response = blobbyConstructor(JSON.stringify(jsonObject));
if (withGC) gc();
expect(JSON.stringify(await response.json())).toBe(JSON.stringify(jsonObject));
if (withGC) gc();
});
it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> json${
withGC ? " (with gc) " : ""
}`, async () => {
if (withGC) gc();
var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject)));
if (withGC) gc();
expect(JSON.stringify(await response.json())).toBe(JSON.stringify(jsonObject));
if (withGC) gc();
});
it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> invalid json${
withGC ? " (with gc) " : ""
}`, async () => {
if (withGC) gc();
var response = blobbyConstructor(
new TextEncoder().encode(JSON.stringify(jsonObject) + " NOW WE ARE INVALID JSON"),
);
if (withGC) gc();
var failed = false;
try {
await response.json();
} catch (e) {
failed = true;
}
expect(failed).toBe(true);
if (withGC) gc();
});
it(`${jsonObject.hello === true ? "latin1" : "utf16"} text${withGC ? " (with gc) " : ""}`, async () => {
if (withGC) gc();
var response = blobbyConstructor(JSON.stringify(jsonObject));
if (withGC) gc();
expect(await response.text()).toBe(JSON.stringify(jsonObject));
if (withGC) gc();
});
it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> text${
withGC ? " (with gc) " : ""
}`, async () => {
if (withGC) gc();
var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject)));
if (withGC) gc();
expect(await response.text()).toBe(JSON.stringify(jsonObject));
if (withGC) gc();
});
it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer${withGC ? " (with gc) " : ""}`, async () => {
if (withGC) gc();
var response = blobbyConstructor(JSON.stringify(jsonObject));
if (withGC) gc();
const bytes = new TextEncoder().encode(JSON.stringify(jsonObject));
if (withGC) gc();
const compare = new Uint8Array(await response.arrayBuffer());
if (withGC) gc();
withoutAggressiveGC(() => {
for (let i = 0; i < compare.length; i++) {
if (withGC) gc();
expect(compare[i]).toBe(bytes[i]);
if (withGC) gc();
}
});
if (withGC) gc();
});
it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> arrayBuffer${
withGC ? " (with gc) " : ""
}`, async () => {
if (withGC) gc();
var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject)));
if (withGC) gc();
const bytes = new TextEncoder().encode(JSON.stringify(jsonObject));
if (withGC) gc();
const compare = new Uint8Array(await response.arrayBuffer());
if (withGC) gc();
withoutAggressiveGC(() => {
for (let i = 0; i < compare.length; i++) {
if (withGC) gc();
expect(compare[i]).toBe(bytes[i]);
if (withGC) gc();
}
});
if (withGC) gc();
});
hasBlobFn &&
it(`${jsonObject.hello === true ? "latin1" : "utf16"} blob${withGC ? " (with gc) " : ""}`, async () => {
if (withGC) gc();
const text = JSON.stringify(jsonObject);
var response = blobbyConstructor(text);
if (withGC) gc();
const size = new TextEncoder().encode(text).byteLength;
if (withGC) gc();
const blobed = await response.blob();
if (withGC) gc();
expect(blobed instanceof Blob).toBe(true);
if (withGC) gc();
expect(blobed.size).toBe(size);
if (withGC) gc();
expect(blobed.type).toBe("text/plain;charset=utf-8");
const out = await blobed.text();
expect(out).toBe(text);
if (withGC) gc();
await new Promise(resolve => setTimeout(resolve, 1));
if (withGC) gc();
expect(out).toBe(text);
const first = await blobed.arrayBuffer();
const initial = first[0];
first[0] = 254;
const second = await blobed.arrayBuffer();
expect(second[0]).toBe(initial);
expect(first[0]).toBe(254);
});
}
}
}
describe("Bun.file", () => {
let count = 0;
testBlobInterface(data => {
const blob = new Blob([data]);
const buffer = Bun.peek(blob.arrayBuffer()) as ArrayBuffer;
const path = join(tmp_dir, `tmp-${count++}.bytes`);
writeFileSync(path, buffer);
const file = Bun.file(path);
expect(blob.size).toBe(file.size);
expect(file.lastModified).toBeGreaterThan(0);
return file;
});
it("size is Infinity on a fifo", () => {
const path = join(tmp_dir, "test-fifo");
mkfifo(path);
const { size } = Bun.file(path);
expect(size).toBe(Infinity);
});
const method = ["arrayBuffer", "text", "json"] as const;
function forEachMethod(fn: (m: (typeof method)[number]) => any, skip?: AnyFunction) {
for (const m of method) {
(skip ? it.skip : it)(m, fn(m));
}
}
describe("bad permissions throws", () => {
const path = join(tmp_dir, "my-new-file");
beforeAll(async () => {
await Bun.write(path, "hey");
chmodSync(path, 0o000);
});
forEachMethod(
m => () => {
const file = Bun.file(path);
expect(async () => await file[m]()).toThrow("Permission denied");
},
() => {
try {
readFileSync(path);
} catch {
return false;
}
return true;
},
);
});
describe("non-existent file throws", () => {
const path = join(tmp_dir, "does-not-exist");
forEachMethod(m => async () => {
const file = Bun.file(path);
expect(async () => await file[m]()).toThrow("No such file or directory");
});
});
});
describe("Blob", () => {
testBlobInterface(data => new Blob([data]));
it("should have expected content type", async () => {
var response = new Response("
hello
", {
headers: {
"content-type": "multipart/form-data;boundary=boundary",
},
});
expect((await response.blob()).type).toBe("multipart/form-data;boundary=boundary");
response = new Response("hello
", {
headers: {
"content-type": "text/html; charset=utf-8",
},
});
expect((await response.blob()).type).toBe("text/html;charset=utf-8");
response = new Response("hello
", {
headers: {
"content-type": "octet/stream",
},
});
expect((await response.blob()).type).toBe("octet/stream");
response = new Response("hello
", {
headers: {
"content-type": "text/plain;charset=utf-8",
},
});
expect((await response.blob()).type).toBe("text/plain;charset=utf-8");
});
var blobConstructorValues = [
["123", "456"],
["123", 456],
["123", "456", "789"],
["123", 456, 789],
[1, 2, 3, 4, 5, 6, 7, 8, 9],
[Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 9])],
[Uint8Array.from([1, 2, 3, 4]), "5678", 9],
[new Blob([Uint8Array.from([1, 2, 3, 4])]), "5678", 9],
[
new Blob([
new TextEncoder().encode(
"π π π π π π
π π€£ π₯² βΊοΈ π π π π π π π π₯° π π π π π π π π π€ͺ π€¨ π§ π€ π π₯Έ π€© π₯³",
),
]),
],
[
new TextEncoder().encode(
"π π π π π π
π π€£ π₯² βΊοΈ π π π π π π π π₯° π π π π π π π π π€ͺ π€¨ π§ π€ π π₯Έ π€© π₯³",
),
],
] as any[];
var expected = [
"123456",
"123456",
"123456789",
"123456789",
"123456789",
"\x01\x02\x03\x04\x05\x06\x07\t",
"\x01\x02\x03\x0456789",
"\x01\x02\x03\x0456789",
"π π π π π π
π π€£ π₯² βΊοΈ π π π π π π π π₯° π π π π π π π π π€ͺ π€¨ π§ π€ π π₯Έ π€© π₯³",
"π π π π π π
π π€£ π₯² βΊοΈ π π π π π π π π₯° π π π π π π π π π€ͺ π€¨ π§ π€ π π₯Έ π€© π₯³",
];
it(`blobConstructorValues`, async () => {
for (let i = 0; i < blobConstructorValues.length; i++) {
var response = new Blob(blobConstructorValues[i]);
const res = await response.text();
if (res !== expected[i]) {
throw new Error(
`Failed: ${expected[i].split("").map(a => a.charCodeAt(0))}, received: ${res
.split("")
.map(a => a.charCodeAt(0))}`,
);
}
expect(res).toBe(expected[i]);
}
});
for (let withGC of [false, true]) {
it(`Blob.slice() ${withGC ? " with gc" : ""}`, async () => {
var parts = ["hello", " ", "world"];
if (withGC) gc();
var str = parts.join("");
if (withGC) gc();
var combined = new Blob(parts);
if (withGC) gc();
for (let part of parts) {
if (withGC) gc();
expect(await combined.slice(str.indexOf(part), str.indexOf(part) + part.length).text()).toBe(part);
if (withGC) gc();
}
if (withGC) gc();
for (let part of parts) {
if (withGC) gc();
expect(await combined.slice(str.indexOf(part), str.indexOf(part) + part.length).text()).toBe(part);
if (withGC) gc();
}
});
}
});
{
const sample = new TextEncoder().encode("Hello World!");
const typedArrays = [
Uint8Array,
Uint8ClampedArray,
Int8Array,
Uint16Array,
Int16Array,
Uint32Array,
Int32Array,
Float32Array,
Float64Array,
];
const Constructors = [Blob, Response, Request];
for (let withGC of [false, true]) {
for (let TypedArray of typedArrays) {
for (let Constructor of Constructors) {
it(`${Constructor.name} arrayBuffer() with ${TypedArray.name}${withGC ? " with gc" : ""}`, async () => {
const data = new TypedArray(sample);
if (withGC) gc();
const input =
Constructor === Blob ? [data] : Constructor === Request ? { body: data, url: "http://example.com" } : data;
if (withGC) gc();
const blob = new Constructor(input as any);
if (withGC) gc();
const out = await blob.arrayBuffer();
if (withGC) gc();
expect(out instanceof ArrayBuffer).toBe(true);
if (withGC) gc();
expect(out.byteLength).toBe(data.byteLength);
if (withGC) gc();
});
}
}
}
}
describe("Response", () => {
describe("Response.json", () => {
it("works", async () => {
const inputs = ["hellooo", [[123], 456, 789], { hello: "world" }, { ok: "π π π π₯° π " }];
for (let input of inputs) {
const output = JSON.stringify(input);
expect(await Response.json(input).text()).toBe(output);
}
// JSON.stringify() returns undefined
expect(await Response.json().text()).toBe("");
// JSON.stringify("") returns '""'
expect(await Response.json("").text()).toBe('""');
});
it("sets the content-type header", () => {
let response = Response.json("hello");
expect(response.type).toBe("default");
expect(response.headers.get("content-type")).toBe("application/json;charset=utf-8");
expect(response.status).toBe(200);
});
it("supports number status code", () => {
let response = Response.json("hello", 407);
expect(response.type).toBe("default");
expect(response.headers.get("content-type")).toBe("application/json;charset=utf-8");
expect(response.status).toBe(407);
});
it("supports headers", () => {
var response = Response.json("hello", {
headers: {
"content-type": "potato",
"x-hello": "world",
},
status: 408,
});
expect(response.headers.get("x-hello")).toBe("world");
expect(response.status).toBe(408);
});
});
describe("Response.redirect", () => {
it("works", () => {
const inputs = [
"http://example.com",
"http://example.com/",
"http://example.com/hello",
"http://example.com/hello/",
"http://example.com/hello/world",
"http://example.com/hello/world/",
];
for (let input of inputs) {
expect(Response.redirect(input).headers.get("Location")).toBe(input);
}
});
it("supports headers", () => {
var response = Response.redirect("https://example.com", {
headers: {
"content-type": "potato",
"x-hello": "world",
Location: "https://wrong.com",
},
status: 408,
});
expect(response.headers.get("x-hello")).toBe("world");
expect(response.headers.get("Location")).toBe("https://example.com");
expect(response.status).toBe(302);
expect(response.type).toBe("default");
expect(response.ok).toBe(false);
});
});
describe("Response.error", () => {
it("works", () => {
expect(Response.error().type).toBe("error");
expect(Response.error().ok).toBe(false);
expect(Response.error().status).toBe(0);
});
});
it("clone", async () => {
gc();
var body = new Response("hello
", {
headers: {
"content-type": "text/html; charset=utf-8",
},
});
gc();
var clone = body.clone();
gc();
body.headers.set("content-type", "text/plain");
gc();
expect(clone.headers.get("content-type")).toBe("text/html; charset=utf-8");
gc();
expect(body.headers.get("content-type")).toBe("text/plain");
gc();
expect(await clone.text()).toBe("hello
");
gc();
});
it("invalid json", async () => {
gc();
var body = new Response("hello
", {
headers: {
"content-type": "text/html; charset=utf-8",
},
});
try {
await body.json();
expect(false).toBe(true);
} catch (exception) {
expect(exception instanceof SyntaxError).toBe(true);
}
});
describe("should consume body correctly", async () => {
it("with text first", async () => {
var response = new Response("hello
");
expect(await response.text()).toBe("hello
");
expect(async () => {
await response.text();
}).toThrow("Body already used");
expect(async () => {
await response.json();
}).toThrow("Body already used");
expect(async () => {
await response.formData();
}).toThrow("Body already used");
expect(async () => {
await response.blob();
}).toThrow("Body already used");
expect(async () => {
await response.arrayBuffer();
}).toThrow("Body already used");
});
it("with json first", async () => {
var response = new Response('{ "hello": "world" }');
expect(await response.json()).toEqual({ "hello": "world" });
expect(async () => {
await response.json();
}).toThrow("Body already used");
expect(async () => {
await response.text();
}).toThrow("Body already used");
expect(async () => {
await response.formData();
}).toThrow("Body already used");
expect(async () => {
await response.blob();
}).toThrow("Body already used");
expect(async () => {
await response.arrayBuffer();
}).toThrow("Body already used");
});
it("with formData first", async () => {
var response = new Response("--boundary--", {
headers: {
"content-type": "multipart/form-data;boundary=boundary",
},
});
expect(await response.formData()).toBeInstanceOf(FormData);
expect(async () => {
await response.formData();
}).toThrow("Body already used");
expect(async () => {
await response.text();
}).toThrow("Body already used");
expect(async () => {
await response.json();
}).toThrow("Body already used");
expect(async () => {
await response.blob();
}).toThrow("Body already used");
expect(async () => {
await response.arrayBuffer();
}).toThrow("Body already used");
});
it("with blob first", async () => {
var response = new Response("hello
");
expect(response.body instanceof ReadableStream).toBe(true);
expect(response.headers instanceof Headers).toBe(true);
expect(response.type).toBe("default");
var blob = await response.blob();
expect(blob).toBeInstanceOf(Blob);
expect(blob.stream()).toBeInstanceOf(ReadableStream);
expect(async () => {
await response.blob();
}).toThrow("Body already used");
expect(async () => {
await response.text();
}).toThrow("Body already used");
expect(async () => {
await response.json();
}).toThrow("Body already used");
expect(async () => {
await response.formData();
}).toThrow("Body already used");
expect(async () => {
await response.arrayBuffer();
}).toThrow("Body already used");
});
it("with arrayBuffer first", async () => {
var response = new Response("hello
");
expect(await response.arrayBuffer()).toBeInstanceOf(ArrayBuffer);
expect(async () => {
await response.arrayBuffer();
}).toThrow("Body already used");
expect(async () => {
await response.text();
}).toThrow("Body already used");
expect(async () => {
await response.json();
}).toThrow("Body already used");
expect(async () => {
await response.formData();
}).toThrow("Body already used");
expect(async () => {
await response.blob();
}).toThrow("Body already used");
});
it("with Bun.file() streams", async () => {
var stream = Bun.file(import.meta.dir + "/fixtures/file.txt").stream();
expect(stream instanceof ReadableStream).toBe(true);
var input = new Response((await new Response(stream).blob()).stream()).arrayBuffer();
var output = Bun.file(import.meta.dir + "/fixtures/file.txt").arrayBuffer();
expect(await input).toEqual(await output);
});
it("with Bun.file() with request/response", async () => {
startServer({
async fetch(request: Request) {
var text = await request.text();
expect(async () => {
await request.arrayBuffer();
}).toThrow();
return (response = new Response((await new Response(text).blob()).stream()));
},
});
var response = await fetch(`http://127.0.0.1:${server.port}`, {
method: "POST",
body: await Bun.file(import.meta.dir + "/fixtures/file.txt").arrayBuffer(),
});
var input = await response.arrayBuffer();
var output = await Bun.file(import.meta.dir + "/fixtures/file.txt").stream();
expect(input).toEqual((await output.getReader().read()).value?.buffer);
});
});
it("should work with bigint", () => {
var r = new Response("hello status", { status: 200n });
expect(r.status).toBe(200);
r = new Response("hello status", { status: 599n });
expect(r.status).toBe(599);
r = new Response("hello status", { status: BigInt(200) });
expect(r.status).toBe(200);
r = new Response("hello status", { status: BigInt(599) });
expect(r.status).toBe(599);
});
testBlobInterface(data => new Response(data), true);
});
describe("Request", () => {
it("clone", async () => {
gc();
var body = new Request("https://hello.com", {
headers: {
"content-type": "text/html; charset=utf-8",
},
body: "hello
",
});
gc();
expect(body.signal).toBeDefined();
gc();
expect(body.headers.get("content-type")).toBe("text/html; charset=utf-8");
gc();
var clone = body.clone();
gc();
expect(clone.signal).toBeDefined();
gc();
body.headers.set("content-type", "text/plain");
gc();
expect(clone.headers.get("content-type")).toBe("text/html; charset=utf-8");
gc();
expect(body.headers.get("content-type")).toBe("text/plain");
gc();
expect(await clone.text()).toBe("hello
");
});
it("signal", async () => {
gc();
const controller = new AbortController();
const req = new Request("https://hello.com", { signal: controller.signal });
expect(req.signal.aborted).toBe(false);
gc();
controller.abort();
gc();
expect(req.signal.aborted).toBe(true);
});
it("cloned signal", async () => {
gc();
const controller = new AbortController();
const req = new Request("https://hello.com", { signal: controller.signal });
expect(req.signal.aborted).toBe(false);
gc();
controller.abort();
gc();
expect(req.signal.aborted).toBe(true);
gc();
const cloned = req.clone();
expect(cloned.signal.aborted).toBe(true);
});
testBlobInterface(data => new Request("https://hello.com", { body: data }), true);
});
describe("Headers", () => {
it("writes", async () => {
var headers = new Headers({
"content-type": "text/html; charset=utf-8",
});
gc();
expect(headers.get("content-type")).toBe("text/html; charset=utf-8");
gc();
headers.delete("content-type");
gc();
expect(headers.get("content-type")).toBe(null);
gc();
headers.append("content-type", "text/plain");
gc();
expect(headers.get("content-type")).toBe("text/plain");
gc();
headers.append("content-type", "text/plain");
gc();
expect(headers.get("content-type")).toBe("text/plain, text/plain");
gc();
headers.set("content-type", "text/html; charset=utf-8");
gc();
expect(headers.get("content-type")).toBe("text/html; charset=utf-8");
headers.delete("content-type");
gc();
expect(headers.get("content-type")).toBe(null);
gc();
});
});
it("body nullable", async () => {
gc();
{
const req = new Request("https://hello.com", { body: null });
expect(req.body).toBeNull();
}
gc();
{
const req = new Request("https://hello.com", { body: undefined });
expect(req.body).toBeNull();
}
gc();
{
const req = new Request("https://hello.com");
expect(req.body).toBeNull();
}
gc();
{
const req = new Request("https://hello.com", { body: "" });
expect(req.body).not.toBeNull();
}
});
it("Request({}) throws", async () => {
// @ts-expect-error
expect(() => new Request({})).toThrow();
});
it("Request({toString() { throw 'wat'; } }) throws", async () => {
expect(
() =>
// @ts-expect-error
new Request({
toString() {
throw "wat";
},
}),
).toThrow("wat");
});
it("should not be able to parse json from empty body", () => {
expect(async () => await new Response().json()).toThrow(SyntaxError);
expect(async () => await new Request("http://example.com/").json()).toThrow(SyntaxError);
});
it("#874", () => {
expect(new Request(new Request("https://example.com"), {}).url).toBe("https://example.com/");
expect(new Request(new Request("https://example.com")).url).toBe("https://example.com/");
expect(new Request({ url: "https://example.com" }).url).toBe("https://example.com/");
});
it("#2794", () => {
expect(typeof globalThis.fetch.bind).toBe("function");
expect(typeof Bun.fetch.bind).toBe("function");
});
it("#3545", () => {
expect(() => fetch("http://example.com?a=b")).not.toThrow();
});
it("invalid header doesnt crash", () => {
expect(() =>
fetch("http://example.com", {
headers: {
["lol!!!!!" + "emoji" + "π"]: "hello",
},
}),
).toThrow();
});
it("new Request(https://example.com, otherRequest) uses url from left instead of right", () => {
const req1 = new Request("http://localhost/abc", {
headers: {
foo: "bar",
},
});
// Want to rewrite the URL with keeping header values
const req2 = new Request("http://localhost/def", req1);
// Should be `http://localhost/def` But actual: http://localhost/abc
expect(req2.url).toBe("http://localhost/def");
expect(req2.headers.get("foo")).toBe("bar");
});
it("fetch() file:// works", async () => {
expect(await (await fetch(import.meta.url)).text()).toEqual(await Bun.file(import.meta.path).text());
expect(await (await fetch(new URL("fetch.test.ts", import.meta.url))).text()).toEqual(
await Bun.file(Bun.fileURLToPath(new URL("fetch.test.ts", import.meta.url))).text(),
);
gc(true);
var fileResponse = await fetch(new URL("file with space in the name.txt", import.meta.url));
gc(true);
var fileResponseText = await fileResponse.text();
gc(true);
var bunFile = Bun.file(Bun.fileURLToPath(new URL("file with space in the name.txt", import.meta.url)));
gc(true);
var bunFileText = await bunFile.text();
gc(true);
expect(fileResponseText).toEqual(bunFileText);
gc(true);
});
it("cloned response headers are independent before accessing", () => {
const response = new Response("hello", {
headers: {
"content-type": "text/html; charset=utf-8",
},
});
const cloned = response.clone();
cloned.headers.set("content-type", "text/plain");
expect(response.headers.get("content-type")).toBe("text/html; charset=utf-8");
});
it("cloned response headers are independent after accessing", () => {
const response = new Response("hello", {
headers: {
"content-type": "text/html; charset=utf-8",
},
});
// create the headers
response.headers;
const cloned = response.clone();
cloned.headers.set("content-type", "text/plain");
expect(response.headers.get("content-type")).toBe("text/html; charset=utf-8");
});
it("should work with http 100 continue", async () => {
let server: net.Server | undefined;
try {
server = net.createServer(socket => {
socket.on("data", data => {
const lines = data.toString().split("\r\n");
for (const line of lines) {
if (line.length == 0) {
socket.write("HTTP/1.1 100 Continue\r\n\r\n");
socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, World!");
break;
}
}
});
});
const { promise: start, resolve } = Promise.withResolvers();
server.listen(8080, resolve);
await start;
const address = server.address() as net.AddressInfo;
const result = await fetch(`http://localhost:${address.port}`).then(r => r.text());
expect(result).toBe("Hello, World!");
} finally {
server?.close();
}
});
it("should work with http 100 continue on the same buffer", async () => {
let server: net.Server | undefined;
try {
server = net.createServer(socket => {
socket.on("data", data => {
const lines = data.toString().split("\r\n");
for (const line of lines) {
if (line.length == 0) {
socket.write(
"HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, World!",
);
break;
}
}
});
});
const { promise: start, resolve } = Promise.withResolvers();
server.listen(8080, resolve);
await start;
const address = server.address() as net.AddressInfo;
const result = await fetch(`http://localhost:${address.port}`).then(r => r.text());
expect(result).toBe("Hello, World!");
} finally {
server?.close();
}
});
describe("should strip headers", () => {
it("status code 303", async () => {
const server = Bun.serve({
port: 0,
async fetch(request: Request) {
if (request.url.endsWith("/redirect")) {
return new Response("hello", {
headers: {
...request.headers,
"Location": "/redirected",
},
status: 303,
});
}
return new Response("hello", {
headers: request.headers,
});
},
});
const { headers, url, redirected } = await fetch(`http://${server.hostname}:${server.port}/redirect`, {
method: "POST",
headers: {
"I-Am-Here": "yes",
"Content-Language": "This should be stripped",
},
});
expect(headers.get("I-Am-Here")).toBe("yes");
expect(headers.get("Content-Language")).toBeNull();
expect(url).toEndWith("/redirected");
expect(redirected).toBe(true);
server.stop(true);
});
it("cross-origin status code 302", async () => {
const server1 = Bun.serve({
port: 0,
async fetch(request: Request) {
if (request.url.endsWith("/redirect")) {
return new Response("hello", {
headers: {
...request.headers,
"Location": `http://${server2.hostname}:${server2.port}/redirected`,
},
status: 302,
});
}
return new Response("hello", {
headers: request.headers,
});
},
});
const server2 = Bun.serve({
port: 0,
async fetch(request: Request, server) {
if (request.url.endsWith("/redirect")) {
return new Response("hello", {
headers: {
...request.headers,
"Location": `http://${server.hostname}:${server.port}/redirected`,
},
status: 302,
});
}
return new Response("hello", {
headers: request.headers,
});
},
});
const { headers, url, redirected } = await fetch(`http://${server1.hostname}:${server1.port}/redirect`, {
method: "GET",
headers: {
"Authorization": "yes",
},
});
expect(headers.get("Authorization")).toBeNull();
expect(url).toEndWith("/redirected");
expect(redirected).toBe(true);
server1.stop(true);
server2.stop(true);
});
});
it("same-origin status code 302 should not strip headers", async () => {
const server = Bun.serve({
port: 0,
async fetch(request: Request, server) {
if (request.url.endsWith("/redirect")) {
return new Response("hello", {
headers: {
...request.headers,
"Location": `http://${server.hostname}:${server.port}/redirected`,
},
status: 302,
});
}
return new Response("hello", {
headers: request.headers,
});
},
});
const { headers, url, redirected } = await fetch(`http://${server.hostname}:${server.port}/redirect`, {
method: "GET",
headers: {
"Authorization": "yes",
},
});
expect(headers.get("Authorization")).toEqual("yes");
expect(url).toEndWith("/redirected");
expect(redirected).toBe(true);
server.stop(true);
});
describe("should handle relative location in the redirect, issue#5635", () => {
var server: Server;
beforeAll(async () => {
server = Bun.serve({
port: 0,
async fetch(request: Request) {
return new Response("Not Found", {
status: 404,
});
},
});
});
afterAll(() => {
server.stop(true);
});
it.each([
["/a/b", "/c", "/c"],
["/a/b", "c", "/a/c"],
["/a/b", "/c/d", "/c/d"],
["/a/b", "c/d", "/a/c/d"],
["/a/b", "../c", "/c"],
["/a/b", "../c/d", "/c/d"],
["/a/b", "../../../c", "/c"],
// slash
["/a/b/", "/c", "/c"],
["/a/b/", "c", "/a/b/c"],
["/a/b/", "/c/d", "/c/d"],
["/a/b/", "c/d", "/a/b/c/d"],
["/a/b/", "../c", "/a/c"],
["/a/b/", "../c/d", "/a/c/d"],
["/a/b/", "../../../c", "/c"],
])("('%s', '%s')", async (pathname, location, expected) => {
server.reload({
async fetch(request: Request) {
const url = new URL(request.url);
if (url.pathname == pathname) {
return new Response("redirecting", {
headers: {
"Location": location,
},
status: 302,
});
} else if (url.pathname == expected) {
return new Response("Fine.");
}
return new Response("Not Found", {
status: 404,
});
},
});
const resp = await fetch(`http://${server.hostname}:${server.port}${pathname}`);
expect(resp.redirected).toBe(true);
expect(new URL(resp.url).pathname).toStrictEqual(expected);
expect(resp.status).toBe(200);
expect(await resp.text()).toBe("Fine.");
});
});
it("should throw RedirectURLTooLong when location is too long", async () => {
const server = Bun.serve({
port: 0,
async fetch(request: Request) {
gc();
const url = new URL(request.url);
if (url.pathname == "/redirect") {
return new Response("redirecting", {
headers: {
"Location": "B".repeat(8193),
},
status: 302,
});
}
return new Response("Not Found", {
status: 404,
});
},
});
let err = undefined;
try {
gc();
const resp = await fetch(`http://${server.hostname}:${server.port}/redirect`);
} catch (error) {
gc();
err = error;
}
expect(err).not.toBeUndefined();
expect(err).toBeInstanceOf(Error);
expect(err.code).toStrictEqual("RedirectURLTooLong");
server.stop(true);
});