summaryrefslogtreecommitdiff
path: root/tests/unit/fetch_test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'tests/unit/fetch_test.ts')
-rw-r--r--tests/unit/fetch_test.ts2071
1 files changed, 2071 insertions, 0 deletions
diff --git a/tests/unit/fetch_test.ts b/tests/unit/fetch_test.ts
new file mode 100644
index 000000000..80837a456
--- /dev/null
+++ b/tests/unit/fetch_test.ts
@@ -0,0 +1,2071 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertThrows,
+ delay,
+ fail,
+ unimplemented,
+} from "./test_util.ts";
+import { Buffer } from "@test_util/std/io/buffer.ts";
+
+const listenPort = 4506;
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchRequiresOneArgument() {
+ await assertRejects(
+ fetch as unknown as () => Promise<void>,
+ TypeError,
+ );
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function fetchProtocolError() {
+ await assertRejects(
+ async () => {
+ await fetch("ftp://localhost:21/a/file");
+ },
+ TypeError,
+ "not supported",
+ );
+});
+
+function findClosedPortInRange(
+ minPort: number,
+ maxPort: number,
+): number | never {
+ let port = minPort;
+
+ // If we hit the return statement of this loop
+ // that means that we did not throw an
+ // AddrInUse error when we executed Deno.listen.
+ while (port < maxPort) {
+ try {
+ const listener = Deno.listen({ port });
+ listener.close();
+ return port;
+ } catch (_e) {
+ port++;
+ }
+ }
+
+ unimplemented(
+ `No available ports between ${minPort} and ${maxPort} to test fetch`,
+ );
+}
+
+Deno.test(
+ // TODO(bartlomieju): reenable this test
+ // https://github.com/denoland/deno/issues/18350
+ { ignore: Deno.build.os === "windows", permissions: { net: true } },
+ async function fetchConnectionError() {
+ const port = findClosedPortInRange(4000, 9999);
+ await assertRejects(
+ async () => {
+ await fetch(`http://localhost:${port}`);
+ },
+ TypeError,
+ "error trying to connect",
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchDnsError() {
+ await assertRejects(
+ async () => {
+ await fetch("http://nil/");
+ },
+ TypeError,
+ "error trying to connect",
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInvalidUriError() {
+ await assertRejects(
+ async () => {
+ await fetch("http://<invalid>/");
+ },
+ TypeError,
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchMalformedUriError() {
+ await assertRejects(
+ async () => {
+ const url = new URL("http://{{google/");
+ await fetch(url);
+ },
+ TypeError,
+ );
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function fetchJsonSuccess() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+ const json = await response.json();
+ assertEquals(json.name, "deno");
+});
+
+Deno.test({ permissions: { net: false } }, async function fetchPerm() {
+ await assertRejects(async () => {
+ await fetch("http://localhost:4545/assets/fixture.json");
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test({ permissions: { net: true } }, async function fetchUrl() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+ assertEquals(response.url, "http://localhost:4545/assets/fixture.json");
+ const _json = await response.json();
+});
+
+Deno.test({ permissions: { net: true } }, async function fetchURL() {
+ const response = await fetch(
+ new URL("http://localhost:4545/assets/fixture.json"),
+ );
+ assertEquals(response.url, "http://localhost:4545/assets/fixture.json");
+ const _json = await response.json();
+});
+
+Deno.test({ permissions: { net: true } }, async function fetchHeaders() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+ const headers = response.headers;
+ assertEquals(headers.get("Content-Type"), "application/json");
+ const _json = await response.json();
+});
+
+Deno.test({ permissions: { net: true } }, async function fetchBlob() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+ const headers = response.headers;
+ const blob = await response.blob();
+ assertEquals(blob.type, headers.get("Content-Type"));
+ assertEquals(blob.size, Number(headers.get("Content-Length")));
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchBodyUsedReader() {
+ const response = await fetch(
+ "http://localhost:4545/assets/fixture.json",
+ );
+ assert(response.body !== null);
+
+ const reader = response.body.getReader();
+ // Getting a reader should lock the stream but does not consume the body
+ // so bodyUsed should not be true
+ assertEquals(response.bodyUsed, false);
+ reader.releaseLock();
+ await response.json();
+ assertEquals(response.bodyUsed, true);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchBodyUsedCancelStream() {
+ const response = await fetch(
+ "http://localhost:4545/assets/fixture.json",
+ );
+ assert(response.body !== null);
+
+ assertEquals(response.bodyUsed, false);
+ const promise = response.body.cancel();
+ assertEquals(response.bodyUsed, true);
+ await promise;
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function fetchAsyncIterator() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+ const headers = response.headers;
+
+ assert(response.body !== null);
+ let total = 0;
+ for await (const chunk of response.body) {
+ assert(chunk instanceof Uint8Array);
+ total += chunk.length;
+ }
+
+ assertEquals(total, Number(headers.get("Content-Length")));
+});
+
+Deno.test({ permissions: { net: true } }, async function fetchBodyReader() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+ const headers = response.headers;
+ assert(response.body !== null);
+ const reader = response.body.getReader();
+ let total = 0;
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ assert(value);
+ assert(value instanceof Uint8Array);
+ total += value.length;
+ }
+
+ assertEquals(total, Number(headers.get("Content-Length")));
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchBodyReaderBigBody() {
+ const data = "a".repeat(10 << 10); // 10mb
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: data,
+ });
+ assert(response.body !== null);
+ const reader = await response.body.getReader();
+ let total = 0;
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ assert(value);
+ total += value.length;
+ }
+
+ assertEquals(total, data.length);
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function responseClone() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+ const response1 = response.clone();
+ assert(response !== response1);
+ assertEquals(response.status, response1.status);
+ assertEquals(response.statusText, response1.statusText);
+ const u8a = new Uint8Array(await response.arrayBuffer());
+ const u8a1 = new Uint8Array(await response1.arrayBuffer());
+ for (let i = 0; i < u8a.byteLength; i++) {
+ assertEquals(u8a[i], u8a1[i]);
+ }
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchMultipartFormDataSuccess() {
+ const response = await fetch(
+ "http://localhost:4545/multipart_form_data.txt",
+ );
+ const formData = await response.formData();
+ assert(formData.has("field_1"));
+ assertEquals(formData.get("field_1")!.toString(), "value_1 \r\n");
+ assert(formData.has("field_2"));
+ const file = formData.get("field_2") as File;
+ assertEquals(file.name, "file.js");
+
+ assertEquals(await file.text(), `console.log("Hi")`);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchMultipartFormBadContentType() {
+ const response = await fetch(
+ "http://localhost:4545/multipart_form_bad_content_type",
+ );
+ assert(response.body !== null);
+
+ await assertRejects(
+ async () => {
+ await response.formData();
+ },
+ TypeError,
+ "Body can not be decoded as form data",
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchURLEncodedFormDataSuccess() {
+ const response = await fetch(
+ "http://localhost:4545/subdir/form_urlencoded.txt",
+ );
+ const formData = await response.formData();
+ assert(formData.has("field_1"));
+ assertEquals(formData.get("field_1")!.toString(), "Hi");
+ assert(formData.has("field_2"));
+ assertEquals(formData.get("field_2")!.toString(), "<Deno>");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitFormDataBinaryFileBody() {
+ // Some random bytes
+ // deno-fmt-ignore
+ const binaryFile = new Uint8Array([108,2,0,0,145,22,162,61,157,227,166,77,138,75,180,56,119,188,177,183]);
+ const response = await fetch("http://localhost:4545/echo_multipart_file", {
+ method: "POST",
+ body: binaryFile,
+ });
+ const resultForm = await response.formData();
+ const resultFile = resultForm.get("file") as File;
+
+ assertEquals(resultFile.type, "application/octet-stream");
+ assertEquals(resultFile.name, "file.bin");
+ assertEquals(new Uint8Array(await resultFile.arrayBuffer()), binaryFile);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitFormDataMultipleFilesBody() {
+ const files = [
+ {
+ // deno-fmt-ignore
+ content: new Uint8Array([137,80,78,71,13,10,26,10, 137, 1, 25]),
+ type: "image/png",
+ name: "image",
+ fileName: "some-image.png",
+ },
+ {
+ // deno-fmt-ignore
+ content: new Uint8Array([108,2,0,0,145,22,162,61,157,227,166,77,138,75,180,56,119,188,177,183]),
+ name: "file",
+ fileName: "file.bin",
+ expectedType: "application/octet-stream",
+ },
+ {
+ content: new TextEncoder().encode("deno land"),
+ type: "text/plain",
+ name: "text",
+ fileName: "deno.txt",
+ },
+ ];
+ const form = new FormData();
+ form.append("field", "value");
+ for (const file of files) {
+ form.append(
+ file.name,
+ new Blob([file.content], { type: file.type }),
+ file.fileName,
+ );
+ }
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: form,
+ });
+ const resultForm = await response.formData();
+ assertEquals(form.get("field"), resultForm.get("field"));
+ for (const file of files) {
+ const inputFile = form.get(file.name) as File;
+ const resultFile = resultForm.get(file.name) as File;
+ assertEquals(inputFile.size, resultFile.size);
+ assertEquals(inputFile.name, resultFile.name);
+ assertEquals(file.expectedType || file.type, resultFile.type);
+ assertEquals(
+ new Uint8Array(await resultFile.arrayBuffer()),
+ file.content,
+ );
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchWithRedirection() {
+ const response = await fetch("http://localhost:4546/assets/hello.txt");
+ assertEquals(response.status, 200);
+ assertEquals(response.statusText, "OK");
+ assertEquals(response.url, "http://localhost:4545/assets/hello.txt");
+ const body = await response.text();
+ assert(body.includes("Hello world!"));
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchWithRelativeRedirection() {
+ const response = await fetch(
+ "http://localhost:4545/run/001_hello.js",
+ );
+ assertEquals(response.status, 200);
+ assertEquals(response.statusText, "OK");
+ const body = await response.text();
+ assert(body.includes("Hello"));
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchWithRelativeRedirectionUrl() {
+ const cases = [
+ ["end", "http://localhost:4550/a/b/end"],
+ ["/end", "http://localhost:4550/end"],
+ ];
+ for (const [loc, redUrl] of cases) {
+ const response = await fetch("http://localhost:4550/a/b/c", {
+ headers: new Headers([["x-location", loc]]),
+ });
+ assertEquals(response.url, redUrl);
+ assertEquals(response.redirected, true);
+ assertEquals(response.status, 404);
+ assertEquals(await response.text(), "");
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchWithInfRedirection() {
+ await assertRejects(
+ () => fetch("http://localhost:4549"),
+ TypeError,
+ "redirect",
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitStringBody() {
+ const data = "Hello World";
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: data,
+ });
+ const text = await response.text();
+ assertEquals(text, data);
+ assert(response.headers.get("content-type")!.startsWith("text/plain"));
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchRequestInitStringBody() {
+ const data = "Hello World";
+ const req = new Request("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: data,
+ });
+ const response = await fetch(req);
+ const text = await response.text();
+ assertEquals(text, data);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchSeparateInit() {
+ // related to: https://github.com/denoland/deno/issues/10396
+ const req = new Request("http://localhost:4545/run/001_hello.js");
+ const init = {
+ method: "GET",
+ };
+ req.headers.set("foo", "bar");
+ const res = await fetch(req, init);
+ assertEquals(res.status, 200);
+ await res.text();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitTypedArrayBody() {
+ const data = "Hello World";
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: new TextEncoder().encode(data),
+ });
+ const text = await response.text();
+ assertEquals(text, data);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitArrayBufferBody() {
+ const data = "Hello World";
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: new TextEncoder().encode(data).buffer,
+ });
+ const text = await response.text();
+ assertEquals(text, data);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitURLSearchParamsBody() {
+ const data = "param1=value1&param2=value2";
+ const params = new URLSearchParams(data);
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: params,
+ });
+ const text = await response.text();
+ assertEquals(text, data);
+ assert(
+ response.headers
+ .get("content-type")!
+ .startsWith("application/x-www-form-urlencoded"),
+ );
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function fetchInitBlobBody() {
+ const data = "const a = 1 🦕";
+ const blob = new Blob([data], {
+ type: "text/javascript",
+ });
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: blob,
+ });
+ const text = await response.text();
+ assertEquals(text, data);
+ assert(response.headers.get("content-type")!.startsWith("text/javascript"));
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitFormDataBody() {
+ const form = new FormData();
+ form.append("field", "value");
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: form,
+ });
+ const resultForm = await response.formData();
+ assertEquals(form.get("field"), resultForm.get("field"));
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitFormDataBlobFilenameBody() {
+ const form = new FormData();
+ form.append("field", "value");
+ form.append(
+ "file",
+ new Blob([new TextEncoder().encode("deno")]),
+ "file name",
+ );
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: form,
+ });
+ const resultForm = await response.formData();
+ assertEquals(form.get("field"), resultForm.get("field"));
+ const file = resultForm.get("file");
+ assert(file instanceof File);
+ assertEquals(file.name, "file name");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitFormDataFileFilenameBody() {
+ const form = new FormData();
+ form.append("field", "value");
+ form.append(
+ "file",
+ new File([new Blob([new TextEncoder().encode("deno")])], "file name"),
+ );
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: form,
+ });
+ const resultForm = await response.formData();
+ assertEquals(form.get("field"), resultForm.get("field"));
+ const file = resultForm.get("file");
+ assert(file instanceof File);
+ assertEquals(file.name, "file name");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchInitFormDataTextFileBody() {
+ const fileContent = "deno land";
+ const form = new FormData();
+ form.append("field", "value");
+ form.append(
+ "file",
+ new Blob([new TextEncoder().encode(fileContent)], {
+ type: "text/plain",
+ }),
+ "deno.txt",
+ );
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: form,
+ });
+ const resultForm = await response.formData();
+ assertEquals(form.get("field"), resultForm.get("field"));
+
+ const file = form.get("file") as File;
+ const resultFile = resultForm.get("file") as File;
+
+ assertEquals(file.size, resultFile.size);
+ assertEquals(file.name, resultFile.name);
+ assertEquals(file.type, resultFile.type);
+ assertEquals(await file.text(), await resultFile.text());
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function fetchUserAgent() {
+ const data = "Hello World";
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: new TextEncoder().encode(data),
+ });
+ assertEquals(response.headers.get("user-agent"), `Deno/${Deno.version.deno}`);
+ await response.text();
+});
+
+function bufferServer(addr: string): Promise<Buffer> {
+ const [hostname, port] = addr.split(":");
+ const listener = Deno.listen({
+ hostname,
+ port: Number(port),
+ }) as Deno.Listener;
+ return listener.accept().then(async (conn: Deno.Conn) => {
+ const buf = new Buffer();
+ const p1 = buf.readFrom(conn);
+ const p2 = conn.write(
+ new TextEncoder().encode(
+ "HTTP/1.0 404 Not Found\r\nContent-Length: 2\r\n\r\nNF",
+ ),
+ );
+ // Wait for both an EOF on the read side of the socket and for the write to
+ // complete before closing it. Due to keep-alive, the EOF won't be sent
+ // until the Connection close (HTTP/1.0) response, so readFrom() can't
+ // proceed write. Conversely, if readFrom() is async, waiting for the
+ // write() to complete is not a guarantee that we've read the incoming
+ // request.
+ await Promise.all([p1, p2]);
+ conn.close();
+ listener.close();
+ return buf;
+ });
+}
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchRequest() {
+ const addr = `127.0.0.1:${listenPort}`;
+ const bufPromise = bufferServer(addr);
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [
+ ["Hello", "World"],
+ ["Foo", "Bar"],
+ ],
+ });
+ await response.arrayBuffer();
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode((await bufPromise).bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "content-length: 0\r\n",
+ "hello: World\r\n",
+ "foo: Bar\r\n",
+ "accept: */*\r\n",
+ "accept-language: *\r\n",
+ `user-agent: Deno/${Deno.version.deno}\r\n`,
+ "accept-encoding: gzip, br\r\n",
+ `host: ${addr}\r\n\r\n`,
+ ].join("");
+ assertEquals(actual, expected);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchRequestAcceptHeaders() {
+ const addr = `127.0.0.1:${listenPort}`;
+ const bufPromise = bufferServer(addr);
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [
+ ["Accept", "text/html"],
+ ["Accept-Language", "en-US"],
+ ],
+ });
+ await response.arrayBuffer();
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode((await bufPromise).bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "content-length: 0\r\n",
+ "accept: text/html\r\n",
+ "accept-language: en-US\r\n",
+ `user-agent: Deno/${Deno.version.deno}\r\n`,
+ "accept-encoding: gzip, br\r\n",
+ `host: ${addr}\r\n\r\n`,
+ ].join("");
+ assertEquals(actual, expected);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchPostBodyString() {
+ const addr = `127.0.0.1:${listenPort}`;
+ const bufPromise = bufferServer(addr);
+ const body = "hello world";
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [
+ ["Hello", "World"],
+ ["Foo", "Bar"],
+ ],
+ body,
+ });
+ await response.arrayBuffer();
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode((await bufPromise).bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "hello: World\r\n",
+ "foo: Bar\r\n",
+ "content-type: text/plain;charset=UTF-8\r\n",
+ "accept: */*\r\n",
+ "accept-language: *\r\n",
+ `user-agent: Deno/${Deno.version.deno}\r\n`,
+ "accept-encoding: gzip, br\r\n",
+ `host: ${addr}\r\n`,
+ `content-length: ${body.length}\r\n\r\n`,
+ body,
+ ].join("");
+ assertEquals(actual, expected);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchPostBodyTypedArray() {
+ const addr = `127.0.0.1:${listenPort}`;
+ const bufPromise = bufferServer(addr);
+ const bodyStr = "hello world";
+ const body = new TextEncoder().encode(bodyStr);
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [
+ ["Hello", "World"],
+ ["Foo", "Bar"],
+ ],
+ body,
+ });
+ await response.arrayBuffer();
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode((await bufPromise).bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "hello: World\r\n",
+ "foo: Bar\r\n",
+ "accept: */*\r\n",
+ "accept-language: *\r\n",
+ `user-agent: Deno/${Deno.version.deno}\r\n`,
+ "accept-encoding: gzip, br\r\n",
+ `host: ${addr}\r\n`,
+ `content-length: ${body.byteLength}\r\n\r\n`,
+ bodyStr,
+ ].join("");
+ assertEquals(actual, expected);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchUserSetContentLength() {
+ const addr = `127.0.0.1:${listenPort}`;
+ const bufPromise = bufferServer(addr);
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [
+ ["Content-Length", "10"],
+ ],
+ });
+ await response.arrayBuffer();
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode((await bufPromise).bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "content-length: 0\r\n",
+ "accept: */*\r\n",
+ "accept-language: *\r\n",
+ `user-agent: Deno/${Deno.version.deno}\r\n`,
+ "accept-encoding: gzip, br\r\n",
+ `host: ${addr}\r\n\r\n`,
+ ].join("");
+ assertEquals(actual, expected);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchUserSetTransferEncoding() {
+ const addr = `127.0.0.1:${listenPort}`;
+ const bufPromise = bufferServer(addr);
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [
+ ["Transfer-Encoding", "chunked"],
+ ],
+ });
+ await response.arrayBuffer();
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode((await bufPromise).bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "content-length: 0\r\n",
+ `host: ${addr}\r\n`,
+ "accept: */*\r\n",
+ "accept-language: *\r\n",
+ `user-agent: Deno/${Deno.version.deno}\r\n`,
+ "accept-encoding: gzip, br\r\n\r\n",
+ ].join("");
+ assertEquals(actual, expected);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchWithNonAsciiRedirection() {
+ const response = await fetch("http://localhost:4545/non_ascii_redirect", {
+ redirect: "manual",
+ });
+ assertEquals(response.status, 301);
+ assertEquals(response.headers.get("location"), "/redirect®");
+ await response.text();
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchWithManualRedirection() {
+ const response = await fetch("http://localhost:4546/", {
+ redirect: "manual",
+ }); // will redirect to http://localhost:4545/
+ assertEquals(response.status, 301);
+ assertEquals(response.url, "http://localhost:4546/");
+ assertEquals(response.type, "basic");
+ assertEquals(response.headers.get("Location"), "http://localhost:4545/");
+ await response.body!.cancel();
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchWithErrorRedirection() {
+ await assertRejects(
+ () =>
+ fetch("http://localhost:4546/", {
+ redirect: "error",
+ }),
+ TypeError,
+ "redirect",
+ );
+ },
+);
+
+Deno.test(function responseRedirect() {
+ const redir = Response.redirect("http://example.com/newLocation", 301);
+ assertEquals(redir.status, 301);
+ assertEquals(redir.statusText, "");
+ assertEquals(redir.url, "");
+ assertEquals(
+ redir.headers.get("Location"),
+ "http://example.com/newLocation",
+ );
+ assertEquals(redir.type, "default");
+});
+
+Deno.test(function responseRedirectTakeURLObjectAsParameter() {
+ const redir = Response.redirect(new URL("https://example.com/"));
+ assertEquals(
+ redir.headers.get("Location"),
+ "https://example.com/",
+ );
+});
+
+Deno.test(async function responseWithoutBody() {
+ const response = new Response();
+ assertEquals(await response.arrayBuffer(), new ArrayBuffer(0));
+ const blob = await response.blob();
+ assertEquals(blob.size, 0);
+ assertEquals(await blob.arrayBuffer(), new ArrayBuffer(0));
+ assertEquals(await response.text(), "");
+ await assertRejects(async () => {
+ await response.json();
+ });
+});
+
+Deno.test({ permissions: { net: true } }, async function fetchBodyReadTwice() {
+ const response = await fetch("http://localhost:4545/assets/fixture.json");
+
+ // Read body
+ const _json = await response.json();
+ assert(_json);
+
+ // All calls after the body was consumed, should fail
+ const methods = ["json", "text", "formData", "arrayBuffer"] as const;
+ for (const method of methods) {
+ try {
+ await response[method]();
+ fail(
+ "Reading body multiple times should failed, the stream should've been locked.",
+ );
+ } catch {
+ // pass
+ }
+ }
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchBodyReaderAfterRead() {
+ const response = await fetch(
+ "http://localhost:4545/assets/fixture.json",
+ );
+ assert(response.body !== null);
+ const reader = await response.body.getReader();
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ assert(value);
+ }
+
+ try {
+ response.body.getReader();
+ fail("The stream should've been locked.");
+ } catch {
+ // pass
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchBodyReaderWithCancelAndNewReader() {
+ const data = "a".repeat(1 << 10);
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: data,
+ });
+ assert(response.body !== null);
+ const firstReader = await response.body.getReader();
+
+ // Acquire reader without reading & release
+ await firstReader.releaseLock();
+
+ const reader = await response.body.getReader();
+
+ let total = 0;
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ assert(value);
+ total += value.length;
+ }
+
+ assertEquals(total, data.length);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchBodyReaderWithReadCancelAndNewReader() {
+ const data = "a".repeat(1 << 10);
+
+ const response = await fetch("http://localhost:4545/echo_server", {
+ method: "POST",
+ body: data,
+ });
+ assert(response.body !== null);
+ const firstReader = await response.body.getReader();
+
+ // Do one single read with first reader
+ const { value: firstValue } = await firstReader.read();
+ assert(firstValue);
+ await firstReader.releaseLock();
+
+ // Continue read with second reader
+ const reader = await response.body.getReader();
+ let total = firstValue.length || 0;
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ assert(value);
+ total += value.length;
+ }
+ assertEquals(total, data.length);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchResourceCloseAfterStreamCancel() {
+ const res = await fetch("http://localhost:4545/assets/fixture.json");
+ assert(res.body !== null);
+
+ // After ReadableStream.cancel is called, resource handle must be closed
+ // The test should not fail with: Test case is leaking resources
+ await res.body.cancel();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchNullBodyStatus() {
+ const nullBodyStatus = [101, 204, 205, 304];
+
+ for (const status of nullBodyStatus) {
+ const headers = new Headers([["x-status", String(status)]]);
+ const res = await fetch("http://localhost:4545/echo_server", {
+ body: "deno",
+ method: "POST",
+ headers,
+ });
+ assertEquals(res.body, null);
+ assertEquals(res.status, status);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchResponseContentLength() {
+ const body = new Uint8Array(2 ** 16);
+ const headers = new Headers([["content-type", "application/octet-stream"]]);
+ const res = await fetch("http://localhost:4545/echo_server", {
+ body: body,
+ method: "POST",
+ headers,
+ });
+ assertEquals(Number(res.headers.get("content-length")), body.byteLength);
+
+ const blob = await res.blob();
+ // Make sure Body content-type is correctly set
+ assertEquals(blob.type, "application/octet-stream");
+ assertEquals(blob.size, body.byteLength);
+ },
+);
+
+Deno.test(function fetchResponseConstructorNullBody() {
+ const nullBodyStatus = [204, 205, 304];
+
+ for (const status of nullBodyStatus) {
+ try {
+ new Response("deno", { status });
+ fail("Response with null body status cannot have body");
+ } catch (e) {
+ assert(e instanceof TypeError);
+ assertEquals(
+ e.message,
+ "Response with null body status cannot have body",
+ );
+ }
+ }
+});
+
+Deno.test(function fetchResponseConstructorInvalidStatus() {
+ const invalidStatus = [100, 600, 199, null, "", NaN];
+
+ for (const status of invalidStatus) {
+ try {
+ // deno-lint-ignore ban-ts-comment
+ // @ts-ignore
+ new Response("deno", { status });
+ fail(`Invalid status: ${status}`);
+ } catch (e) {
+ assert(e instanceof RangeError);
+ assert(
+ e.message.endsWith(
+ "is not equal to 101 and outside the range [200, 599].",
+ ),
+ );
+ }
+ }
+});
+
+Deno.test(function fetchResponseEmptyConstructor() {
+ const response = new Response();
+ assertEquals(response.status, 200);
+ assertEquals(response.body, null);
+ assertEquals(response.type, "default");
+ assertEquals(response.url, "");
+ assertEquals(response.redirected, false);
+ assertEquals(response.ok, true);
+ assertEquals(response.bodyUsed, false);
+ assertEquals([...response.headers], []);
+});
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function fetchCustomHttpClientParamCertificateSuccess(): Promise<
+ void
+ > {
+ const caCert = Deno.readTextFileSync("tests/testdata/tls/RootCA.pem");
+ const client = Deno.createHttpClient({ caCerts: [caCert] });
+ const response = await fetch("https://localhost:5545/assets/fixture.json", {
+ client,
+ });
+ const json = await response.json();
+ assertEquals(json.name, "deno");
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ function createHttpClientAcceptPoolIdleTimeout() {
+ const client = Deno.createHttpClient({
+ poolIdleTimeout: 1000,
+ });
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchCustomClientUserAgent(): Promise<
+ void
+ > {
+ const data = "Hello World";
+ const client = Deno.createHttpClient({});
+ const response = await fetch("http://localhost:4545/echo_server", {
+ client,
+ method: "POST",
+ body: new TextEncoder().encode(data),
+ });
+ assertEquals(
+ response.headers.get("user-agent"),
+ `Deno/${Deno.version.deno}`,
+ );
+ await response.text();
+ client.close();
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function fetchPostBodyReadableStream() {
+ const addr = `127.0.0.1:${listenPort}`;
+ const bufPromise = bufferServer(addr);
+ const stream = new TransformStream();
+ const writer = stream.writable.getWriter();
+ // transformer writes don't resolve until they are read, so awaiting these
+ // will cause the transformer to hang, as the suspend the transformer, it
+ // is also illogical to await for the reads, as that is the whole point of
+ // streams is to have a "queue" which gets drained...
+ writer.write(new TextEncoder().encode("hello "));
+ writer.write(new TextEncoder().encode("world"));
+ writer.close();
+ const response = await fetch(`http://${addr}/blah`, {
+ method: "POST",
+ headers: [
+ ["Hello", "World"],
+ ["Foo", "Bar"],
+ ],
+ body: stream.readable,
+ });
+ await response.arrayBuffer();
+ assertEquals(response.status, 404);
+ assertEquals(response.headers.get("Content-Length"), "2");
+
+ const actual = new TextDecoder().decode((await bufPromise).bytes());
+ const expected = [
+ "POST /blah HTTP/1.1\r\n",
+ "hello: World\r\n",
+ "foo: Bar\r\n",
+ "accept: */*\r\n",
+ "accept-language: *\r\n",
+ `user-agent: Deno/${Deno.version.deno}\r\n`,
+ "accept-encoding: gzip, br\r\n",
+ `host: ${addr}\r\n`,
+ `transfer-encoding: chunked\r\n\r\n`,
+ "B\r\n",
+ "hello world\r\n",
+ "0\r\n\r\n",
+ ].join("");
+ assertEquals(actual, expected);
+ },
+);
+
+Deno.test({}, function fetchWritableRespProps() {
+ const original = new Response("https://deno.land", {
+ status: 404,
+ headers: { "x-deno": "foo" },
+ });
+ const new_ = new Response("https://deno.land", original);
+ assertEquals(original.status, new_.status);
+ assertEquals(new_.headers.get("x-deno"), "foo");
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchFilterOutCustomHostHeader(): Promise<
+ void
+ > {
+ const addr = `127.0.0.1:${listenPort}`;
+ const [hostname, port] = addr.split(":");
+ const listener = Deno.listen({
+ hostname,
+ port: Number(port),
+ }) as Deno.Listener;
+
+ let httpConn: Deno.HttpConn;
+ listener.accept().then(async (conn: Deno.Conn) => {
+ httpConn = Deno.serveHttp(conn);
+
+ await httpConn.nextRequest()
+ .then(async (requestEvent: Deno.RequestEvent | null) => {
+ const hostHeader = requestEvent?.request.headers.get("Host");
+ const headersToReturn = hostHeader
+ ? { "Host": hostHeader }
+ : undefined;
+
+ await requestEvent?.respondWith(
+ new Response("", {
+ status: 200,
+ headers: headersToReturn,
+ }),
+ );
+ });
+ });
+
+ const response = await fetch(`http://${addr}/`, {
+ headers: { "Host": "example.com" },
+ });
+ await response.text();
+ listener.close();
+ httpConn!.close();
+
+ assertEquals(response.headers.get("Host"), addr);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchNoServerReadableStreamBody() {
+ const completed = Promise.withResolvers<void>();
+ const failed = Promise.withResolvers<void>();
+ const body = new ReadableStream({
+ start(controller) {
+ controller.enqueue(new Uint8Array([1]));
+ setTimeout(async () => {
+ // This is technically a race. If the fetch has failed by this point, the enqueue will
+ // throw. If not, it will succeed. Windows appears to take a while to time out the fetch,
+ // so we will just wait for that here before we attempt to enqueue so it's consistent
+ // across platforms.
+ await failed.promise;
+ assertThrows(() => controller.enqueue(new Uint8Array([2])));
+ completed.resolve();
+ }, 1000);
+ },
+ });
+ const nonExistentHostname = "http://localhost:47582";
+ await assertRejects(async () => {
+ await fetch(nonExistentHostname, { body, method: "POST" });
+ }, TypeError);
+ failed.resolve();
+ await completed.promise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchHeadRespBody() {
+ const res = await fetch("http://localhost:4545/echo_server", {
+ method: "HEAD",
+ });
+ assertEquals(res.body, null);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function fetchClientCertWrongPrivateKey(): Promise<void> {
+ await assertRejects(async () => {
+ const client = Deno.createHttpClient({
+ certChain: "bad data",
+ privateKey: await Deno.readTextFile(
+ "tests/testdata/tls/localhost.key",
+ ),
+ });
+ await fetch("https://localhost:5552/assets/fixture.json", {
+ client,
+ });
+ }, Deno.errors.InvalidData);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function fetchClientCertBadPrivateKey(): Promise<void> {
+ await assertRejects(async () => {
+ const client = Deno.createHttpClient({
+ certChain: await Deno.readTextFile(
+ "tests/testdata/tls/localhost.crt",
+ ),
+ privateKey: "bad data",
+ });
+ await fetch("https://localhost:5552/assets/fixture.json", {
+ client,
+ });
+ }, Deno.errors.InvalidData);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function fetchClientCertNotPrivateKey(): Promise<void> {
+ await assertRejects(async () => {
+ const client = Deno.createHttpClient({
+ certChain: await Deno.readTextFile(
+ "tests/testdata/tls/localhost.crt",
+ ),
+ privateKey: "",
+ });
+ await fetch("https://localhost:5552/assets/fixture.json", {
+ client,
+ });
+ }, Deno.errors.InvalidData);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function fetchCustomClientPrivateKey(): Promise<
+ void
+ > {
+ const data = "Hello World";
+ const caCert = await Deno.readTextFile("tests/testdata/tls/RootCA.crt");
+ const client = Deno.createHttpClient({
+ certChain: await Deno.readTextFile(
+ "tests/testdata/tls/localhost.crt",
+ ),
+ privateKey: await Deno.readTextFile(
+ "tests/testdata/tls/localhost.key",
+ ),
+ caCerts: [caCert],
+ });
+ const response = await fetch("https://localhost:5552/echo_server", {
+ client,
+ method: "POST",
+ body: new TextEncoder().encode(data),
+ });
+ assertEquals(
+ response.headers.get("user-agent"),
+ `Deno/${Deno.version.deno}`,
+ );
+ await response.text();
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchAbortWhileUploadStreaming(): Promise<void> {
+ const abortController = new AbortController();
+ try {
+ await fetch(
+ "http://localhost:5552/echo_server",
+ {
+ method: "POST",
+ body: new ReadableStream({
+ pull(controller) {
+ abortController.abort();
+ controller.enqueue(new Uint8Array([1, 2, 3, 4]));
+ },
+ }),
+ signal: abortController.signal,
+ },
+ );
+ fail("Fetch didn't reject.");
+ } catch (error) {
+ assert(error instanceof DOMException);
+ assertEquals(error.name, "AbortError");
+ assertEquals(error.message, "The signal has been aborted");
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchAbortWhileUploadStreamingWithReason(): Promise<void> {
+ const abortController = new AbortController();
+ const abortReason = new Error();
+ try {
+ await fetch(
+ "http://localhost:5552/echo_server",
+ {
+ method: "POST",
+ body: new ReadableStream({
+ pull(controller) {
+ abortController.abort(abortReason);
+ controller.enqueue(new Uint8Array([1, 2, 3, 4]));
+ },
+ }),
+ signal: abortController.signal,
+ },
+ );
+ fail("Fetch didn't reject.");
+ } catch (error) {
+ assertEquals(error, abortReason);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchAbortWhileUploadStreamingWithPrimitiveReason(): Promise<
+ void
+ > {
+ const abortController = new AbortController();
+ try {
+ await fetch(
+ "http://localhost:5552/echo_server",
+ {
+ method: "POST",
+ body: new ReadableStream({
+ pull(controller) {
+ abortController.abort("Abort reason");
+ controller.enqueue(new Uint8Array([1, 2, 3, 4]));
+ },
+ }),
+ signal: abortController.signal,
+ },
+ );
+ fail("Fetch didn't reject.");
+ } catch (error) {
+ assertEquals(error, "Abort reason");
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchHeaderValueShouldNotPanic() {
+ for (let i = 0; i < 0x21; i++) {
+ if (i === 0x09 || i === 0x0A || i === 0x0D || i === 0x20) {
+ continue; // these header value will be normalized, will not cause an error.
+ }
+ // ensure there will be an error instead of panic.
+ await assertRejects(() =>
+ fetch("http://localhost:4545/echo_server", {
+ method: "HEAD",
+ headers: { "val": String.fromCharCode(i) },
+ }), TypeError);
+ }
+ await assertRejects(() =>
+ fetch("http://localhost:4545/echo_server", {
+ method: "HEAD",
+ headers: { "val": String.fromCharCode(127) },
+ }), TypeError);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchHeaderNameShouldNotPanic() {
+ const validTokens =
+ "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUWVXYZ^_`abcdefghijklmnopqrstuvwxyz|~"
+ .split("");
+ for (let i = 0; i <= 255; i++) {
+ const token = String.fromCharCode(i);
+ if (validTokens.includes(token)) {
+ continue;
+ }
+ // ensure there will be an error instead of panic.
+ await assertRejects(() =>
+ fetch("http://localhost:4545/echo_server", {
+ method: "HEAD",
+ headers: { [token]: "value" },
+ }), TypeError);
+ }
+ await assertRejects(() =>
+ fetch("http://localhost:4545/echo_server", {
+ method: "HEAD",
+ headers: { "": "value" },
+ }), TypeError);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function fetchSupportsHttpsOverIpAddress() {
+ const caCert = await Deno.readTextFile("tests/testdata/tls/RootCA.pem");
+ const client = Deno.createHttpClient({ caCerts: [caCert] });
+ const res = await fetch("https://localhost:5546/http_version", { client });
+ assert(res.ok);
+ assertEquals(await res.text(), "HTTP/1.1");
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function fetchSupportsHttp1Only() {
+ const caCert = await Deno.readTextFile("tests/testdata/tls/RootCA.pem");
+ const client = Deno.createHttpClient({ caCerts: [caCert] });
+ const res = await fetch("https://localhost:5546/http_version", { client });
+ assert(res.ok);
+ assertEquals(await res.text(), "HTTP/1.1");
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function fetchSupportsHttp2() {
+ const caCert = await Deno.readTextFile("tests/testdata/tls/RootCA.pem");
+ const client = Deno.createHttpClient({ caCerts: [caCert] });
+ const res = await fetch("https://localhost:5547/http_version", { client });
+ assert(res.ok);
+ assertEquals(await res.text(), "HTTP/2.0");
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function fetchForceHttp1OnHttp2Server() {
+ const client = Deno.createHttpClient({ http2: false, http1: true });
+ await assertRejects(
+ () => fetch("http://localhost:5549/http_version", { client }),
+ TypeError,
+ );
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function fetchForceHttp2OnHttp1Server() {
+ const client = Deno.createHttpClient({ http2: true, http1: false });
+ await assertRejects(
+ () => fetch("http://localhost:5548/http_version", { client }),
+ TypeError,
+ );
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function fetchPrefersHttp2() {
+ const caCert = await Deno.readTextFile("tests/testdata/tls/RootCA.pem");
+ const client = Deno.createHttpClient({ caCerts: [caCert] });
+ const res = await fetch("https://localhost:5545/http_version", { client });
+ assert(res.ok);
+ assertEquals(await res.text(), "HTTP/2.0");
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, read: true } },
+ async function createHttpClientAllowHost() {
+ const client = Deno.createHttpClient({
+ allowHost: true,
+ });
+ const res = await fetch("http://localhost:4545/echo_server", {
+ headers: {
+ "host": "example.com",
+ },
+ client,
+ });
+ assert(res.ok);
+ assertEquals(res.headers.get("host"), "example.com");
+ await res.body?.cancel();
+ client.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function createHttpClientExplicitResourceManagement() {
+ using client = Deno.createHttpClient({});
+ const response = await fetch("http://localhost:4545/assets/fixture.json", {
+ client,
+ });
+ const json = await response.json();
+ assertEquals(json.name, "deno");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function createHttpClientExplicitResourceManagementDoubleClose() {
+ using client = Deno.createHttpClient({});
+ const response = await fetch("http://localhost:4545/assets/fixture.json", {
+ client,
+ });
+ const json = await response.json();
+ assertEquals(json.name, "deno");
+ // Close the client even though we declared it with `using` to confirm that
+ // the cleanup done as per `Symbol.dispose` will not throw any errors.
+ client.close();
+ },
+);
+
+Deno.test({ permissions: { read: false } }, async function fetchFilePerm() {
+ await assertRejects(async () => {
+ await fetch(import.meta.resolve("../testdata/subdir/json_1.json"));
+ }, Deno.errors.PermissionDenied);
+});
+
+Deno.test(
+ { permissions: { read: false } },
+ async function fetchFilePermDoesNotExist() {
+ await assertRejects(async () => {
+ await fetch(import.meta.resolve("./bad.json"));
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function fetchFileBadMethod() {
+ await assertRejects(
+ async () => {
+ await fetch(
+ import.meta.resolve("../testdata/subdir/json_1.json"),
+ {
+ method: "POST",
+ },
+ );
+ },
+ TypeError,
+ "Fetching files only supports the GET method. Received POST.",
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function fetchFileDoesNotExist() {
+ await assertRejects(
+ async () => {
+ await fetch(import.meta.resolve("./bad.json"));
+ },
+ TypeError,
+ );
+ },
+);
+
+Deno.test(
+ { permissions: { read: true } },
+ async function fetchFile() {
+ const res = await fetch(
+ import.meta.resolve("../testdata/subdir/json_1.json"),
+ );
+ assert(res.ok);
+ const fixture = await Deno.readTextFile(
+ "tests/testdata/subdir/json_1.json",
+ );
+ assertEquals(await res.text(), fixture);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchContentLengthPost() {
+ const response = await fetch("http://localhost:4545/content_length", {
+ method: "POST",
+ });
+ const length = await response.text();
+ assertEquals(length, 'Some("0")');
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchContentLengthPut() {
+ const response = await fetch("http://localhost:4545/content_length", {
+ method: "PUT",
+ });
+ const length = await response.text();
+ assertEquals(length, 'Some("0")');
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchContentLengthPatch() {
+ const response = await fetch("http://localhost:4545/content_length", {
+ method: "PATCH",
+ });
+ const length = await response.text();
+ assertEquals(length, "None");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchContentLengthPostWithStringBody() {
+ const response = await fetch("http://localhost:4545/content_length", {
+ method: "POST",
+ body: "Hey!",
+ });
+ const length = await response.text();
+ assertEquals(length, 'Some("4")');
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchContentLengthPostWithBufferBody() {
+ const response = await fetch("http://localhost:4545/content_length", {
+ method: "POST",
+ body: new TextEncoder().encode("Hey!"),
+ });
+ const length = await response.text();
+ assertEquals(length, 'Some("4")');
+ },
+);
+
+Deno.test(async function staticResponseJson() {
+ const data = { hello: "world" };
+ const resp = Response.json(data);
+ assertEquals(resp.status, 200);
+ assertEquals(resp.headers.get("content-type"), "application/json");
+ const res = await resp.json();
+ assertEquals(res, data);
+});
+
+function invalidServer(addr: string, body: Uint8Array): Deno.Listener {
+ const [hostname, port] = addr.split(":");
+ const listener = Deno.listen({
+ hostname,
+ port: Number(port),
+ }) as Deno.Listener;
+
+ (async () => {
+ for await (const conn of listener) {
+ const p1 = conn.read(new Uint8Array(2 ** 14));
+ const p2 = conn.write(body);
+
+ await Promise.all([p1, p2]);
+ conn.close();
+ }
+ })();
+
+ return listener;
+}
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchWithInvalidContentLengthAndTransferEncoding(): Promise<
+ void
+ > {
+ const addr = `127.0.0.1:${listenPort}`;
+ const data = "a".repeat(10 << 10);
+
+ const body = new TextEncoder().encode(
+ `HTTP/1.1 200 OK\r\nContent-Length: ${
+ Math.round(data.length * 2)
+ }\r\nTransfer-Encoding: chunked\r\n\r\n${
+ data.length.toString(16)
+ }\r\n${data}\r\n0\r\n\r\n`,
+ );
+
+ // if transfer-encoding is sent, content-length is ignored
+ // even if it has an invalid value (content-length > totalLength)
+ const listener = invalidServer(addr, body);
+ const response = await fetch(`http://${addr}/`);
+
+ const res = await response.arrayBuffer();
+ const buf = new TextEncoder().encode(data);
+ assertEquals(res.byteLength, buf.byteLength);
+ assertEquals(new Uint8Array(res), buf);
+
+ listener.close();
+ },
+);
+
+Deno.test(
+ // TODO(bartlomieju): reenable this test
+ // https://github.com/denoland/deno/issues/18350
+ { ignore: Deno.build.os === "windows", permissions: { net: true } },
+ async function fetchWithInvalidContentLength(): Promise<
+ void
+ > {
+ const addr = `127.0.0.1:${listenPort}`;
+ const data = "a".repeat(10 << 10);
+
+ const body = new TextEncoder().encode(
+ `HTTP/1.1 200 OK\r\nContent-Length: ${
+ Math.round(data.length / 2)
+ }\r\nContent-Length: ${data.length}\r\n\r\n${data}`,
+ );
+
+ // It should fail if multiple content-length headers with different values are sent
+ const listener = invalidServer(addr, body);
+ await assertRejects(
+ async () => {
+ await fetch(`http://${addr}/`);
+ },
+ TypeError,
+ "invalid content-length parsed",
+ );
+
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchWithInvalidContentLength2(): Promise<
+ void
+ > {
+ const addr = `127.0.0.1:${listenPort}`;
+ const data = "a".repeat(10 << 10);
+
+ const contentLength = data.length / 2;
+ const body = new TextEncoder().encode(
+ `HTTP/1.1 200 OK\r\nContent-Length: ${contentLength}\r\n\r\n${data}`,
+ );
+
+ const listener = invalidServer(addr, body);
+ const response = await fetch(`http://${addr}/`);
+
+ // If content-length < totalLength, a maximum of content-length bytes
+ // should be returned.
+ const res = await response.arrayBuffer();
+ const buf = new TextEncoder().encode(data);
+ assertEquals(res.byteLength, contentLength);
+ assertEquals(new Uint8Array(res), buf.subarray(contentLength));
+
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchWithInvalidContentLength3(): Promise<
+ void
+ > {
+ const addr = `127.0.0.1:${listenPort}`;
+ const data = "a".repeat(10 << 10);
+
+ const contentLength = data.length * 2;
+ const body = new TextEncoder().encode(
+ `HTTP/1.1 200 OK\r\nContent-Length: ${contentLength}\r\n\r\n${data}`,
+ );
+
+ const listener = invalidServer(addr, body);
+ const response = await fetch(`http://${addr}/`);
+ // If content-length > totalLength, a maximum of content-length bytes
+ // should be returned.
+ await assertRejects(
+ async () => {
+ await response.arrayBuffer();
+ },
+ Error,
+ "end of file before message length reached",
+ );
+
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchBlobUrl(): Promise<void> {
+ const blob = new Blob(["ok"], { type: "text/plain" });
+ const url = URL.createObjectURL(blob);
+ assert(url.startsWith("blob:"), `URL was ${url}`);
+ const res = await fetch(url);
+ assertEquals(res.url, url);
+ assertEquals(res.status, 200);
+ assertEquals(res.headers.get("content-length"), "2");
+ assertEquals(res.headers.get("content-type"), "text/plain");
+ assertEquals(await res.text(), "ok");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchResponseStreamIsLockedWhileReading() {
+ const response = await fetch("http://localhost:4545/echo_server", {
+ body: new Uint8Array(5000),
+ method: "POST",
+ });
+
+ assertEquals(response.body!.locked, false);
+ const promise = response.arrayBuffer();
+ assertEquals(response.body!.locked, true);
+
+ await promise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchConstructorClones() {
+ const req = new Request("https://example.com", {
+ method: "POST",
+ body: "foo",
+ });
+ assertEquals(await req.text(), "foo");
+ await assertRejects(() => req.text());
+
+ const req2 = new Request(req, { method: "PUT", body: "bar" }); // should not have any impact on req
+ await assertRejects(() => req.text());
+ assertEquals(await req2.text(), "bar");
+
+ assertEquals(req.method, "POST");
+ assertEquals(req2.method, "PUT");
+
+ assertEquals(req.headers.get("x-foo"), null);
+ assertEquals(req2.headers.get("x-foo"), null);
+ req2.headers.set("x-foo", "bar"); // should not have any impact on req
+ assertEquals(req.headers.get("x-foo"), null);
+ assertEquals(req2.headers.get("x-foo"), "bar");
+ },
+);
+
+Deno.test(
+ // TODO(bartlomieju): reenable this test
+ // https://github.com/denoland/deno/issues/18350
+ { ignore: Deno.build.os === "windows", permissions: { net: true } },
+ async function fetchRequestBodyErrorCatchable() {
+ const listener = Deno.listen({ hostname: "127.0.0.1", port: listenPort });
+ const server = (async () => {
+ const conn = await listener.accept();
+ listener.close();
+ const buf = new Uint8Array(256);
+ const n = await conn.read(buf);
+ const data = new TextDecoder().decode(buf.subarray(0, n!)); // this is the request headers + first body chunk
+ assert(data.startsWith("POST / HTTP/1.1\r\n"));
+ assert(data.endsWith("1\r\na\r\n"));
+ const n2 = await conn.read(buf);
+ assertEquals(n2, 6); // this is the second body chunk
+ const n3 = await conn.read(buf);
+ assertEquals(n3, null); // the connection now abruptly closes because the client has errored
+ conn.close();
+ })();
+
+ const stream = new ReadableStream({
+ async start(controller) {
+ controller.enqueue(new TextEncoder().encode("a"));
+ await delay(1000);
+ controller.enqueue(new TextEncoder().encode("b"));
+ await delay(1000);
+ controller.error(new Error("foo"));
+ },
+ });
+
+ const err = await assertRejects(() =>
+ fetch(`http://localhost:${listenPort}/`, {
+ body: stream,
+ method: "POST",
+ })
+ );
+
+ assert(err instanceof TypeError, `err was not a TypeError ${err}`);
+ assert(err.cause, `err.cause was null ${err}`);
+ assert(
+ err.cause instanceof Error,
+ `err.cause was not an Error ${err.cause}`,
+ );
+ assertEquals(err.cause.message, "foo");
+
+ await server;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function fetchRequestBodyEmptyStream() {
+ const body = new ReadableStream({
+ start(controller) {
+ controller.enqueue(new Uint8Array([]));
+ controller.close();
+ },
+ });
+
+ await assertRejects(
+ async () => {
+ const controller = new AbortController();
+ const promise = fetch("http://localhost:4545/echo_server", {
+ body,
+ method: "POST",
+ signal: controller.signal,
+ });
+ try {
+ controller.abort();
+ } catch (e) {
+ console.log(e);
+ fail("abort should not throw");
+ }
+ await promise;
+ },
+ DOMException,
+ "The signal has been aborted",
+ );
+ },
+);
+
+Deno.test("Request with subarray TypedArray body", async () => {
+ const body = new Uint8Array([1, 2, 3, 4, 5]).subarray(1);
+ const req = new Request("https://example.com", { method: "POST", body });
+ const actual = new Uint8Array(await req.arrayBuffer());
+ const expected = new Uint8Array([2, 3, 4, 5]);
+ assertEquals(actual, expected);
+});
+
+Deno.test("Response with subarray TypedArray body", async () => {
+ const body = new Uint8Array([1, 2, 3, 4, 5]).subarray(1);
+ const req = new Response(body);
+ const actual = new Uint8Array(await req.arrayBuffer());
+ const expected = new Uint8Array([2, 3, 4, 5]);
+ assertEquals(actual, expected);
+});