Files
bun.sh/test/js/web/fetch/client-fetch.test.ts
2024-09-03 21:32:52 -07:00

500 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 = "";
for await (const chunk of req) {
body += chunk;
}
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();
}
});