summaryrefslogtreecommitdiff
path: root/tests/unit/http_test.ts
diff options
context:
space:
mode:
authorMatt Mastracci <matthew@mastracci.com>2024-02-10 13:22:13 -0700
committerGitHub <noreply@github.com>2024-02-10 20:22:13 +0000
commitf5e46c9bf2f50d66a953fa133161fc829cecff06 (patch)
tree8faf2f5831c1c7b11d842cd9908d141082c869a5 /tests/unit/http_test.ts
parentd2477f780630a812bfd65e3987b70c0d309385bb (diff)
chore: move cli/tests/ -> tests/ (#22369)
This looks like a massive PR, but it's only a move from cli/tests -> tests, and updates of relative paths for files. This is the first step towards aggregate all of the integration test files under tests/, which will lead to a set of integration tests that can run without the CLI binary being built. While we could leave these tests under `cli`, it would require us to keep a more complex directory structure for the various test runners. In addition, we have a lot of complexity to ignore various test files in the `cli` project itself (cargo publish exclusion rules, autotests = false, etc). And finally, the `tests/` folder will eventually house the `test_ffi`, `test_napi` and other testing code, reducing the size of the root repo directory. For easier review, the extremely large and noisy "move" is in the first commit (with no changes -- just a move), while the remainder of the changes to actual files is in the second commit.
Diffstat (limited to 'tests/unit/http_test.ts')
-rw-r--r--tests/unit/http_test.ts2801
1 files changed, 2801 insertions, 0 deletions
diff --git a/tests/unit/http_test.ts b/tests/unit/http_test.ts
new file mode 100644
index 000000000..17023004e
--- /dev/null
+++ b/tests/unit/http_test.ts
@@ -0,0 +1,2801 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { Buffer, BufReader, BufWriter } from "@test_util/std/io/mod.ts";
+import { TextProtoReader } from "../testdata/run/textproto.ts";
+import {
+ assert,
+ assertEquals,
+ assertRejects,
+ assertStrictEquals,
+ assertThrows,
+ delay,
+ fail,
+} from "./test_util.ts";
+import { join } from "@test_util/std/path/mod.ts";
+
+const listenPort = 4507;
+const listenPort2 = 4508;
+
+const {
+ buildCaseInsensitiveCommaValueFinder,
+ // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
+} = Deno[Deno.internal];
+
+async function writeRequestAndReadResponse(conn: Deno.Conn): Promise<string> {
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ const w = new BufWriter(conn);
+ const r = new BufReader(conn);
+ const body = `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`;
+ const writeResult = await w.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await w.flush();
+ const tpr = new TextProtoReader(r);
+ const statusLine = await tpr.readLine();
+ assert(statusLine !== null);
+ const headers = await tpr.readMimeHeader();
+ assert(headers !== null);
+
+ const chunkedReader = chunkedBodyReader(headers, r);
+ const buf = new Uint8Array(5);
+ const dest = new Buffer();
+ let result: number | null;
+ while ((result = await chunkedReader.read(buf)) !== null) {
+ const len = Math.min(buf.byteLength, result);
+ await dest.write(buf.subarray(0, len));
+ }
+ return decoder.decode(dest.bytes());
+}
+
+Deno.test({ permissions: { net: true } }, async function httpServerBasic() {
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ assertEquals(new URL(request.url).href, `http://127.0.0.1:${listenPort}/`);
+ assertEquals(await request.text(), "");
+ await respondWith(
+ new Response("Hello World", { headers: { "foo": "bar" } }),
+ );
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
+ headers: { "connection": "close" },
+ });
+ const clone = resp.clone();
+ const text = await resp.text();
+ assertEquals(text, "Hello World");
+ assertEquals(resp.headers.get("foo"), "bar");
+ const cloneText = await clone.text();
+ assertEquals(cloneText, "Hello World");
+ await promise;
+
+ httpConn!.close();
+});
+
+// https://github.com/denoland/deno/issues/15107
+Deno.test(
+ { permissions: { net: true } },
+ async function httpLazyHeadersIssue15107() {
+ let headers: Headers;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: 2333 });
+ const conn = await listener.accept();
+ listener.close();
+ const httpConn = Deno.serveHttp(conn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request } = e;
+ request.text();
+ headers = request.headers;
+ httpConn!.close();
+ })();
+
+ const conn = await Deno.connect({ port: 2333 });
+ // Send GET request with a body + content-length.
+ const encoder = new TextEncoder();
+ const body =
+ `GET / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nContent-Length: 5\r\n\r\n12345`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await promise;
+ conn.close();
+ assertEquals(headers!.get("content-length"), "5");
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpReadHeadersAfterClose() {
+ const promise = (async () => {
+ const listener = Deno.listen({ port: 2334 });
+ const conn = await listener.accept();
+ listener.close();
+ const httpConn = Deno.serveHttp(conn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+
+ await request.text(); // Read body
+ await respondWith(new Response("Hello World")); // Closes request
+
+ assertThrows(() => request.headers, TypeError, "request closed");
+ httpConn!.close();
+ })();
+
+ const conn = await Deno.connect({ port: 2334 });
+ // Send GET request with a body + content-length.
+ const encoder = new TextEncoder();
+ const body =
+ `GET / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nContent-Length: 5\r\n\r\n12345`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await promise;
+ conn.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerGetRequestBody() {
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.body, null);
+ await respondWith(new Response("", { headers: {} }));
+ })();
+
+ const conn = await Deno.connect({ port: listenPort });
+ // Send GET request with a body + content-length.
+ const encoder = new TextEncoder();
+ const body =
+ `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\nContent-Length: 5\r\n\r\n12345`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+
+ const resp = new Uint8Array(200);
+ const readResult = await conn.read(resp);
+ assertEquals(readResult, 138);
+
+ conn.close();
+
+ await promise;
+ httpConn!.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerStreamResponse() {
+ const stream = new TransformStream();
+ const writer = stream.writable.getWriter();
+ writer.write(new TextEncoder().encode("hello "));
+ writer.write(new TextEncoder().encode("world"));
+ writer.close();
+
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ httpConn = Deno.serveHttp(conn);
+ const evt = await httpConn.nextRequest();
+ assert(evt);
+ const { request, respondWith } = evt;
+ assert(!request.body);
+ await respondWith(new Response(stream.readable));
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
+ const respBody = await resp.text();
+ assertEquals("hello world", respBody);
+ await promise;
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerStreamRequest() {
+ const stream = new TransformStream();
+ const writer = stream.writable.getWriter();
+ writer.write(new TextEncoder().encode("hello "));
+ writer.write(new TextEncoder().encode("world"));
+ writer.close();
+
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ const httpConn = Deno.serveHttp(conn);
+ const evt = await httpConn.nextRequest();
+ assert(evt);
+ const { request, respondWith } = evt;
+ const reqBody = await request.text();
+ assertEquals("hello world", reqBody);
+ await respondWith(new Response(""));
+
+ // TODO(ry) If we don't call httpConn.nextRequest() here we get "error sending
+ // request for url (https://localhost:${listenPort}/): connection closed before
+ // message completed".
+ assertEquals(await httpConn.nextRequest(), null);
+
+ listener.close();
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
+ body: stream.readable,
+ method: "POST",
+ headers: { "connection": "close" },
+ });
+
+ await resp.arrayBuffer();
+ await promise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerStreamDuplex() {
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ httpConn = Deno.serveHttp(conn);
+ const evt = await httpConn.nextRequest();
+ assert(evt);
+ const { request, respondWith } = evt;
+ assert(request.body);
+ await respondWith(new Response(request.body));
+ })();
+
+ const ts = new TransformStream();
+ const writable = ts.writable.getWriter();
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
+ method: "POST",
+ body: ts.readable,
+ });
+ assert(resp.body);
+ const reader = resp.body.getReader();
+ await writable.write(new Uint8Array([1]));
+ const chunk1 = await reader.read();
+ assert(!chunk1.done);
+ assertEquals(chunk1.value, new Uint8Array([1]));
+ await writable.write(new Uint8Array([2]));
+ const chunk2 = await reader.read();
+ assert(!chunk2.done);
+ assertEquals(chunk2.value, new Uint8Array([2]));
+
+ await writable.close();
+ const chunk3 = await reader.read();
+ assert(chunk3.done);
+ await promise;
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function httpServerClose() {
+ const listener = Deno.listen({ port: listenPort });
+ const client = await Deno.connect({ port: listenPort });
+ const httpConn = Deno.serveHttp(await listener.accept());
+ client.close();
+ const evt = await httpConn.nextRequest();
+ assertEquals(evt, null);
+ // Note httpConn is automatically closed when "done" is reached.
+ listener.close();
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerInvalidMethod() {
+ const listener = Deno.listen({ port: listenPort });
+ const client = await Deno.connect({ port: listenPort });
+ const httpConn = Deno.serveHttp(await listener.accept());
+ await client.write(new Uint8Array([1, 2, 3]));
+ await assertRejects(
+ async () => {
+ await httpConn.nextRequest();
+ },
+ Deno.errors.Http,
+ "invalid HTTP method parsed",
+ );
+ // Note httpConn is automatically closed when it errors.
+ client.close();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, net: true } },
+ async function httpServerWithTls() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ const promise = (async () => {
+ const listener = Deno.listenTls({
+ hostname,
+ port,
+ cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"),
+ key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"),
+ });
+ const conn = await listener.accept();
+ const httpConn = Deno.serveHttp(conn);
+ const evt = await httpConn.nextRequest();
+ assert(evt);
+ const { respondWith } = evt;
+ await respondWith(new Response("Hello World"));
+
+ // TODO(ry) If we don't call httpConn.nextRequest() here we get "error sending
+ // request for url (https://localhost:${listenPort}/): connection closed before
+ // message completed".
+ assertEquals(await httpConn.nextRequest(), null);
+
+ listener.close();
+ })();
+
+ const caCert = Deno.readTextFileSync("tests/testdata/tls/RootCA.pem");
+ const client = Deno.createHttpClient({ caCerts: [caCert] });
+ const resp = await fetch(`https://${hostname}:${port}/`, {
+ headers: { "connection": "close" },
+ client,
+ });
+ client.close();
+ const respBody = await resp.text();
+ assertEquals("Hello World", respBody);
+ await promise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerRegressionHang() {
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ httpConn = Deno.serveHttp(conn);
+ const event = await httpConn.nextRequest();
+ assert(event);
+ const { request, respondWith } = event;
+ const reqBody = await request.text();
+ assertEquals("request", reqBody);
+ await respondWith(new Response("response"));
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
+ method: "POST",
+ body: "request",
+ });
+ const respBody = await resp.text();
+ assertEquals("response", respBody);
+ await promise;
+
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerCancelBodyOnResponseFailure() {
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ const httpConn = Deno.serveHttp(conn);
+ const event = await httpConn.nextRequest();
+ assert(event);
+ const { respondWith } = event;
+ let cancelReason: string;
+ await assertRejects(
+ async () => {
+ let interval = 0;
+ await respondWith(
+ new Response(
+ new ReadableStream({
+ start(controller) {
+ interval = setInterval(() => {
+ const message = `data: ${Date.now()}\n\n`;
+ controller.enqueue(new TextEncoder().encode(message));
+ }, 200);
+ },
+ cancel(reason) {
+ cancelReason = reason;
+ clearInterval(interval);
+ },
+ }),
+ ),
+ );
+ },
+ Deno.errors.Http,
+ cancelReason!,
+ );
+ assert(cancelReason!);
+ httpConn!.close();
+ listener.close();
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
+ await resp.body!.cancel();
+ await promise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerNextRequestErrorExposedInResponse() {
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ const httpConn = Deno.serveHttp(conn);
+ const event = await httpConn.nextRequest();
+ assert(event);
+ // Start polling for the next request before awaiting response.
+ const nextRequestPromise = httpConn.nextRequest();
+ const { respondWith } = event;
+ await assertRejects(
+ async () => {
+ let interval = 0;
+ await respondWith(
+ new Response(
+ new ReadableStream({
+ start(controller) {
+ interval = setInterval(() => {
+ const message = `data: ${Date.now()}\n\n`;
+ controller.enqueue(new TextEncoder().encode(message));
+ }, 200);
+ },
+ cancel() {
+ clearInterval(interval);
+ },
+ }),
+ ),
+ );
+ },
+ Deno.errors.Http,
+ "connection closed",
+ );
+ // The error from `op_http_accept` reroutes to `respondWith()`.
+ assertEquals(await nextRequestPromise, null);
+ listener.close();
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
+ await resp.body!.cancel();
+ await promise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerEmptyBlobResponse() {
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ httpConn = Deno.serveHttp(conn);
+ const event = await httpConn.nextRequest();
+ assert(event);
+ const { respondWith } = event;
+ await respondWith(new Response(new Blob([])));
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
+ const respBody = await resp.text();
+ assertEquals("", respBody);
+ await promise;
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerNextRequestResolvesOnClose() {
+ const httpConnList: Deno.HttpConn[] = [];
+
+ async function serve(l: Deno.Listener) {
+ for await (const conn of l) {
+ (async () => {
+ const c = Deno.serveHttp(conn);
+ httpConnList.push(c);
+ for await (const { respondWith } of c) {
+ respondWith(new Response("hello"));
+ }
+ })();
+ }
+ }
+
+ const l = Deno.listen({ port: listenPort });
+ serve(l);
+
+ await delay(300);
+ const res = await fetch(`http://localhost:${listenPort}/`);
+ const _text = await res.text();
+
+ // Close connection and listener.
+ httpConnList.forEach((conn) => conn.close());
+ l.close();
+
+ await delay(300);
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ // Issue: https://github.com/denoland/deno/issues/10870
+ async function httpServerHang() {
+ // Quick and dirty way to make a readable stream from a string. Alternatively,
+ // `readableStreamFromReader(file)` could be used.
+ function stream(s: string): ReadableStream<Uint8Array> {
+ return new Response(s).body!;
+ }
+
+ const httpConns: Deno.HttpConn[] = [];
+ const promise = (async () => {
+ let count = 0;
+ const listener = Deno.listen({ port: listenPort });
+ for await (const conn of listener) {
+ (async () => {
+ const httpConn = Deno.serveHttp(conn);
+ httpConns.push(httpConn);
+ for await (const { respondWith } of httpConn) {
+ respondWith(new Response(stream("hello")));
+
+ count++;
+ if (count >= 2) {
+ listener.close();
+ }
+ }
+ })();
+ }
+ })();
+
+ const clientConn = await Deno.connect({ port: listenPort });
+
+ const r1 = await writeRequestAndReadResponse(clientConn);
+ assertEquals(r1, "hello");
+
+ const r2 = await writeRequestAndReadResponse(clientConn);
+ assertEquals(r2, "hello");
+
+ clientConn.close();
+ await promise;
+ for (const conn of httpConns) {
+ conn.close();
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ // Issue: https://github.com/denoland/deno/issues/10930
+ async function httpServerStreamingResponse() {
+ // This test enqueues a single chunk for readable
+ // stream and waits for client to read that chunk and signal
+ // it before enqueueing subsequent chunk. Issue linked above
+ // presented a situation where enqueued chunks were not
+ // written to the HTTP connection until the next chunk was enqueued.
+
+ let counter = 0;
+
+ const deferreds = [
+ Promise.withResolvers<void>(),
+ Promise.withResolvers<void>(),
+ Promise.withResolvers<void>(),
+ ];
+
+ async function writeRequest(conn: Deno.Conn) {
+ const encoder = new TextEncoder();
+ const decoder = new TextDecoder();
+
+ const w = new BufWriter(conn);
+ const r = new BufReader(conn);
+ const body = `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`;
+ const writeResult = await w.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await w.flush();
+ const tpr = new TextProtoReader(r);
+ const statusLine = await tpr.readLine();
+ assert(statusLine !== null);
+ const headers = await tpr.readMimeHeader();
+ assert(headers !== null);
+
+ const chunkedReader = chunkedBodyReader(headers, r);
+ const buf = new Uint8Array(5);
+ const dest = new Buffer();
+ let result: number | null;
+ while ((result = await chunkedReader.read(buf)) !== null) {
+ const len = Math.min(buf.byteLength, result);
+ await dest.write(buf.subarray(0, len));
+ // Resolve a deferred - this will make response stream to
+ // enqueue next chunk.
+ deferreds[counter - 1].resolve();
+ }
+ return decoder.decode(dest.bytes());
+ }
+
+ function periodicStream() {
+ return new ReadableStream({
+ start(controller) {
+ controller.enqueue(`${counter}\n`);
+ counter++;
+ },
+
+ async pull(controller) {
+ if (counter >= 3) {
+ return controller.close();
+ }
+
+ await deferreds[counter - 1].promise;
+
+ controller.enqueue(`${counter}\n`);
+ counter++;
+ },
+ }).pipeThrough(new TextEncoderStream());
+ }
+
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const finished = (async () => {
+ const conn = await listener.accept();
+ httpConn = Deno.serveHttp(conn);
+ const requestEvent = await httpConn.nextRequest();
+ const { respondWith } = requestEvent!;
+ await respondWith(new Response(periodicStream()));
+ })();
+
+ // start a client
+ const clientConn = await Deno.connect({ port: listenPort });
+
+ const r1 = await writeRequest(clientConn);
+ assertEquals(r1, "0\n1\n2\n");
+
+ await finished;
+ clientConn.close();
+
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpRequestLatin1Headers() {
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ assertEquals(request.headers.get("X-Header-Test"), "á");
+ await respondWith(
+ new Response("", { headers: { "X-Header-Test": "Æ" } }),
+ );
+ })();
+
+ const clientConn = await Deno.connect({ port: listenPort });
+ const requestText =
+ `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\nX-Header-Test: á\r\n\r\n`;
+ const requestBytes = new Uint8Array(requestText.length);
+ for (let i = 0; i < requestText.length; i++) {
+ requestBytes[i] = requestText.charCodeAt(i);
+ }
+ let written = 0;
+ while (written < requestBytes.byteLength) {
+ written += await clientConn.write(requestBytes.slice(written));
+ }
+
+ let responseText = "";
+ const buf = new Uint8Array(1024);
+ let read;
+
+ while ((read = await clientConn.read(buf)) !== null) {
+ httpConn!.close();
+ for (let i = 0; i < read; i++) {
+ responseText += String.fromCharCode(buf[i]);
+ }
+ }
+
+ clientConn.close();
+
+ assert(/\r\n[Xx]-[Hh]eader-[Tt]est: Æ\r\n/.test(responseText));
+
+ await promise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerRequestWithoutPath() {
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ assertEquals(
+ new URL(request.url).href,
+ `http://127.0.0.1:${listenPort}/`,
+ );
+ assertEquals(await request.text(), "");
+ await respondWith(new Response());
+ })();
+
+ const clientConn = await Deno.connect({ port: listenPort });
+
+ async function writeRequest(conn: Deno.Conn) {
+ const encoder = new TextEncoder();
+
+ const w = new BufWriter(conn);
+ const r = new BufReader(conn);
+ const body =
+ `CONNECT 127.0.0.1:${listenPort} HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`;
+ const writeResult = await w.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+ await w.flush();
+ const tpr = new TextProtoReader(r);
+ const statusLine = await tpr.readLine();
+ assert(statusLine !== null);
+ const m = statusLine.match(/^(.+?) (.+?) (.+?)$/);
+ assert(m !== null, "must be matched");
+ const [_, _proto, status, _ok] = m;
+ assertEquals(status, "200");
+ const headers = await tpr.readMimeHeader();
+ assert(headers !== null);
+ }
+
+ await writeRequest(clientConn);
+ clientConn.close();
+ await promise;
+ httpConn!.close();
+ },
+);
+
+Deno.test({ permissions: { net: true } }, async function httpServerWebSocket() {
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ const httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ const {
+ response,
+ socket,
+ } = Deno.upgradeWebSocket(request);
+ socket.onerror = () => fail();
+ socket.onmessage = (m) => {
+ socket.send(m.data);
+ socket.close(1001);
+ };
+ const close = new Promise<void>((resolve) => {
+ socket.onclose = () => resolve();
+ });
+ await respondWith(response);
+ await close;
+ })();
+
+ const def = Promise.withResolvers<void>();
+ const ws = new WebSocket(`ws://localhost:${listenPort}`);
+ ws.onmessage = (m) => assertEquals(m.data, "foo");
+ ws.onerror = () => fail();
+ ws.onclose = () => def.resolve();
+ ws.onopen = () => ws.send("foo");
+ await def.promise;
+ await promise;
+});
+
+Deno.test(function httpUpgradeWebSocket() {
+ const request = new Request("https://deno.land/", {
+ headers: {
+ connection: "Upgrade",
+ upgrade: "websocket",
+ "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
+ },
+ });
+ const { response } = Deno.upgradeWebSocket(request);
+ assertEquals(response.status, 101);
+ assertEquals(response.headers.get("connection"), "Upgrade");
+ assertEquals(response.headers.get("upgrade"), "websocket");
+ assertEquals(
+ response.headers.get("sec-websocket-accept"),
+ "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=",
+ );
+});
+
+Deno.test(function httpUpgradeWebSocketMultipleConnectionOptions() {
+ const request = new Request("https://deno.land/", {
+ headers: {
+ connection: "keep-alive, upgrade",
+ upgrade: "websocket",
+ "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
+ },
+ });
+ const { response } = Deno.upgradeWebSocket(request);
+ assertEquals(response.status, 101);
+});
+
+Deno.test(function httpUpgradeWebSocketMultipleUpgradeOptions() {
+ const request = new Request("https://deno.land/", {
+ headers: {
+ connection: "upgrade",
+ upgrade: "websocket, foo",
+ "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
+ },
+ });
+ const { response } = Deno.upgradeWebSocket(request);
+ assertEquals(response.status, 101);
+});
+
+Deno.test(function httpUpgradeWebSocketCaseInsensitiveUpgradeHeader() {
+ const request = new Request("https://deno.land/", {
+ headers: {
+ connection: "upgrade",
+ upgrade: "Websocket",
+ "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
+ },
+ });
+ const { response } = Deno.upgradeWebSocket(request);
+ assertEquals(response.status, 101);
+});
+
+Deno.test(function httpUpgradeWebSocketInvalidUpgradeHeader() {
+ assertThrows(
+ () => {
+ const request = new Request("https://deno.land/", {
+ headers: {
+ connection: "upgrade",
+ upgrade: "invalid",
+ "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
+ },
+ });
+ Deno.upgradeWebSocket(request);
+ },
+ TypeError,
+ "Invalid Header: 'upgrade' header must contain 'websocket'",
+ );
+});
+
+Deno.test(function httpUpgradeWebSocketWithoutUpgradeHeader() {
+ assertThrows(
+ () => {
+ const request = new Request("https://deno.land/", {
+ headers: {
+ connection: "upgrade",
+ "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
+ },
+ });
+ Deno.upgradeWebSocket(request);
+ },
+ TypeError,
+ "Invalid Header: 'upgrade' header must contain 'websocket'",
+ );
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpCookieConcatenation() {
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ assertEquals(
+ new URL(request.url).href,
+ `http://127.0.0.1:${listenPort}/`,
+ );
+ assertEquals(await request.text(), "");
+ assertEquals(request.headers.get("cookie"), "foo=bar; bar=foo");
+ await respondWith(new Response("ok"));
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
+ headers: [
+ ["connection", "close"],
+ ["cookie", "foo=bar"],
+ ["cookie", "bar=foo"],
+ ],
+ });
+ const text = await resp.text();
+ assertEquals(text, "ok");
+ await promise;
+ httpConn!.close();
+ },
+);
+
+// https://github.com/denoland/deno/issues/11651
+Deno.test({ permissions: { net: true } }, async function httpServerPanic() {
+ const listener = Deno.listen({ port: listenPort });
+ const client = await Deno.connect({ port: listenPort });
+ const conn = await listener.accept();
+ const httpConn = Deno.serveHttp(conn);
+
+ // This message is incomplete on purpose, we'll forcefully close client connection
+ // after it's flushed to cause connection to error out on the server side.
+ const encoder = new TextEncoder();
+ await client.write(encoder.encode("GET / HTTP/1.1"));
+
+ httpConn.nextRequest();
+ await client.write(encoder.encode("\r\n\r\n"));
+ httpConn!.close();
+
+ client.close();
+ listener.close();
+});
+
+Deno.test(
+ { permissions: { net: true, write: true, read: true } },
+ async function httpServerCorrectSizeResponse() {
+ const tmpFile = await Deno.makeTempFile();
+ using file = await Deno.open(tmpFile, { write: true, read: true });
+ await file.write(new Uint8Array(70 * 1024).fill(1)); // 70kb sent in 64kb + 6kb chunks
+
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ httpConn = Deno.serveHttp(conn);
+ const ev = await httpConn.nextRequest();
+ const { respondWith } = ev!;
+ const f = await Deno.open(tmpFile, { read: true });
+ await respondWith(new Response(f.readable, { status: 200 }));
+ })();
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
+ const body = await resp.arrayBuffer();
+ assertEquals(body.byteLength, 70 * 1024);
+ await promise;
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true, write: true, read: true } },
+ async function httpServerClosedStream() {
+ const listener = Deno.listen({ port: listenPort });
+
+ const client = await Deno.connect({ port: listenPort });
+ await client.write(new TextEncoder().encode(
+ `GET / HTTP/1.0\r\n\r\n`,
+ ));
+
+ const conn = await listener.accept();
+ const httpConn = Deno.serveHttp(conn);
+ const ev = await httpConn.nextRequest();
+ const { respondWith } = ev!;
+
+ const tmpFile = await Deno.makeTempFile();
+ const file = await Deno.open(tmpFile, { write: true, read: true });
+ await file.write(new TextEncoder().encode("hello"));
+
+ const reader = await file.readable.getReader();
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ assert(value);
+ }
+
+ let didThrow = false;
+ try {
+ await respondWith(new Response(file.readable));
+ } catch {
+ // pass
+ didThrow = true;
+ }
+
+ assert(didThrow);
+ httpConn!.close();
+ listener.close();
+ client.close();
+ },
+);
+
+// https://github.com/denoland/deno/issues/11595
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerIncompleteMessage() {
+ const listener = Deno.listen({ port: listenPort });
+
+ const client = await Deno.connect({ port: listenPort });
+ await client.write(new TextEncoder().encode(
+ `GET / HTTP/1.0\r\n\r\n`,
+ ));
+
+ const conn = await listener.accept();
+ const httpConn = Deno.serveHttp(conn);
+ const ev = await httpConn.nextRequest();
+ const { respondWith } = ev!;
+
+ const errors: Error[] = [];
+
+ const readable = new ReadableStream({
+ async pull(controller) {
+ client.close();
+ await delay(1000);
+ controller.enqueue(new TextEncoder().encode(
+ "written to the writable side of a TransformStream",
+ ));
+ controller.close();
+ },
+ cancel(error) {
+ errors.push(error);
+ },
+ });
+
+ const res = new Response(readable);
+
+ await respondWith(res).catch((error: Error) => errors.push(error));
+
+ httpConn!.close();
+ listener.close();
+
+ assert(errors.length >= 1);
+ for (const error of errors) {
+ assertEquals(error.name, "Http");
+ assert(error.message.includes("connection"));
+ }
+ },
+);
+
+// https://github.com/denoland/deno/issues/11743
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerDoesntLeakResources() {
+ const listener = Deno.listen({ port: listenPort });
+ const [conn, clientConn] = await Promise.all([
+ listener.accept(),
+ Deno.connect({ port: listenPort }),
+ ]);
+ const httpConn = Deno.serveHttp(conn);
+
+ await Promise.all([
+ httpConn.nextRequest(),
+ clientConn.write(new TextEncoder().encode(
+ `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`,
+ )),
+ ]);
+
+ httpConn!.close();
+ listener.close();
+ clientConn.close();
+ },
+);
+
+// https://github.com/denoland/deno/issues/11926
+// verify that the only new resource is "httpConnection", to make
+// sure "request" resource is closed even if its body was not read
+// by server handler
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerDoesntLeakResources2() {
+ let listener: Deno.Listener;
+ let httpConn: Deno.HttpConn;
+
+ const promise = (async () => {
+ listener = Deno.listen({ port: listenPort });
+ for await (const conn of listener) {
+ httpConn = Deno.serveHttp(conn);
+ for await (const { request, respondWith } of httpConn) {
+ assertEquals(
+ new URL(request.url).href,
+ `http://127.0.0.1:${listenPort}/`,
+ );
+ // not reading request body on purpose
+ respondWith(new Response("ok"));
+ }
+ }
+ })();
+
+ const response = await fetch(`http://127.0.0.1:${listenPort}`, {
+ method: "POST",
+ body: "hello world",
+ });
+ await response.text();
+
+ listener!.close();
+ httpConn!.close();
+ await promise;
+ },
+);
+
+// https://github.com/denoland/deno/pull/12216
+Deno.test(
+ { permissions: { net: true } },
+ async function droppedConnSenderNoPanic() {
+ async function server() {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ const http = Deno.serveHttp(conn);
+ const evt = await http.nextRequest();
+ http.close();
+ try {
+ await evt!.respondWith(new Response("boom"));
+ } catch {
+ // Ignore error.
+ }
+ listener.close();
+ }
+
+ async function client() {
+ try {
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
+ await resp.body?.cancel();
+ } catch {
+ // Ignore error
+ }
+ }
+
+ await Promise.all([server(), client()]);
+ },
+);
+
+// https://github.com/denoland/deno/issues/12193
+Deno.test(
+ { permissions: { net: true } },
+ async function httpConnConcurrentNextRequestCalls() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ hostname, port });
+ async function server() {
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const promises = new Array(10).fill(null).map(async (_, i) => {
+ const event = await httpConn.nextRequest();
+ assert(event);
+ const { pathname } = new URL(event.request.url);
+ assertStrictEquals(pathname, `/${i}`);
+ const response = new Response(`Response #${i}`);
+ await event.respondWith(response);
+ });
+ await Promise.all(promises);
+ }
+
+ async function client() {
+ for (let i = 0; i < 10; i++) {
+ const response = await fetch(`http://${hostname}:${port}/${i}`);
+ const body = await response.text();
+ assertStrictEquals(body, `Response #${i}`);
+ }
+ }
+
+ await Promise.all([server(), delay(100).then(client)]);
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+// https://github.com/denoland/deno/pull/12704
+// https://github.com/denoland/deno/pull/12732
+Deno.test(
+ { permissions: { net: true } },
+ async function httpConnAutoCloseDelayedOnUpgrade() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ const httpConn = Deno.serveHttp(tcpConn);
+
+ const event1 = await httpConn.nextRequest() as Deno.RequestEvent;
+ const event2Promise = httpConn.nextRequest();
+
+ const { socket, response } = Deno.upgradeWebSocket(event1.request);
+ socket.onmessage = (event) => socket.send(event.data);
+ const socketClosed = new Promise<void>((resolve) => {
+ socket.onclose = () => resolve();
+ });
+ event1.respondWith(response);
+
+ const event2 = await event2Promise;
+ assertStrictEquals(event2, null);
+
+ listener.close();
+ await socketClosed;
+ }
+
+ async function client() {
+ const socket = new WebSocket(`ws://${hostname}:${port}/`);
+ socket.onopen = () => socket.send("bla bla");
+ const closed = new Promise<void>((resolve) => {
+ socket.onclose = () => resolve();
+ });
+ const { data } = await new Promise<MessageEvent<string>>((res) =>
+ socket.onmessage = res
+ );
+ assertStrictEquals(data, "bla bla");
+ socket.close();
+ await closed;
+ }
+
+ await Promise.all([server(), client()]);
+ },
+);
+
+// https://github.com/denoland/deno/issues/12741
+// https://github.com/denoland/deno/pull/12746
+// https://github.com/denoland/deno/pull/12798
+Deno.test(
+ { permissions: { net: true, run: true } },
+ async function httpServerDeleteRequestHasBody() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ hostname, port });
+ async function server() {
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const event = await httpConn.nextRequest() as Deno.RequestEvent;
+ assert(event.request.body);
+ const response = new Response();
+ await event.respondWith(response);
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = ["-X", "DELETE", url];
+ const { success } = await new Deno.Command("curl", {
+ args,
+ stdout: "null",
+ stderr: "null",
+ }).output();
+ assert(success);
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ listener.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerRespondNonAsciiUint8Array() {
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.body, null);
+ await respondWith(
+ new Response(new Uint8Array([128]), {}),
+ );
+ })();
+
+ const resp = await fetch(`http://localhost:${listenPort}/`);
+ assertEquals(resp.status, 200);
+ const body = await resp.arrayBuffer();
+ assertEquals(new Uint8Array(body), new Uint8Array([128]));
+
+ await promise;
+ httpConn!.close();
+ },
+);
+
+function tmpUnixSocketPath(): string {
+ const folder = Deno.makeTempDirSync();
+ return join(folder, "socket");
+}
+
+// https://github.com/denoland/deno/pull/13628
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ async function httpServerOnUnixSocket() {
+ const filePath = tmpUnixSocketPath();
+
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ path: filePath, transport: "unix" });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ const url = new URL(request.url);
+ assertEquals(url.protocol, "http+unix:");
+ assertEquals(decodeURIComponent(url.host), filePath);
+ assertEquals(url.pathname, "/path/name");
+ await respondWith(new Response("", { headers: {} }));
+ })();
+
+ // fetch() does not supports unix domain sockets yet https://github.com/denoland/deno/issues/8821
+ const conn = await Deno.connect({ path: filePath, transport: "unix" });
+ const encoder = new TextEncoder();
+ // The Host header must be present and empty if it is not a Internet host name (RFC2616, Section 14.23)
+ const body = `GET /path/name HTTP/1.1\r\nHost:\r\n\r\n`;
+ const writeResult = await conn.write(encoder.encode(body));
+ assertEquals(body.length, writeResult);
+
+ const resp = new Uint8Array(200);
+ const readResult = await conn.read(resp);
+ assertEquals(readResult, 138);
+
+ conn.close();
+
+ await promise;
+ httpConn!.close();
+ },
+);
+
+/* Automatic Body Compression */
+
+const decoder = new TextDecoder();
+
+Deno.test({
+ name: "http server compresses body - check headers",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+ const listener = Deno.listen({ hostname, port });
+
+ const data = { hello: "deno", now: "with", compressed: "body" };
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(JSON.stringify(data), {
+ headers: { "content-type": "application/json" },
+ });
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(output.includes("content-encoding: gzip\r\n"));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server compresses body - check body",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+ const listener = Deno.listen({ hostname, port });
+
+ const data = { hello: "deno", now: "with", compressed: "body" };
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(JSON.stringify(data), {
+ headers: { "content-type": "application/json" },
+ });
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const proc = new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).spawn();
+ const status = await proc.status;
+ assert(status.success);
+ const stdout = proc.stdout
+ .pipeThrough(new DecompressionStream("gzip"))
+ .pipeThrough(new TextDecoderStream());
+ let body = "";
+ for await (const chunk of stdout) {
+ body += chunk;
+ }
+ assertEquals(JSON.parse(body), data);
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server doesn't compress small body",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(
+ JSON.stringify({ hello: "deno" }),
+ {
+ headers: { "content-type": "application/json" },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout).toLocaleLowerCase();
+ assert(output.includes("vary: accept-encoding\r\n"));
+ assert(!output.includes("content-encoding: "));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server respects accept-encoding weights",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(
+ request.headers.get("Accept-Encoding"),
+ "gzip;q=0.8, br;q=1.0, *;q=0.1",
+ );
+ const response = new Response(
+ JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
+ {
+ headers: { "content-type": "application/json" },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip;q=0.8, br;q=1.0, *;q=0.1",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(output.includes("content-encoding: br\r\n"));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server augments vary header",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(
+ JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
+ {
+ headers: { "content-type": "application/json", vary: "Accept" },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding, Accept\r\n"));
+ assert(output.includes("content-encoding: gzip\r\n"));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server weakens etag header",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(
+ JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
+ {
+ headers: {
+ "content-type": "application/json",
+ etag: "33a64df551425fcc55e4d42a148795d9f25f89d4",
+ },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "curl",
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(
+ output.includes("etag: W/33a64df551425fcc55e4d42a148795d9f25f89d4\r\n"),
+ );
+ assert(output.includes("content-encoding: gzip\r\n"));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server passes through weak etag header",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(
+ JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
+ {
+ headers: {
+ "content-type": "application/json",
+ etag: "W/33a64df551425fcc55e4d42a148795d9f25f89d4",
+ },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(
+ output.includes("etag: W/33a64df551425fcc55e4d42a148795d9f25f89d4\r\n"),
+ );
+ assert(output.includes("content-encoding: gzip\r\n"));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server doesn't compress body when no-transform is set",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(
+ JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
+ {
+ headers: {
+ "content-type": "application/json",
+ "cache-control": "no-transform",
+ },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(!output.includes("content-encoding: "));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server doesn't compress body when content-range is set",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const response = new Response(
+ JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
+ {
+ headers: {
+ "content-type": "application/json",
+ "content-range": "bytes 200-100/67589",
+ },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(!output.includes("content-encoding: "));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server compresses streamed bodies - check headers",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ const encoder = new TextEncoder();
+ const listener = Deno.listen({ hostname, port });
+
+ const data = { hello: "deno", now: "with", compressed: "body" };
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const bodyInit = new ReadableStream({
+ start(controller) {
+ controller.enqueue(encoder.encode(JSON.stringify(data)));
+ controller.close();
+ },
+ });
+ const response = new Response(
+ bodyInit,
+ { headers: { "content-type": "application/json" } },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "curl",
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(output.includes("content-encoding: gzip\r\n"));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server compresses streamed bodies - check body",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ const encoder = new TextEncoder();
+ const listener = Deno.listen({ hostname, port });
+
+ const data = { hello: "deno", now: "with", compressed: "body" };
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const bodyInit = new ReadableStream({
+ start(controller) {
+ controller.enqueue(encoder.encode(JSON.stringify(data)));
+ controller.close();
+ },
+ });
+ const response = new Response(
+ bodyInit,
+ { headers: { "content-type": "application/json" } },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const proc = new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).spawn();
+ const status = await proc.status;
+ assert(status.success);
+ const stdout = proc.stdout
+ .pipeThrough(new DecompressionStream("gzip"))
+ .pipeThrough(new TextDecoderStream());
+ let body = "";
+ for await (const chunk of stdout) {
+ body += chunk;
+ }
+ assertEquals(JSON.parse(body), data);
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server updates content-length header if compression is applied",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+ let contentLength: string;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const body = JSON.stringify({
+ hello: "deno",
+ now: "with",
+ compressed: "body",
+ });
+ contentLength = String(body.length);
+ const response = new Response(
+ body,
+ {
+ headers: {
+ "content-type": "application/json",
+ "content-length": contentLength,
+ },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ ];
+ const { success, stdout } = await new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).output();
+ assert(success);
+ const output = decoder.decode(stdout);
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(output.includes("content-encoding: gzip\r\n"));
+ // Ensure the content-length header is updated (but don't check the exact length).
+ assert(!output.includes(`content-length: ${contentLength}\r\n`));
+ assert(output.includes("content-length: "));
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server compresses when accept-encoding is deflate, gzip",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+ let contentLength: string;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "deflate, gzip");
+ const body = "x".repeat(10000);
+ contentLength = String(body.length);
+ const response = new Response(
+ body,
+ {
+ headers: {
+ "content-length": contentLength,
+ },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const cmd = [
+ "curl",
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ // "--compressed", // Windows curl does not support --compressed
+ "--header",
+ "Accept-Encoding: deflate, gzip",
+ ];
+ // deno-lint-ignore no-deprecated-deno-api
+ const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" });
+ const status = await proc.status();
+ assert(status.success);
+ const output = decoder.decode(await proc.output());
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(output.includes("content-encoding: gzip\r\n"));
+ // Ensure the content-length header is updated.
+ assert(!output.includes(`content-length: ${contentLength}\r\n`));
+ assert(output.includes("content-length: "));
+ proc.close();
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test({
+ name: "http server custom content-encoding is left untouched",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+ let contentLength: string;
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const listener = Deno.listen({ hostname, port });
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "deflate, gzip");
+ const body = new Uint8Array([3, 1, 4, 1]);
+ contentLength = String(body.length);
+ const response = new Response(
+ body,
+ {
+ headers: {
+ "content-length": contentLength,
+ "content-encoding": "arbitrary",
+ },
+ },
+ );
+ await respondWith(response);
+ listener.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const cmd = [
+ "curl",
+ "-i",
+ "--request",
+ "GET",
+ "--url",
+ url,
+ // "--compressed", // Windows curl does not support --compressed
+ "--header",
+ "Accept-Encoding: deflate, gzip",
+ ];
+ // deno-lint-ignore no-deprecated-deno-api
+ const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" });
+ const status = await proc.status();
+ assert(status.success);
+ const output = decoder.decode(await proc.output());
+ assert(output.includes("vary: Accept-Encoding\r\n"));
+ assert(output.includes("content-encoding: arbitrary\r\n"));
+ proc.close();
+ }
+
+ await Promise.all([server(), client()]);
+ httpConn!.close();
+ },
+});
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerReadLargeBodyWithContentLength() {
+ const TLS_PACKET_SIZE = 16 * 1024 + 256;
+ // We want the body to be read in multiple packets
+ const body = "aa\n" + "deno.land large body\n".repeat(TLS_PACKET_SIZE) +
+ "zz";
+
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ assertEquals(await request.text(), body);
+ await respondWith(new Response(body));
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
+ method: "POST",
+ headers: { "connection": "close" },
+ body,
+ });
+ const text = await resp.text();
+ assertEquals(text, body);
+ await promise;
+
+ httpConn!.close();
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerReadLargeBodyWithTransferChunked() {
+ const TLS_PACKET_SIZE = 16 * 1024 + 256;
+
+ // We want the body to be read in multiple packets
+ const chunks = [
+ "aa\n",
+ "deno.land large body\n".repeat(TLS_PACKET_SIZE),
+ "zz",
+ ];
+
+ const body = chunks.join("");
+
+ const stream = new TransformStream();
+ const writer = stream.writable.getWriter();
+ for (const chunk of chunks) {
+ writer.write(new TextEncoder().encode(chunk));
+ }
+ writer.close();
+
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ assertEquals(await request.text(), body);
+ await respondWith(new Response(body));
+ })();
+
+ const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
+ method: "POST",
+ headers: { "connection": "close" },
+ body: stream.readable,
+ });
+ const text = await resp.text();
+ assertEquals(text, body);
+ await promise;
+
+ httpConn!.close();
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true },
+ },
+ async function httpServerWithoutExclusiveAccessToTcp() {
+ const port = listenPort;
+ const listener = Deno.listen({ port });
+
+ const [clientConn, serverConn] = await Promise.all([
+ Deno.connect({ port }),
+ listener.accept(),
+ ]);
+
+ const buf = new Uint8Array(128);
+ const readPromise = serverConn.read(buf);
+ assertThrows(() => Deno.serveHttp(serverConn), Deno.errors.BadResource);
+
+ clientConn.close();
+ listener.close();
+ await readPromise;
+ },
+);
+
+Deno.test(
+ {
+ permissions: { net: true, read: true },
+ },
+ async function httpServerWithoutExclusiveAccessToTls() {
+ const hostname = "localhost";
+ const port = listenPort;
+ const listener = Deno.listenTls({
+ hostname,
+ port,
+ cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"),
+ key: await Deno.readTextFile("tests/testdata/tls/localhost.key"),
+ });
+
+ const caCerts = [
+ await Deno.readTextFile("tests/testdata/tls/RootCA.pem"),
+ ];
+ const [clientConn, serverConn] = await Promise.all([
+ Deno.connectTls({ hostname, port, caCerts }),
+ listener.accept(),
+ ]);
+ await Promise.all([clientConn.handshake(), serverConn.handshake()]);
+
+ const buf = new Uint8Array(128);
+ const readPromise = serverConn.read(buf);
+ assertThrows(() => Deno.serveHttp(serverConn), Deno.errors.BadResource);
+
+ clientConn.close();
+ listener.close();
+ await readPromise;
+ },
+);
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { read: true, write: true },
+ },
+ async function httpServerWithoutExclusiveAccessToUnixSocket() {
+ const filePath = tmpUnixSocketPath();
+ const listener = Deno.listen({ path: filePath, transport: "unix" });
+
+ const [clientConn, serverConn] = await Promise.all([
+ Deno.connect({ path: filePath, transport: "unix" }),
+ listener.accept(),
+ ]);
+
+ const buf = new Uint8Array(128);
+ const readPromise = serverConn.read(buf);
+ assertThrows(() => Deno.serveHttp(serverConn), Deno.errors.BadResource);
+
+ clientConn.close();
+ listener.close();
+ await readPromise;
+ },
+);
+
+Deno.test(
+ { permissions: { net: true } },
+ async function httpServerRequestResponseClone() {
+ const body = "deno".repeat(64 * 1024);
+ let httpConn: Deno.HttpConn;
+ const listener = Deno.listen({ port: listenPort });
+ const promise = (async () => {
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const reqEvent = await httpConn.nextRequest();
+ assert(reqEvent);
+ const { request, respondWith } = reqEvent;
+ const clone = request.clone();
+ const reader = clone.body!.getReader();
+
+ // get first chunk from branch2
+ const clonedChunks = [];
+ const { value, done } = await reader.read();
+ assert(!done);
+ clonedChunks.push(value);
+
+ // consume request after first chunk single read
+ // readAll should read correctly the rest of the body.
+ // firstChunk should be in the stream internal buffer
+ const body1 = await request.text();
+
+ while (true) {
+ const { value, done } = await reader.read();
+ if (done) break;
+ clonedChunks.push(value);
+ }
+ let offset = 0;
+ const body2 = new Uint8Array(body.length);
+ for (const chunk of clonedChunks) {
+ body2.set(chunk, offset);
+ offset += chunk.byteLength;
+ }
+
+ assertEquals(body1, body);
+ assertEquals(body1, new TextDecoder().decode(body2));
+ await respondWith(new Response(body));
+ })();
+
+ const response = await fetch(`http://localhost:${listenPort}`, {
+ body,
+ method: "POST",
+ });
+ const clone = response.clone();
+ assertEquals(await response.text(), await clone.text());
+
+ await promise;
+ httpConn!.close();
+ },
+);
+
+Deno.test({
+ name: "http server compresses and flushes each chunk of a streamed resource",
+ permissions: { net: true, run: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+ const port2 = listenPort2;
+
+ const encoder = new TextEncoder();
+ const listener = Deno.listen({ hostname, port });
+ const listener2 = Deno.listen({ hostname, port: port2 });
+
+ let httpConn: Deno.HttpConn;
+ async function server() {
+ const tcpConn = await listener.accept();
+ httpConn = Deno.serveHttp(tcpConn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { request, respondWith } = e;
+ assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
+ const resp = await fetch(`http://${hostname}:${port2}/`);
+ await respondWith(resp);
+ listener.close();
+ }
+
+ const ts = new TransformStream();
+ const writer = ts.writable.getWriter();
+ writer.write(encoder.encode("hello"));
+
+ let httpConn2: Deno.HttpConn;
+ async function server2() {
+ const tcpConn = await listener2.accept();
+ httpConn2 = Deno.serveHttp(tcpConn);
+ const e = await httpConn2.nextRequest();
+ assert(e);
+ await e.respondWith(
+ new Response(ts.readable, {
+ headers: { "Content-Type": "text/plain" },
+ }),
+ );
+ listener2.close();
+ }
+
+ async function client() {
+ const url = `http://${hostname}:${port}/`;
+ const args = [
+ "--request",
+ "GET",
+ "--url",
+ url,
+ "--header",
+ "Accept-Encoding: gzip, deflate, br",
+ "--no-buffer",
+ ];
+ const proc = new Deno.Command("curl", {
+ args,
+ stderr: "null",
+ stdout: "piped",
+ }).spawn();
+ const stdout = proc.stdout
+ .pipeThrough(new DecompressionStream("gzip"))
+ .pipeThrough(new TextDecoderStream());
+ let body = "";
+ for await (const chunk of stdout) {
+ body += chunk;
+ if (body === "hello") {
+ writer.write(encoder.encode(" world"));
+ writer.close();
+ }
+ }
+ assertEquals(body, "hello world");
+ const status = await proc.status;
+ assert(status.success);
+ }
+
+ await Promise.all([server(), server2(), client()]);
+ httpConn!.close();
+ httpConn2!.close();
+ },
+});
+
+Deno.test("case insensitive comma value finder", async (t) => {
+ const cases = /** @type {[string, boolean][]} */ ([
+ ["websocket", true],
+ ["wEbSOcKET", true],
+ [",wEbSOcKET", true],
+ [",wEbSOcKET,", true],
+ [", wEbSOcKET ,", true],
+ ["test, wEbSOcKET ,", true],
+ ["test ,\twEbSOcKET\t\t ,", true],
+ ["test , wEbSOcKET", true],
+ ["test, asdf,web,wEbSOcKET", true],
+ ["test, asdf,web,wEbSOcKETs", false],
+ ["test, asdf,awebsocket,wEbSOcKETs", false],
+ ]);
+
+ const findValue = buildCaseInsensitiveCommaValueFinder("websocket");
+ for (const [input, expected] of cases) {
+ await t.step(input.toString(), () => {
+ const actual = findValue(input);
+ assertEquals(actual, expected);
+ });
+ }
+});
+
+async function httpServerWithErrorBody(
+ listener: Deno.Listener,
+ compression: boolean,
+): Promise<Deno.HttpConn> {
+ const conn = await listener.accept();
+ listener.close();
+ const httpConn = Deno.serveHttp(conn);
+ const e = await httpConn.nextRequest();
+ assert(e);
+ const { respondWith } = e;
+ const originalErr = new Error("boom");
+ const rs = new ReadableStream({
+ async start(controller) {
+ controller.enqueue(new Uint8Array([65]));
+ await delay(1000);
+ controller.error(originalErr);
+ },
+ });
+ const init = compression ? { headers: { "content-type": "text/plain" } } : {};
+ const response = new Response(rs, init);
+ const err = await assertRejects(() => respondWith(response));
+ assert(err === originalErr);
+ return httpConn;
+}
+
+for (const compression of [true, false]) {
+ Deno.test({
+ name: `http server errors stream if response body errors (http/1.1${
+ compression ? " + compression" : ""
+ })`,
+ permissions: { net: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ const listener = Deno.listen({ hostname, port });
+ const server = httpServerWithErrorBody(listener, compression);
+
+ const conn = await Deno.connect({ hostname, port });
+ const msg = new TextEncoder().encode(
+ `GET / HTTP/1.1\r\nHost: ${hostname}:${port}\r\n\r\n`,
+ );
+ const nwritten = await conn.write(msg);
+ assertEquals(nwritten, msg.byteLength);
+
+ const buf = new Uint8Array(1024);
+ const nread = await conn.read(buf);
+ assert(nread);
+ const data = new TextDecoder().decode(buf.subarray(0, nread));
+ assert(data.endsWith("1\r\nA\r\n"));
+ const nread2 = await conn.read(buf); // connection should be closed now because the stream errored
+ assertEquals(nread2, null);
+ conn.close();
+
+ const httpConn = await server;
+ httpConn.close();
+ },
+ });
+
+ Deno.test({
+ name: `http server errors stream if response body errors (http/1.1 + fetch${
+ compression ? " + compression" : ""
+ })`,
+ permissions: { net: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ const listener = Deno.listen({ hostname, port });
+ const server = httpServerWithErrorBody(listener, compression);
+
+ const resp = await fetch(`http://${hostname}:${port}/`);
+ assert(resp.body);
+ const reader = resp.body.getReader();
+ const result = await reader.read();
+ assert(!result.done);
+ assertEquals(result.value, new Uint8Array([65]));
+ const err = await assertRejects(() => reader.read());
+ assert(err instanceof TypeError);
+ assert(err.message.includes("unexpected EOF"));
+
+ const httpConn = await server;
+ httpConn.close();
+ },
+ });
+
+ Deno.test({
+ name: `http server errors stream if response body errors (http/2 + fetch${
+ compression ? " + compression" : ""
+ }))`,
+ permissions: { net: true, read: true },
+ async fn() {
+ const hostname = "localhost";
+ const port = listenPort;
+
+ const listener = Deno.listenTls({
+ hostname,
+ port,
+ cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"),
+ key: await Deno.readTextFile("tests/testdata/tls/localhost.key"),
+ alpnProtocols: ["h2"],
+ });
+ const server = httpServerWithErrorBody(listener, compression);
+
+ const caCert = Deno.readTextFileSync("tests/testdata/tls/RootCA.pem");
+ const client = Deno.createHttpClient({ caCerts: [caCert] });
+ const resp = await fetch(`https://${hostname}:${port}/`, { client });
+ client.close();
+ assert(resp.body);
+ const reader = resp.body.getReader();
+ const result = await reader.read();
+ assert(!result.done);
+ assertEquals(result.value, new Uint8Array([65]));
+ const err = await assertRejects(() => reader.read());
+ assert(err instanceof TypeError);
+ assert(err.message.includes("unexpected internal error encountered"));
+
+ const httpConn = await server;
+ httpConn.close();
+ },
+ });
+}
+
+Deno.test({
+ name: "request signal is aborted when response errors",
+ permissions: { net: true },
+ async fn() {
+ let httpConn: Deno.HttpConn;
+ const promise = (async () => {
+ const listener = Deno.listen({ port: listenPort });
+ const conn = await listener.accept();
+ listener.close();
+ httpConn = Deno.serveHttp(conn);
+ const ev = await httpConn.nextRequest();
+ const { request, respondWith } = ev!;
+
+ await delay(300);
+ await assertRejects(() => respondWith(new Response("Hello World")));
+ assert(request.signal.aborted);
+ })();
+
+ const abortController = new AbortController();
+
+ fetch(`http://127.0.0.1:${listenPort}/`, {
+ signal: abortController.signal,
+ }).catch(() => {
+ // ignore
+ });
+
+ await delay(100);
+ abortController.abort();
+ await promise;
+ httpConn!.close();
+ },
+});
+
+Deno.test(
+ async function httpConnExplicitResourceManagement() {
+ let promise;
+
+ {
+ const listen = Deno.listen({ port: listenPort });
+ promise = fetch(`http://localhost:${listenPort}/`).catch(() => null);
+ const serverConn = await listen.accept();
+ listen.close();
+
+ using _httpConn = Deno.serveHttp(serverConn);
+ }
+
+ const response = await promise;
+ assertEquals(response, null);
+ },
+);
+
+function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader {
+ // Based on https://tools.ietf.org/html/rfc2616#section-19.4.6
+ const tp = new TextProtoReader(r);
+ let finished = false;
+ const chunks: Array<{
+ offset: number;
+ data: Uint8Array;
+ }> = [];
+ async function read(buf: Uint8Array): Promise<number | null> {
+ if (finished) return null;
+ const [chunk] = chunks;
+ if (chunk) {
+ const chunkRemaining = chunk.data.byteLength - chunk.offset;
+ const readLength = Math.min(chunkRemaining, buf.byteLength);
+ for (let i = 0; i < readLength; i++) {
+ buf[i] = chunk.data[chunk.offset + i];
+ }
+ chunk.offset += readLength;
+ if (chunk.offset === chunk.data.byteLength) {
+ chunks.shift();
+ // Consume \r\n;
+ if ((await tp.readLine()) === null) {
+ throw new Deno.errors.UnexpectedEof();
+ }
+ }
+ return readLength;
+ }
+ const line = await tp.readLine();
+ if (line === null) throw new Deno.errors.UnexpectedEof();
+ // TODO(bartlomieju): handle chunk extension
+ const [chunkSizeString] = line.split(";");
+ const chunkSize = parseInt(chunkSizeString, 16);
+ if (Number.isNaN(chunkSize) || chunkSize < 0) {
+ throw new Deno.errors.InvalidData("Invalid chunk size");
+ }
+ if (chunkSize > 0) {
+ if (chunkSize > buf.byteLength) {
+ let eof = await r.readFull(buf);
+ if (eof === null) {
+ throw new Deno.errors.UnexpectedEof();
+ }
+ const restChunk = new Uint8Array(chunkSize - buf.byteLength);
+ eof = await r.readFull(restChunk);
+ if (eof === null) {
+ throw new Deno.errors.UnexpectedEof();
+ } else {
+ chunks.push({
+ offset: 0,
+ data: restChunk,
+ });
+ }
+ return buf.byteLength;
+ } else {
+ const bufToFill = buf.subarray(0, chunkSize);
+ const eof = await r.readFull(bufToFill);
+ if (eof === null) {
+ throw new Deno.errors.UnexpectedEof();
+ }
+ // Consume \r\n
+ if ((await tp.readLine()) === null) {
+ throw new Deno.errors.UnexpectedEof();
+ }
+ return chunkSize;
+ }
+ } else {
+ assert(chunkSize === 0);
+ // Consume \r\n
+ if ((await r.readLine()) === null) {
+ throw new Deno.errors.UnexpectedEof();
+ }
+ await readTrailers(h, r);
+ finished = true;
+ return null;
+ }
+ }
+ return { read };
+}
+
+async function readTrailers(
+ headers: Headers,
+ r: BufReader,
+) {
+ const trailers = parseTrailer(headers.get("trailer"));
+ if (trailers == null) return;
+ const trailerNames = [...trailers.keys()];
+ const tp = new TextProtoReader(r);
+ const result = await tp.readMimeHeader();
+ if (result == null) {
+ throw new Deno.errors.InvalidData("Missing trailer header.");
+ }
+ const undeclared = [...result.keys()].filter(
+ (k) => !trailerNames.includes(k),
+ );
+ if (undeclared.length > 0) {
+ throw new Deno.errors.InvalidData(
+ `Undeclared trailers: ${Deno.inspect(undeclared)}.`,
+ );
+ }
+ for (const [k, v] of result) {
+ headers.append(k, v);
+ }
+ const missingTrailers = trailerNames.filter((k) => !result.has(k));
+ if (missingTrailers.length > 0) {
+ throw new Deno.errors.InvalidData(
+ `Missing trailers: ${Deno.inspect(missingTrailers)}.`,
+ );
+ }
+ headers.delete("trailer");
+}
+
+function parseTrailer(field: string | null): Headers | undefined {
+ if (field == null) {
+ return undefined;
+ }
+ const trailerNames = field.split(",").map((v) => v.trim().toLowerCase());
+ if (trailerNames.length === 0) {
+ throw new Deno.errors.InvalidData("Empty trailer header.");
+ }
+ const prohibited = trailerNames.filter((k) => isProhibitedForTrailer(k));
+ if (prohibited.length > 0) {
+ throw new Deno.errors.InvalidData(
+ `Prohibited trailer names: ${Deno.inspect(prohibited)}.`,
+ );
+ }
+ return new Headers(trailerNames.map((key) => [key, ""]));
+}
+
+function isProhibitedForTrailer(key: string): boolean {
+ const s = new Set(["transfer-encoding", "content-length", "trailer"]);
+ return s.has(key.toLowerCase());
+}