diff options
Diffstat (limited to 'tests/unit/fetch_test.ts')
-rw-r--r-- | tests/unit/fetch_test.ts | 2071 |
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¶m2=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); +}); |