mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com> Co-authored-by: Pham Minh Triet <92496972+Nanome203@users.noreply.github.com> Co-authored-by: snwy <snwy@snwy.me> Co-authored-by: Ciro Spaciari <ciro.spaciari@gmail.com> Co-authored-by: cirospaciari <cirospaciari@users.noreply.github.com> Co-authored-by: Ben Grant <ben@bun.sh>
503 lines
15 KiB
TypeScript
503 lines
15 KiB
TypeScript
/* globals AbortController */
|
|
|
|
import { expect, test } from "bun:test";
|
|
import { createHash, randomFillSync } from "node:crypto";
|
|
import { once } from "node:events";
|
|
import { createServer } from "node:http";
|
|
import { promisify } from "node:util";
|
|
import { gzipSync } from "node:zlib";
|
|
|
|
test("function signature", () => {
|
|
expect(fetch.name).toBe("fetch");
|
|
expect(fetch.length).toBe(1);
|
|
});
|
|
|
|
test("args validation", async () => {
|
|
expect(fetch()).rejects.toThrow(TypeError);
|
|
expect(fetch("ftp://unsupported")).rejects.toThrow(TypeError);
|
|
});
|
|
|
|
test("request json", async () => {
|
|
const obj = { asd: true };
|
|
await using server = createServer((req, res) => {
|
|
res.end(JSON.stringify(obj));
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
const body = await fetch(`http://localhost:${server.address().port}`);
|
|
expect(obj).toEqual(await body.json());
|
|
});
|
|
|
|
test("request text", async () => {
|
|
const obj = { asd: true };
|
|
await using server = createServer((req, res) => {
|
|
res.end(JSON.stringify(obj));
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
const body = await fetch(`http://localhost:${server.address().port}`);
|
|
expect(JSON.stringify(obj)).toEqual(await body.text());
|
|
});
|
|
|
|
test("request arrayBuffer", async () => {
|
|
const obj = { asd: true };
|
|
await using server = createServer((req, res) => {
|
|
res.end(JSON.stringify(obj));
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
const body = await fetch(`http://localhost:${server.address().port}`);
|
|
expect(Buffer.from(JSON.stringify(obj))).toEqual(Buffer.from(await body.arrayBuffer()));
|
|
});
|
|
|
|
test("should set type of blob object to the value of the `Content-Type` header from response", async () => {
|
|
const obj = { asd: true };
|
|
await using server = createServer((req, res) => {
|
|
res.setHeader("Content-Type", "application/json");
|
|
res.end(JSON.stringify(obj));
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
const response = await fetch(`http://localhost:${server.address().port}`);
|
|
expect("application/json;charset=utf-8").toBe((await response.blob()).type);
|
|
});
|
|
|
|
test("pre aborted with readable request body", async () => {
|
|
const server = createServer((req, res) => {}).listen(0);
|
|
try {
|
|
await once(server, "listening");
|
|
|
|
const ac = new AbortController();
|
|
ac.abort();
|
|
expect(
|
|
fetch(`http://localhost:${server.address().port}`, {
|
|
signal: ac.signal,
|
|
method: "POST",
|
|
body: new ReadableStream({
|
|
async cancel(reason) {
|
|
expect(reason.name).toBe("AbortError");
|
|
},
|
|
}),
|
|
duplex: "half",
|
|
}),
|
|
).rejects.toThrow();
|
|
} finally {
|
|
server.closeAllConnections();
|
|
}
|
|
});
|
|
|
|
test("pre aborted with closed readable request body", async () => {
|
|
await using server = createServer((req, res) => {}).listen(0);
|
|
await once(server, "listening");
|
|
const ac = new AbortController();
|
|
ac.abort();
|
|
const body = new ReadableStream({
|
|
async start(c) {
|
|
expect(true).toBe(true);
|
|
c.close();
|
|
},
|
|
async cancel(reason) {
|
|
expect.unreachable();
|
|
},
|
|
});
|
|
|
|
expect(
|
|
fetch(`http://localhost:${server.address().port}`, {
|
|
signal: ac.signal,
|
|
method: "POST",
|
|
body,
|
|
duplex: "half",
|
|
}),
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
test("unsupported formData 1", async () => {
|
|
await using server = createServer((req, res) => {
|
|
res.setHeader("content-type", "asdasdsad");
|
|
res.end();
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
expect(fetch(`http://localhost:${server.address().port}`).then(res => res.formData())).rejects.toThrow(TypeError);
|
|
});
|
|
|
|
test("multipart formdata not base64", async () => {
|
|
// Construct example form data, with text and blob fields
|
|
const formData = new FormData();
|
|
formData.append("field1", "value1");
|
|
const blob = new Blob(["example\ntext file"], { type: "text/plain" });
|
|
formData.append("field2", blob, "file.txt");
|
|
|
|
const tempRes = new Response(formData);
|
|
const boundary = tempRes.headers.get("content-type").split("boundary=")[1];
|
|
const formRaw = await tempRes.text();
|
|
|
|
await using server = createServer((req, res) => {
|
|
res.setHeader("content-type", "multipart/form-data; boundary=" + boundary);
|
|
res.write(formRaw);
|
|
res.end();
|
|
});
|
|
const listen = promisify(server.listen.bind(server));
|
|
await listen(0);
|
|
const res = await fetch(`http://localhost:${server.address().port}`);
|
|
const form = await res.formData();
|
|
expect(form.get("field1")).toBe("value1");
|
|
|
|
const text = await form.get("field2").text();
|
|
expect(text).toBe("example\ntext file");
|
|
});
|
|
|
|
test.todo("multipart formdata base64", async () => {
|
|
// Example form data with base64 encoding
|
|
const data = randomFillSync(Buffer.alloc(256));
|
|
const formRaw =
|
|
"------formdata-bun-0.5786922755719377\r\n" +
|
|
'Content-Disposition: form-data; name="file"; filename="test.txt"\r\n' +
|
|
"Content-Type: application/octet-stream\r\n" +
|
|
"Content-Transfer-Encoding: base64\r\n" +
|
|
"\r\n" +
|
|
data.toString("base64") +
|
|
"\r\n" +
|
|
"------formdata-bun-0.5786922755719377--";
|
|
|
|
await using server = createServer(async (req, res) => {
|
|
res.setHeader("content-type", "multipart/form-data; boundary=----formdata-bun-0.5786922755719377");
|
|
|
|
for (let offset = 0; offset < formRaw.length; ) {
|
|
res.write(formRaw.slice(offset, (offset += 2)));
|
|
await new Promise(resolve => setTimeout(resolve));
|
|
}
|
|
res.end();
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
const digest = await fetch(`http://localhost:${server.address().port}`)
|
|
.then(res => res.formData())
|
|
.then(form => form.get("file").arrayBuffer())
|
|
.then(buffer => createHash("sha256").update(Buffer.from(buffer)).digest("base64"));
|
|
expect(createHash("sha256").update(data).digest("base64")).toBe(digest);
|
|
});
|
|
|
|
test("multipart fromdata non-ascii filed names", async () => {
|
|
const request = new Request("http://localhost", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "multipart/form-data; boundary=----formdata-undici-0.6204674738279623",
|
|
},
|
|
body:
|
|
"------formdata-undici-0.6204674738279623\r\n" +
|
|
'Content-Disposition: form-data; name="fiŝo"\r\n' +
|
|
"\r\n" +
|
|
"value1\r\n" +
|
|
"------formdata-undici-0.6204674738279623--",
|
|
});
|
|
|
|
const form = await request.formData();
|
|
expect(form.get("fiŝo")).toBe("value1");
|
|
});
|
|
|
|
test("busboy emit error", async () => {
|
|
const formData = new FormData();
|
|
formData.append("field1", "value1");
|
|
|
|
const tempRes = new Response(formData);
|
|
const formRaw = await tempRes.text();
|
|
|
|
await using server = createServer((req, res) => {
|
|
res.setHeader("content-type", "multipart/form-data; boundary=wrongboundary");
|
|
res.write(formRaw);
|
|
res.end();
|
|
});
|
|
|
|
const listen = promisify(server.listen.bind(server));
|
|
await listen(0);
|
|
|
|
const res = await fetch(`http://localhost:${server.address().port}`);
|
|
expect(res.formData()).rejects.toThrow("FormData parse error missing final boundary");
|
|
});
|
|
|
|
// https://github.com/nodejs/undici/issues/2244
|
|
test("parsing formData preserve full path on files", async () => {
|
|
const formData = new FormData();
|
|
formData.append("field1", new File(["foo"], "a/b/c/foo.txt"));
|
|
|
|
const tempRes = new Response(formData);
|
|
const form = await tempRes.formData();
|
|
|
|
expect(form.get("field1").name).toBe("a/b/c/foo.txt");
|
|
});
|
|
|
|
test("urlencoded formData", async () => {
|
|
await using server = createServer((req, res) => {
|
|
res.setHeader("content-type", "application/x-www-form-urlencoded");
|
|
res.end("field1=value1&field2=value2");
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
const formData = await fetch(`http://localhost:${server.address().port}`).then(res => res.formData());
|
|
expect(formData.get("field1")).toBe("value1");
|
|
expect(formData.get("field2")).toBe("value2");
|
|
});
|
|
|
|
test("text with BOM", async () => {
|
|
await using server = createServer((req, res) => {
|
|
res.setHeader("content-type", "application/x-www-form-urlencoded");
|
|
res.end("\uFEFFtest=\uFEFF");
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
const text = await fetch(`http://localhost:${server.address().port}`).then(res => res.text());
|
|
expect(text).toBe("test=\uFEFF");
|
|
});
|
|
|
|
test.todo("formData with BOM", async () => {
|
|
await using server = createServer((req, res) => {
|
|
res.setHeader("content-type", "application/x-www-form-urlencoded");
|
|
res.end("\uFEFFtest=\uFEFF");
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
const formData = await fetch(`http://localhost:${server.address().port}`).then(res => res.formData());
|
|
expect(formData.get("\uFEFFtest")).toBe("\uFEFF");
|
|
});
|
|
|
|
test("locked blob body", async () => {
|
|
await using server = createServer((req, res) => {
|
|
res.end();
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
const res = await fetch(`http://localhost:${server.address().port}`);
|
|
const reader = res.body.getReader();
|
|
expect(res.blob()).rejects.toThrow("ReadableStream is locked");
|
|
reader.cancel();
|
|
});
|
|
|
|
test("disturbed blob body", async () => {
|
|
await using server = createServer((req, res) => {
|
|
res.end();
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
const res = await fetch(`http://localhost:${server.address().port}`);
|
|
await res.blob();
|
|
expect(res.blob()).rejects.toThrow("Body already used");
|
|
});
|
|
|
|
test("redirect with body", async () => {
|
|
let count = 0;
|
|
await using server = createServer(async (req, res) => {
|
|
let body = "";
|
|
req.on("data", chunk => {
|
|
body += chunk;
|
|
});
|
|
|
|
req.on("end", () => {
|
|
expect(body).toBe("asd");
|
|
if (count++ === 0) {
|
|
res.setHeader("location", "asd");
|
|
res.statusCode = 302;
|
|
res.end();
|
|
} else {
|
|
res.end(String(count));
|
|
}
|
|
});
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
const res = await fetch(`http://localhost:${server.address().port}`, {
|
|
method: "PUT",
|
|
body: "asd",
|
|
});
|
|
expect(await res.text()).toBe("2");
|
|
});
|
|
|
|
test("redirect with stream", async () => {
|
|
const location = "/asd";
|
|
const body = "hello!";
|
|
await using server = createServer(async (req, res) => {
|
|
res.writeHead(302, { location });
|
|
let count = 0;
|
|
const l = setInterval(() => {
|
|
res.write(body[count++]);
|
|
if (count === body.length) {
|
|
res.end();
|
|
clearInterval(l);
|
|
}
|
|
}, 50);
|
|
}).listen(0);
|
|
|
|
await once(server, "listening");
|
|
|
|
const res = await fetch(`http://localhost:${server.address().port}`, {
|
|
redirect: "manual",
|
|
});
|
|
expect(res.status).toBe(302);
|
|
expect(res.headers.get("location")).toBe(location);
|
|
expect(await res.text()).toBe(body);
|
|
});
|
|
|
|
test("fail to extract locked body", () => {
|
|
const stream = new ReadableStream({});
|
|
const reader = stream.getReader();
|
|
try {
|
|
// eslint-disable-next-line
|
|
new Response(stream);
|
|
} catch (err) {
|
|
expect((err as Error).name).toBe("TypeError");
|
|
}
|
|
reader.cancel();
|
|
});
|
|
|
|
test("fail to extract locked body", () => {
|
|
const stream = new ReadableStream({});
|
|
const reader = stream.getReader();
|
|
try {
|
|
// eslint-disable-next-line
|
|
new Request("http://asd", {
|
|
method: "PUT",
|
|
body: stream,
|
|
keepalive: true,
|
|
});
|
|
} catch (err) {
|
|
expect((err as Error).message).toBe("keepalive");
|
|
}
|
|
reader.cancel();
|
|
});
|
|
|
|
test("post FormData with Blob", async () => {
|
|
const body = new FormData();
|
|
body.append("field1", new Blob(["asd1"]));
|
|
|
|
await using server = createServer((req, res) => {
|
|
req.pipe(res);
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
const res = await fetch(`http://localhost:${server.address().port}`, {
|
|
method: "PUT",
|
|
body,
|
|
});
|
|
expect(/asd1/.test(await res.text())).toBeTruthy();
|
|
});
|
|
|
|
test("post FormData with File", async () => {
|
|
const body = new FormData();
|
|
body.append("field1", new File(["asd1"], "filename123"));
|
|
|
|
await using server = createServer((req, res) => {
|
|
req.pipe(res);
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
const res = await fetch(`http://localhost:${server.address().port}`, {
|
|
method: "PUT",
|
|
body,
|
|
});
|
|
const result = await res.text();
|
|
expect(/asd1/.test(result)).toBeTrue();
|
|
expect(/filename123/.test(result)).toBeTrue();
|
|
});
|
|
|
|
test("invalid url", async () => {
|
|
try {
|
|
await fetch("http://invalid");
|
|
} catch (e) {
|
|
expect(e.message).toBe("Unable to connect. Is the computer able to access the url?");
|
|
}
|
|
});
|
|
|
|
test("do not decode redirect body", async () => {
|
|
const obj = { asd: true };
|
|
await using server = createServer((req, res) => {
|
|
if (req.url === "/resource") {
|
|
res.statusCode = 301;
|
|
res.setHeader("location", "/resource/");
|
|
// Some dumb http servers set the content-encoding gzip
|
|
// even if there is no response
|
|
res.setHeader("content-encoding", "gzip");
|
|
res.end();
|
|
return;
|
|
}
|
|
res.setHeader("content-encoding", "gzip");
|
|
res.end(gzipSync(JSON.stringify(obj)));
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
const body = await fetch(`http://localhost:${server.address().port}/resource`);
|
|
expect(JSON.stringify(obj)).toBe(await body.text());
|
|
});
|
|
|
|
test("decode non-redirect body with location header", async () => {
|
|
const obj = { asd: true };
|
|
await using server = createServer((req, res) => {
|
|
res.statusCode = 201;
|
|
res.setHeader("location", "/resource/");
|
|
res.setHeader("content-encoding", "gzip");
|
|
res.end(gzipSync(JSON.stringify(obj)));
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
const body = await fetch(`http://localhost:${server.address().port}/resource`);
|
|
expect(JSON.stringify(obj)).toBe(await body.text());
|
|
});
|
|
|
|
test("error on redirect", async () => {
|
|
await using server = createServer((req, res) => {
|
|
res.statusCode = 302;
|
|
res.end();
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
expect(
|
|
fetch(`http://localhost:${server.address().port}`, {
|
|
redirect: "error",
|
|
}),
|
|
).rejects.toThrow(/UnexpectedRedirect/);
|
|
});
|
|
|
|
test("Receiving non-Latin1 headers", async () => {
|
|
const ContentDisposition = [
|
|
"inline; filename=rock&roll.png",
|
|
"inline; filename=\"rock'n'roll.png\"",
|
|
"inline; filename=\"image â\x80\x94 copy (1).png\"; filename*=UTF-8''image%20%E2%80%94%20copy%20(1).png",
|
|
"inline; filename=\"_å\x9C\x96ç\x89\x87_ð\x9F\x96¼_image_.png\"; filename*=UTF-8''_%E5%9C%96%E7%89%87_%F0%9F%96%BC_image_.png",
|
|
"inline; filename=\"100 % loading&perf.png\"; filename*=UTF-8''100%20%25%20loading%26perf.png",
|
|
];
|
|
|
|
await using server = createServer((req, res) => {
|
|
for (let i = 0; i < ContentDisposition.length; i++) {
|
|
res.setHeader(`Content-Disposition-${i + 1}`, ContentDisposition[i]);
|
|
}
|
|
|
|
res.end();
|
|
}).listen(0);
|
|
await once(server, "listening");
|
|
|
|
const url = `http://localhost:${server.address().port}`;
|
|
const response = await fetch(url, { method: "HEAD" });
|
|
const cdHeaders = [...response.headers].filter(([k]) => k.startsWith("content-disposition")).map(([, v]) => v);
|
|
const lengths = cdHeaders.map(h => h.length);
|
|
|
|
expect(cdHeaders).toEqual(ContentDisposition);
|
|
expect(lengths).toEqual([30, 34, 94, 104, 90]);
|
|
});
|
|
|
|
// https://github.com/nodejs/undici/issues/1527
|
|
test("fetching with Request object - issue #1527", async () => {
|
|
const server = createServer((req, res) => {
|
|
res.end();
|
|
}).listen(0);
|
|
try {
|
|
await once(server, "listening");
|
|
|
|
const body = JSON.stringify({ foo: "bar" });
|
|
const request = new Request(`http://localhost:${server.address().port}`, {
|
|
method: "POST",
|
|
body,
|
|
});
|
|
|
|
expect(fetch(request)).resolves.pass();
|
|
} finally {
|
|
server.closeAllConnections();
|
|
}
|
|
});
|