// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

// deno-lint-ignore-file no-deprecated-deno-api

import { Buffer, BufReader, BufWriter } from "@std/io";
import { TextProtoReader } from "../testdata/run/textproto.ts";
import {
  assert,
  assertEquals,
  assertRejects,
  assertStrictEquals,
  assertThrows,
  delay,
  fail,
} from "./test_util.ts";
import { join } from "@std/path";

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.body?.cancel();
    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.bytes();
    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.bytes();
    assertEquals(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",
      ];
      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",
      ];
      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]));
      await assertRejects(
        () => reader.read(),
        TypeError,
        "body",
      );

      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]));
      await assertRejects(
        () => reader.read(),
        TypeError,
        "body",
      );

      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);
  },
);

Deno.test("proxy with fetch", async () => {
  const listener = Deno.listen({ port: listenPort });
  const deferred = Promise.withResolvers<void>();

  const server = Deno.serve({ port: listenPort + 1 }, (_req) => {
    return new Response("Hello world");
  });

  let httpConn: Deno.HttpConn;
  async function handleHttp(conn: Deno.Conn) {
    httpConn = Deno.serveHttp(conn);
    for await (const e of httpConn) {
      await e.respondWith(serve(e.request));
      break;
    }
  }

  async function serve(req: Request) {
    return await fetch(`http://localhost:${listenPort + 1}/`, req);
  }

  const originServer = (async () => {
    for await (const conn of listener) {
      handleHttp(conn);
      break;
    }
  })();

  const proxiedRequest = (async () => {
    const conn = await Deno.connect({ port: listenPort });
    const payload = new TextEncoder().encode(
      "POST /api/sessions HTTP/1.1\x0d\x0aConnection: keep-alive\x0d\x0aContent-Length: 2\x0d\x0a\x0d\x0a{}",
    );
    const n = await conn.write(payload);
    assertEquals(n, 76);
    const buf = new Uint8Array(1000);
    const nread = await conn.read(buf);
    assertEquals(nread, 150);
    const respText = new TextDecoder().decode(buf);
    assert(respText.includes("HTTP/1.1 200 OK"));
    assert(respText.includes("content-type: text/plain;charset=UTF-8"));
    assert(respText.includes("vary: Accept-Encoding"));
    assert(respText.includes("content-length: 11"));
    assert(respText.includes("Hello world"));
    conn.close();
    deferred.resolve();
  })();
  await proxiedRequest;
  await originServer;
  await deferred.promise;
  await server.shutdown();
  await server.finished;
  httpConn!.close();
});

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());
}