// Copyright 2018-2026 the Deno authors. MIT license.

// deno-lint-ignore-file no-console

import { EventEmitter, once } from "node:events";
import {
  AsyncLocalStorage,
  createHook,
  executionAsyncResource,
} from "node:async_hooks";
import http, {
  IncomingMessage,
  type RequestOptions,
  ServerResponse,
} from "node:http";
import url from "node:url";
import https from "node:https";
import zlib from "node:zlib";
import net, { type AddressInfo, Socket } from "node:net";
import fs from "node:fs";
import process from "node:process";
import type { Duplex } from "node:stream";
import { text } from "node:stream/consumers";
import { channel } from "node:diagnostics_channel";
import * as v8 from "node:v8";
import { runInNewContext } from "node:vm";

import { assert, assertEquals, assertStringIncludes, fail } from "@std/assert";
import { assertSpyCalls, spy } from "@std/testing/mock";
import { fromFileUrl, relative } from "@std/path";
import { retry } from "@std/async/retry";

import { gzip } from "node:zlib";
import { Buffer } from "node:buffer";
import { setImmediate } from "node:timers";
import { execCode } from "../unit/test_util.ts";

// Destroy idle keep-alive sockets before each test so that sequential
// tests reusing the same port (e.g. 4505) don't fail with EADDRINUSE.
Deno.test.beforeEach(() => {
  http.globalAgent.destroy();
});

Deno.test("[node/http] HTTPParser.consume keeps stream handle alive", async () => {
  v8.setFlagsFromString("--expose_gc");
  const gc = runInNewContext("gc") as () => void;

  // @ts-ignore: untyped internal binding for direct lifecycle coverage.
  const { HTTPParser } = process.binding("http_parser");
  // @ts-ignore: untyped internal binding for direct lifecycle coverage.
  const { TCP } = process.binding("tcp_wrap");

  const parser = new HTTPParser();
  parser.initialize(HTTPParser.REQUEST, {});

  let weak: WeakRef<object> | undefined;
  {
    const tcp = new TCP(0);
    weak = new WeakRef(tcp);
    parser.consume(tcp);
    tcp.close();
  }

  await new Promise((resolve) => setTimeout(resolve, 10));
  for (let i = 0; i < 5; i++) {
    gc();
  }

  assert(weak?.deref() !== undefined);
  parser.unconsume();
});

Deno.test("[node/http listen]", async () => {
  {
    const server = http.createServer();
    assertEquals(0, EventEmitter.listenerCount(server, "request"));
  }

  {
    const server = http.createServer(() => {});
    assertEquals(1, EventEmitter.listenerCount(server, "request"));
  }

  {
    const { promise, resolve } = Promise.withResolvers<void>();
    const server = http.createServer();

    server.listen(42453, "localhost", () => {
      // @ts-ignore address() is not a string
      assertEquals(server.address()!.address, "127.0.0.1");
      server.close();
    });
    server.on("close", () => {
      resolve();
    });

    await promise;
  }

  {
    const { promise, resolve } = Promise.withResolvers<void>();
    const server = http.createServer();

    server.listen().on("listening", () => {
      server.close();
    });
    server.on("close", () => {
      resolve();
    });

    await promise;
  }

  for (const port of [0, -0, 0.0, "0", null, undefined]) {
    const { promise, resolve } = Promise.withResolvers<void>();
    const server = http.createServer();

    server.listen(port, () => {
      server.close();
    });
    server.on("close", () => {
      resolve();
    });

    await promise;
  }
});

Deno.test("[node/http close]", async () => {
  {
    const deferred1 = Promise.withResolvers<void>();
    const deferred2 = Promise.withResolvers<void>();
    // Node quirk: callback gets exception object, event listener does not.
    // deno-lint-ignore no-explicit-any
    const server = http.createServer().close((err: any) => {
      assertEquals(err.code, "ERR_SERVER_NOT_RUNNING");
      deferred1.resolve();
    });
    // deno-lint-ignore no-explicit-any
    server.on("close", (err: any) => {
      assertEquals(err, undefined);
      deferred2.resolve();
    });
    server.on("listening", () => {
      throw Error("unreachable");
    });
    await deferred1.promise;
    await deferred2.promise;
  }

  {
    const deferred1 = Promise.withResolvers<void>();
    const deferred2 = Promise.withResolvers<void>();
    const server = http.createServer().listen().close((err) => {
      assertEquals(err, undefined);
      deferred1.resolve();
    });
    // deno-lint-ignore no-explicit-any
    server.on("close", (err: any) => {
      assertEquals(err, undefined);
      deferred2.resolve();
    });
    server.on("listening", () => {
      throw Error("unreachable");
    });
    await deferred1.promise;
    await deferred2.promise;
  }
});

Deno.test("[node/http] chunked response", async () => {
  for (
    const body of [undefined, "", "ok"]
  ) {
    const expected = body ?? "";
    const { promise, resolve } = Promise.withResolvers<void>();

    const server = http.createServer((_req, res) => {
      res.writeHead(200, { "transfer-encoding": "chunked" });
      res.end(body);
    });

    server.listen(async () => {
      const res = await fetch(
        // deno-lint-ignore no-explicit-any
        `http://127.0.0.1:${(server.address() as any).port}/`,
      );
      assert(res.ok);

      const actual = await res.text();
      assertEquals(actual, expected);

      server.close(() => resolve());
    });

    await promise;
  }
});

Deno.test("[node/http] .writeHead()", async (t) => {
  async function testWriteHead(
    onRequest: (res: ServerResponse) => void,
    onResponse: (res: Response) => void,
  ) {
    const { promise, resolve } = Promise.withResolvers<void>();
    const server = http.createServer((_req, res) => {
      onRequest(res);
      res.end();
    });
    server.listen(async () => {
      const res = await fetch(
        // deno-lint-ignore no-explicit-any
        `http://127.0.0.1:${(server.address() as any).port}/`,
      );
      await res.body?.cancel();

      onResponse(res);

      server.close(() => resolve());
    });

    await promise;
  }

  await t.step("send status code", async () => {
    await testWriteHead(
      (res) => res.writeHead(404),
      (res) => {
        assertEquals(res.status, 404);
      },
    );
  });

  // TODO(@marvinhagemeister): hyper doesn't support custom status text
  // await t.step("send status + custom status text", async () => {
  //   await testWriteHead(
  //     (res) => res.writeHead(404, "some text"),
  //     (res) => {
  //       assertEquals(res.status, 404);
  //       assertEquals(res.statusText, "some text");
  //     },
  //   );
  // });

  await t.step("send status + custom status text + headers obj", async () => {
    await testWriteHead(
      (res) => res.writeHead(404, "some text", { foo: "bar" }),
      (res) => {
        assertEquals(res.status, 404);
        // TODO(@marvinhagemeister): hyper doesn't support custom
        // status text
        // assertEquals(res.statusText, "some text");
        assertEquals(res.headers.get("foo"), "bar");
      },
    );
  });

  await t.step("send status + headers obj", async () => {
    await testWriteHead(
      (res) => {
        res.writeHead(200, {
          foo: "bar",
          bar: ["foo1", "foo2"],
          foobar: 1,
        });
      },
      (res) => {
        assertEquals(res.status, 200);
        assertEquals(res.headers.get("foo"), "bar");
        assertEquals(res.headers.get("bar"), "foo1, foo2");
        assertEquals(res.headers.get("foobar"), "1");
      },
    );
  });

  await t.step("send status + headers nested array", async () => {
    await testWriteHead(
      (res) => res.writeHead(200, [["foo", "bar"]]),
      (res) => {
        assertEquals(res.status, 200);
        assertEquals(res.headers.get("foo"), "bar");
      },
    );
  });

  await t.step("send status + headers array", async () => {
    await testWriteHead(
      (res) => res.writeHead(200, ["foo", "bar"]),
      (res) => {
        assertEquals(res.status, 200);
        assertEquals(res.headers.get("foo"), "bar");
      },
    );
  });
});

// Test empty chunks: https://github.com/denoland/deno/issues/17194
Deno.test("[node/http] empty chunk in the middle of response", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();

  const server = http.createServer((_req, res) => {
    res.write("a");
    res.write("");
    res.write("b");
    res.end();
  });

  server.listen(async () => {
    const res = await fetch(
      // deno-lint-ignore no-explicit-any
      `http://127.0.0.1:${(server.address() as any).port}/`,
    );
    const actual = await res.text();
    assertEquals(actual, "ab");
    server.close(() => resolve());
  });

  await promise;
});

Deno.test("[node/http] server can respond with 101, 204, 205, 304 status", async () => {
  for (const status of [101, 204, 205, 304]) {
    const { promise, resolve } = Promise.withResolvers<void>();
    const server = http.createServer((_req, res) => {
      res.statusCode = status;
      res.end("");
    });
    server.listen(async () => {
      const res = await fetch(
        // deno-lint-ignore no-explicit-any
        `http://127.0.0.1:${(server.address() as any).port}/`,
      );
      await res.body?.cancel();
      assertEquals(res.status, status);
      server.close(() => resolve());
    });
    await promise;
  }
});

Deno.test("[node/http] multiple set-cookie headers", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();

  const server = http.createServer((_req, res) => {
    res.setHeader("Set-Cookie", ["foo=bar", "bar=foo"]);
    assertEquals(res.getHeader("Set-Cookie"), ["foo=bar", "bar=foo"]);
    res.end();
  });

  server.listen(async () => {
    const res = await fetch(
      // deno-lint-ignore no-explicit-any
      `http://127.0.0.1:${(server.address() as any).port}/`,
    );
    assert(res.ok);

    const setCookieHeaders = res.headers.getSetCookie();
    assertEquals(setCookieHeaders, ["foo=bar", "bar=foo"]);

    await res.body!.cancel();

    server.close(() => resolve());
  });

  await promise;
});

Deno.test("[node/http] ServerResponse.setHeaders preserves Headers set-cookie entries", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();

  const server = http.createServer((_req, res) => {
    res.setHeaders(
      new Headers([
        ["Set-Cookie", "foo=bar"],
        ["Set-Cookie", "bar=foo"],
      ]),
    );
    res.end();
  });

  server.listen(async () => {
    const res = await fetch(
      // deno-lint-ignore no-explicit-any
      `http://127.0.0.1:${(server.address() as any).port}/`,
    );
    assert(res.ok);
    assertEquals(res.headers.getSetCookie(), ["foo=bar", "bar=foo"]);

    await res.body!.cancel();
    server.close(() => resolve());
  });

  await promise;
});

Deno.test("[node/http] IncomingRequest socket has remoteAddress + remotePort", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();

  let remoteAddress: string | undefined;
  let remotePort: number | undefined;
  const server = http.createServer((req, res) => {
    remoteAddress = req.socket.remoteAddress;
    remotePort = req.socket.remotePort;
    res.end();
  });
  server.listen(async () => {
    // deno-lint-ignore no-explicit-any
    const port = (server.address() as any).port;
    const res = await fetch(
      `http://127.0.0.1:${port}/`,
    );
    await res.arrayBuffer();
    // Default-host listen() binds dual-stack, so IPv4 connections arrive
    // as IPv4-mapped IPv6 addresses. Accept either form.
    assert(
      remoteAddress === "127.0.0.1" ||
        remoteAddress === "::ffff:127.0.0.1",
      `unexpected remoteAddress: ${remoteAddress}`,
    );
    assertEquals(typeof remotePort, "number");
    server.close(() => resolve());
  });
  await promise;
});

Deno.test("[node/http] request default protocol", async () => {
  const deferred1 = Promise.withResolvers<void>();
  const deferred2 = Promise.withResolvers<void>();
  const server = http.createServer((_, res) => {
    res.end("ok");
  });

  // @ts-ignore IncomingMessageForClient
  // deno-lint-ignore no-explicit-any
  let clientRes: any;
  // deno-lint-ignore no-explicit-any
  let clientReq: any;
  server.listen(() => {
    clientReq = http.request(
      // deno-lint-ignore no-explicit-any
      { host: "localhost", port: (server.address() as any).port },
      (res) => {
        assert(res.socket instanceof EventEmitter);
        assertEquals(res.complete, false);
        res.on("data", () => {});
        res.on("end", () => {
          server.close();
        });
        clientRes = res;
        assertEquals(res.statusCode, 200);
        deferred2.resolve();
      },
    );
    clientReq.end();
  });
  server.on("close", () => {
    deferred1.resolve();
  });
  await deferred1.promise;
  await deferred2.promise;
  assert(clientReq.socket instanceof EventEmitter);
  assertEquals(clientRes!.complete, true);
});

Deno.test("[node/http] request non-ws upgrade header", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((_req, res) => {
    res.writeHead(200, { "upgrade": "h2,h2c" });
    res.end("ok");
  });
  server.listen(() => {
    const req = http.request(
      {
        host: "localhost",
        // deno-lint-ignore no-explicit-any
        port: (server.address() as any).port,
      },
      (res) => {
        res.on("data", () => {});
        res.on("end", () => {
          server.close();
        });
        assertEquals(res.statusCode, 200);
      },
    );
    req.end();
  });
  server.on("close", () => {
    resolve();
  });
  await promise;
});

Deno.test("[node/http] request with headers", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((req, res) => {
    assertEquals(req.headers["x-foo"], "bar");
    res.end("ok");
  });
  server.listen(() => {
    const req = http.request(
      {
        host: "localhost",
        // deno-lint-ignore no-explicit-any
        port: (server.address() as any).port,
        headers: { "x-foo": "bar" },
      },
      (res) => {
        res.on("data", () => {});
        res.on("end", () => {
          server.close();
        });
        assertEquals(res.statusCode, 200);
      },
    );
    req.end();
  });
  server.on("close", () => {
    resolve();
  });
  await promise;
});

Deno.test("[node/http] request with ipv6 host", async () => {
  const server = http.createServer((_req, res) => res.end()).listen(0, "::1");
  await once(server, "listening");
  const { port } = server.address() as AddressInfo;
  const req = http.request(`http://[::1]:${port}`).end();
  const [res] = await once(req, "response") as [IncomingMessage];
  assertEquals(res.statusCode, 200);
  res.resume();
  await once(res, "end");
  server.close();
  await once(server, "close");
});

Deno.test("[node/http] non-string buffer response", {
  // TODO(kt3k): Enable sanitizer. A "zlib" resource is leaked in this test case.
  sanitizeResources: false,
}, async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((_, res) => {
    gzip(
      Buffer.from("a".repeat(100), "utf8"),
      {},
      (_err: Error | null, data: Buffer) => {
        res.setHeader("Content-Encoding", "gzip");
        res.end(data);
      },
    );
  });
  server.listen(async () => {
    const res = await fetch(
      // deno-lint-ignore no-explicit-any
      `http://localhost:${(server.address() as any).port}`,
    );
    try {
      const text = await res.text();
      assertEquals(text, "a".repeat(100));
    } catch (e) {
      server.emit("error", e);
    } finally {
      server.close(() => resolve());
    }
  });
  await promise;
});

// TODO(kt3k): Enable this test
// Currently IncomingMessage constructor has incompatible signature.
/*
Deno.test("[node/http] http.IncomingMessage can be created without url", () => {
  const message = new http.IncomingMessage(
    // adapted from https://github.com/dougmoscrop/serverless-http/blob/80bfb3e940057d694874a8b0bc12ad96d2abe7ab/lib/request.js#L7
    {
      // @ts-expect-error - non-request properties will also be passed in, e.g. by serverless-http
      encrypted: true,
      readable: false,
      remoteAddress: "foo",
      address: () => ({ port: 443 }),
      // deno-lint-ignore no-explicit-any
      end: Function.prototype as any,
      // deno-lint-ignore no-explicit-any
      destroy: Function.prototype as any,
    },
  );
  message.url = "https://example.com";
});
*/

Deno.test("[node/http] send request with non-chunked body", async () => {
  let requestHeaders: Headers;
  let requestBody = "";

  const hostname = "localhost";

  const handler = async (req: Request) => {
    requestHeaders = req.headers;
    requestBody = await req.text();
    return new Response("ok");
  };
  const abortController = new AbortController();
  const { promise: portPromise, resolve: portResolve } = Promise.withResolvers<
    number
  >();
  const servePromise = Deno.serve({
    hostname,
    port: 0,
    signal: abortController.signal,
    onListen: ({ port }) => portResolve(port),
  }, handler).finished;

  const port = await portPromise;
  const opts: RequestOptions = {
    host: hostname,
    port,
    method: "POST",
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
      "Content-Length": "11",
    },
  };
  const req = http.request(opts, (res) => {
    res.on("data", () => {});
    res.on("end", () => {
      abortController.abort();
    });
    assertEquals(res.statusCode, 200);
    assertEquals(requestHeaders.get("content-length"), "11");
    assertEquals(requestHeaders.has("transfer-encoding"), false);
    assertEquals(requestBody, "hello world");
  });
  req.on("socket", (socket) => {
    assert(socket.writable);
    assert(socket.readable);
    socket.setKeepAlive();
    socket.setTimeout(100);
  });
  req.write("hello ");
  req.write("world");
  req.end();

  await Promise.all([
    servePromise,
    // wait 100ms because of the socket.setTimeout(100) above
    // in order to not cause a flaky test sanitizer failure
    await new Promise((resolve) => setTimeout(resolve, 100)),
  ]);

  if (Deno.build.os === "windows") {
    // FIXME(kt3k): This is necessary for preventing op leak on windows
    await new Promise((resolve) => setTimeout(resolve, 4000));
  }
});

Deno.test(
  "[node/http] send request with chunked body",
  async () => {
    let requestHeaders: Headers;
    let requestBody = "";

    const hostname = "localhost";

    const handler = async (req: Request) => {
      requestHeaders = req.headers;
      requestBody = await req.text();
      return new Response("ok");
    };
    const abortController = new AbortController();
    const { promise: portPromise, resolve: portResolve } = Promise
      .withResolvers<
        number
      >();
    const servePromise = Deno.serve({
      hostname,
      port: 0,
      signal: abortController.signal,
      onListen: ({ port }) => portResolve(port),
    }, handler).finished;

    const port = await portPromise;
    const opts: RequestOptions = {
      host: hostname,
      port,
      method: "POST",
      headers: {
        "Content-Type": "text/plain; charset=utf-8",
        "Transfer-Encoding": "chunked",
      },
    };
    const req = http.request(opts, (res) => {
      res.on("data", () => {});
      res.on("end", () => {
        abortController.abort();
      });
      assertEquals(res.statusCode, 200);
      assertEquals(requestHeaders.has("content-length"), false);
      assertEquals(requestHeaders.get("transfer-encoding"), "chunked");
      assertEquals(requestBody, "hello world");
    });
    console.log("Request is writable:", req.writable);
    req.write("hello ");
    req.write("world");
    req.end();
    console.log("Request ended.");

    await servePromise;
    console.log("Server finished.");
    if (Deno.build.os === "windows") {
      // FIXME(kt3k): This is necessary for preventing op leak on windows
      await new Promise((resolve) => setTimeout(resolve, 4000));
    }
  },
);

Deno.test("[node/http] send request with chunked body as default", async () => {
  let requestHeaders: Headers;
  let requestBody = "";

  const hostname = "localhost";

  const handler = async (req: Request) => {
    requestHeaders = req.headers;
    requestBody = await req.text();
    return new Response("ok");
  };
  const abortController = new AbortController();
  const { promise: portPromise, resolve: portResolve } = Promise.withResolvers<
    number
  >();
  const servePromise = Deno.serve({
    hostname,
    port: 0,
    signal: abortController.signal,
    onListen: ({ port }) => portResolve(port),
  }, handler).finished;

  const port = await portPromise;
  const opts: RequestOptions = {
    host: hostname,
    port,
    method: "POST",
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
    },
  };
  const req = http.request(opts, (res) => {
    res.on("data", () => {});
    res.on("end", () => {
      abortController.abort();
    });
    assertEquals(res.statusCode, 200);
    assertEquals(requestHeaders.has("content-length"), false);
    assertEquals(requestHeaders.get("transfer-encoding"), "chunked");
    assertEquals(requestBody, "hello world");
  });
  req.write("hello ");
  req.write("world");
  req.end();

  await servePromise;

  if (Deno.build.os === "windows") {
    // FIXME(kt3k): This is necessary for preventing op leak on windows
    await new Promise((resolve) => setTimeout(resolve, 4000));
  }
});

Deno.test("[node/http] ServerResponse _implicitHeader", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((_req, res) => {
    const writeHeadSpy = spy(res, "writeHead");
    // deno-lint-ignore no-explicit-any
    (res as any)._implicitHeader();
    assertSpyCalls(writeHeadSpy, 1);
    writeHeadSpy.restore();
    res.end("Hello World");
  });

  server.listen(async () => {
    const { port } = server.address() as { port: number };
    const res = await fetch(`http://localhost:${port}`);
    assertEquals(await res.text(), "Hello World");
    server.close(() => {
      resolve();
    });
  });

  await promise;
});

async function getRawServerResponse(
  handler: http.RequestListener,
): Promise<string> {
  const server = http.createServer(handler);
  const response = Promise.withResolvers<string>();
  let rawResponse = "";

  server.listen(0, "127.0.0.1", () => {
    const { port } = server.address() as AddressInfo;
    const client = net.createConnection(port, "127.0.0.1", () => {
      client.write(
        "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
      );
    });
    client.setEncoding("utf8");
    client.on("data", (chunk) => {
      rawResponse += chunk;
    });
    client.on("end", () => response.resolve(rawResponse));
    client.on("error", response.reject);
  });

  try {
    return await response.promise;
  } finally {
    await new Promise<void>((resolve) => server.close(() => resolve()));
  }
}

Deno.test("[node/http] ServerResponse direct end sets content-length", async () => {
  const rawResponse = await getRawServerResponse((_req, res) => {
    res.end("hi");
  });

  assertStringIncludes(rawResponse, "HTTP/1.1 200 OK\r\n");
  assertStringIncludes(rawResponse, "Content-Length: 2\r\n");
  assert(!rawResponse.includes("Transfer-Encoding: chunked\r\n"));
  assert(rawResponse.endsWith("\r\n\r\nhi"));
});

Deno.test("[node/http] ServerResponse empty end sets content-length", async () => {
  const rawResponse = await getRawServerResponse((_req, res) => {
    res.end();
  });

  assertStringIncludes(rawResponse, "HTTP/1.1 200 OK\r\n");
  assertStringIncludes(rawResponse, "Content-Length: 0\r\n");
  assert(!rawResponse.includes("Transfer-Encoding: chunked\r\n"));
  assert(rawResponse.endsWith("\r\n\r\n"));
});

Deno.test(
  "[node/http] ServerResponse empty end respects pre-generated content-length",
  async () => {
    const rawResponse = await getRawServerResponse((_req, res) => {
      res.writeHead(200, { "Content-Length": "0" });
      res.end();
    });

    assertStringIncludes(rawResponse, "HTTP/1.1 200 OK\r\n");
    assertStringIncludes(rawResponse, "Content-Length: 0\r\n");
    assert(!rawResponse.includes("Transfer-Encoding: chunked\r\n"));
    assert(rawResponse.endsWith("\r\n\r\n"));
  },
);

Deno.test(
  "[node/http] ServerResponse direct end respects explicit chunked transfer-encoding",
  async () => {
    const rawResponse = await getRawServerResponse((_req, res) => {
      res.setHeader("Transfer-Encoding", "chunked");
      res.end("hi");
    });

    assertStringIncludes(rawResponse, "HTTP/1.1 200 OK\r\n");
    assertStringIncludes(rawResponse, "Transfer-Encoding: chunked\r\n");
    assert(!rawResponse.includes("Content-Length:"));
    assert(rawResponse.endsWith("\r\n\r\n2\r\nhi\r\n0\r\n\r\n"));
  },
);

// https://github.com/denoland/deno/issues/34002
Deno.test(
  "[node/http] ServerResponse does not emit 'finish' after client abort",
  async () => {
    const { promise, resolve } = Promise.withResolvers<void>();
    let client: http.ClientRequest;
    let finishEmitted = false;
    let closeEmitted = false;

    const server = http.createServer((_req, res) => {
      res.on("finish", () => {
        finishEmitted = true;
      });
      res.on("close", () => {
        closeEmitted = true;
      });

      client.abort();

      setImmediate(() => {
        setImmediate(() => {
          res.end("ok");
          setImmediate(() => {
            server.close(() => {
              assertEquals(finishEmitted, false);
              assertEquals(closeEmitted, true);
              resolve();
            });
          });
        });
      });
    });

    server.listen(0, () => {
      const { port } = server.address() as { port: number };
      client = http.get(`http://127.0.0.1:${port}`);
      client.on("error", () => {});
    });

    await promise;
  },
);

// https://github.com/denoland/deno/issues/21509
Deno.test("[node/http] ServerResponse flushHeaders", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((_req, res) => {
    res.flushHeaders(); // no-op
    res.end("Hello World");
  });

  server.listen(async () => {
    const { port } = server.address() as { port: number };
    const res = await fetch(`http://localhost:${port}`);
    assertEquals(await res.text(), "Hello World");
    server.close(() => {
      resolve();
    });
  });

  await promise;
});

Deno.test("[node/http] server unref", async () => {
  const [statusCode, _output] = await execCode(`
  import http from "node:http";
  const server = http.createServer((_req, res) => {
    res.statusCode = status;
    res.end("");
  });

  // This should let the program to exit without waiting for the
  // server to close.
  server.unref();

  server.listen(async () => {
  });
  `);
  assertEquals(statusCode, 0);
});

Deno.test("[node/http] ClientRequest handle non-string headers", async () => {
  // deno-lint-ignore no-explicit-any
  let headers: any;
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const req = http.request("http://localhost:4545/echo_server", {
    method: "POST",
    headers: { 1: 2 },
  }, (resp) => {
    headers = resp.headers;

    resp.on("data", () => {});

    resp.on("end", () => {
      resolve();
    });
  });
  req.once("error", (e) => reject(e));
  req.end();
  await promise;
  assertEquals(headers!["1"], "2");
});

Deno.test({
  name: "[node/https] ClientRequest uses HTTP/1.1",
  async fn() {
    let body = "";
    const { promise, resolve, reject } = Promise.withResolvers<void>();
    const req = https.request("https://localhost:5545/http_version", {
      method: "POST",
      headers: { 1: 2 },
    }, (resp) => {
      resp.on("data", (chunk) => {
        body += chunk;
      });

      resp.on("end", () => {
        resolve();
      });
    });
    req.once("error", (e) => reject(e));
    req.end();
    await promise;
    assertEquals(body, "HTTP/1.1");
  },
});

Deno.test("[node/http] ClientRequest setTimeout", async () => {
  let body = "";
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const timer = setTimeout(() => reject("timed out"), 50000);
  const req = http.request("http://localhost:4545/http_version", (resp) => {
    resp.on("data", (chunk) => {
      body += chunk;
    });

    resp.on("end", () => {
      resolve();
    });
  });
  req.setTimeout(120000);
  req.once("error", (e) => reject(e));
  req.end();
  await promise;
  clearTimeout(timer);
  assertEquals(body, "HTTP/1.1");
});

Deno.test("[node/http] ClientRequest setNoDelay", async () => {
  let body = "";
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const timer = setTimeout(() => reject("timed out"), 50000);
  const req = http.request("http://localhost:4545/http_version", (resp) => {
    resp.on("data", (chunk) => {
      body += chunk;
    });

    resp.on("end", () => {
      resolve();
    });
  });
  req.setNoDelay(true);
  req.once("error", (e) => reject(e));
  req.end();
  await promise;
  clearTimeout(timer);
  assertEquals(body, "HTTP/1.1");
});

Deno.test("[node/http] ClientRequest PATCH", async () => {
  let body = "";
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const req = http.request("http://localhost:4545/echo_server", {
    method: "PATCH",
  }, (resp) => {
    resp.on("data", (chunk) => {
      body += chunk;
    });

    resp.on("end", () => {
      resolve();
    });
  });
  req.write("hello ");
  req.write("world");
  req.once("error", (e) => reject(e));
  req.end();
  await promise;
  assertEquals(body, "hello world");
});

Deno.test("[node/http] ClientRequest PUT", async () => {
  let body = "";
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const req = http.request("http://localhost:4545/echo_server", {
    method: "PUT",
  }, (resp) => {
    resp.on("data", (chunk) => {
      body += chunk;
    });

    resp.on("end", () => {
      resolve();
    });
  });
  req.write("hello ");
  req.write("world");
  req.once("error", (e) => reject(e));
  req.end();
  await promise;
  assertEquals(body, "hello world");
});

Deno.test("[node/http] ClientRequest search params", async () => {
  let body = "";
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const req = http.request({
    host: "localhost",
    port: 4545,
    path: "/search_params?foo=bar",
  }, (resp) => {
    resp.on("data", (chunk) => {
      body += chunk;
    });

    resp.on("end", () => {
      resolve();
    });
  });
  req.once("error", (e) => reject(e));
  req.end();
  await promise;
  assertEquals(body, "foo=bar");
});

Deno.test("[node/http] HTTPS server", async () => {
  const deferred = Promise.withResolvers<void>();
  const deferred2 = Promise.withResolvers<void>();
  const client = Deno.createHttpClient({
    caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")],
  });
  const server = https.createServer({
    cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"),
    key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"),
  }, (req, res) => {
    // @ts-ignore: It exists on TLSSocket
    assert(req.socket.encrypted);
    res.end("success!");
  });
  server.listen(() => {
    // deno-lint-ignore no-explicit-any
    fetch(`https://localhost:${(server.address() as any).port}`, {
      client,
    }).then(async (res) => {
      assertEquals(res.status, 200);
      assertEquals(await res.text(), "success!");
      server.close();
      deferred2.resolve();
    });
  })
    .on("error", () => fail());
  server.on("close", () => {
    deferred.resolve();
  });
  await Promise.all([deferred.promise, deferred2.promise]);
  client.close();
});

Deno.test(
  "[node/http] client upgrade",
  { permissions: { net: true } },
  async () => {
    const { promise: serverClosed, resolve: resolveServer } = Promise
      .withResolvers<void>();
    const { promise: socketClosed, resolve: resolveSocket } = Promise
      .withResolvers<void>();
    const server = http.createServer((req, res) => {
      // @ts-ignore: It exists on TLSSocket
      assert(!req.socket.encrypted);
      res.writeHead(200, { "Content-Type": "text/plain" });
      res.end("okay");
    });
    // @ts-ignore it's a socket for real
    let serverSocket;
    server.on("upgrade", (req, socket, _head) => {
      // https://github.com/denoland/deno/issues/21979
      assert(req.socket?.write);
      socket.write(
        "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
          "Upgrade: WebSocket\r\n" +
          "Connection: Upgrade\r\n" +
          "\r\n",
      );
      serverSocket = socket;
    });

    // Now that server is running
    server.listen(1337, "127.0.0.1", () => {
      // make a request
      const options = {
        port: 1337,
        host: "127.0.0.1",
        headers: {
          "Connection": "Upgrade",
          "Upgrade": "websocket",
        },
      };

      const req = http.request(options);
      req.end();

      req.on("upgrade", (_res, socket, _upgradeHead) => {
        socket.end();
        // @ts-ignore it's a socket for real
        serverSocket!.end();
        server.close(() => {
          resolveServer();
        });
        socket.on("close", () => {
          resolveSocket();
        });
      });
    });

    await serverClosed;
    await socketClosed;
  },
);

Deno.test(
  "[node/http] client end with callback",
  { permissions: { net: true } },
  async () => {
    let received = false;
    const ac = new AbortController();
    const server = Deno.serve({
      port: 0,
      signal: ac.signal,
      onListen: undefined,
    }, (_req) => {
      received = true;
      return new Response("hello");
    });
    const port = server.addr.port;
    const { promise, resolve, reject } = Promise.withResolvers<void>();
    let body = "";

    const request = http.request(
      `http://localhost:${port}/`,
      (resp) => {
        resp.on("data", (chunk) => {
          body += chunk;
        });

        resp.on("end", () => {
          resolve();
        });
      },
    );
    let endCallbackCalled = false;
    request.on("error", reject);
    request.end(() => {
      endCallbackCalled = true;
    });

    await promise;
    assert(endCallbackCalled);
    assert(received);
    ac.abort();
    await server.finished;

    assertEquals(body, "hello");
  },
);

Deno.test("[node/http] server emits error if addr in use", async () => {
  const deferred1 = Promise.withResolvers<void>();
  const deferred2 = Promise.withResolvers<Error>();

  const server = http.createServer();
  server.listen(9001);

  const server2 = http.createServer();
  server2.on("error", (e) => {
    deferred2.resolve(e);
  });
  server2.listen(9001);

  const err = await deferred2.promise;
  server.close(() => deferred1.resolve());
  server2.close();
  await deferred1.promise;
  assert(
    err.message.includes("EADDRINUSE"),
    `Wrong error: ${err.message}`,
  );
});

Deno.test(
  "[node/http] client destroy doesn't leak",
  { permissions: { net: true } },
  async () => {
    const ac = new AbortController();
    let timerId;

    const server = Deno.serve(
      { port: 5929, signal: ac.signal },
      async (_req) => {
        await new Promise((resolve) => {
          timerId = setTimeout(resolve, 5000);
        });
        return new Response("hello");
      },
    );
    const { promise, resolve, reject } = Promise.withResolvers<void>();

    const request = http.request("http://127.0.0.1:5929/");
    request.on("error", reject);
    request.on("close", () => {});
    request.end();
    setTimeout(() => {
      request.destroy(new Error());
      resolve();
    }, 100);

    await promise;
    clearTimeout(timerId);
    ac.abort();
    await server.finished;
  },
);

Deno.test(
  "[node/http] client destroy before sending request should not error",
  async () => {
    const { resolve, promise } = Promise.withResolvers<void>();
    const request = http.request("http://127.0.0.1:5929/");
    // Calling this would throw
    request.destroy();
    request.on("error", (e) => {
      assertEquals(e.message, "socket hang up");
    });
    request.on("close", () => resolve());
    await promise;

    if (Deno.build.os === "windows") {
      // FIXME(kt3k): This is necessary for preventing op leak on windows
      await new Promise((resolve) => setTimeout(resolve, 4000));
    }
  },
);

const isWindows = Deno.build.os === "windows";

Deno.test(
  "[node/http] destroyed requests should not be sent",
  { sanitizeResources: !isWindows, sanitizeOps: !isWindows },
  async () => {
    let receivedRequest = false;
    const requestClosed = Promise.withResolvers<void>();
    const ac = new AbortController();
    const server = Deno.serve({ port: 0, signal: ac.signal }, () => {
      receivedRequest = true;
      return new Response(null);
    });
    const request = http.request(`http://127.0.0.1:${server.addr.port}/`);
    request.destroy();
    request.end("hello");
    request.on("error", (err) => {
      assert(err.message.includes("socket hang up"));
      ac.abort();
    });
    request.on("close", () => {
      requestClosed.resolve();
    });
    await requestClosed.promise;
    assertEquals(receivedRequest, false);
    await server.finished;
  },
);

Deno.test("[node/http] node:http exports globalAgent", async () => {
  const http = await import("node:http");
  assert(
    http.globalAgent,
    "node:http must export 'globalAgent' on module namespace",
  );
  assert(
    http.default.globalAgent,
    "node:http must export 'globalAgent' on module default export",
  );
});

Deno.test("[node/https] node:https exports globalAgent", async () => {
  const https = await import("node:https");
  assert(
    https.globalAgent,
    "node:https must export 'globalAgent' on module namespace",
  );
  assert(
    https.default.globalAgent,
    "node:https must export 'globalAgent' on module default export",
  );
});

Deno.test("[node/http] node:http request.setHeader(header, null) doesn't throw", async () => {
  {
    const { promise, resolve } = Promise.withResolvers<void>();
    const req = http.request("http://localhost:4545/", (res) => {
      res.on("data", () => {});
      res.on("end", () => {
        resolve();
      });
    });
    // @ts-expect-error - null is not a valid header value
    req.setHeader("foo", null);
    req.end();
    await promise;
  }
  {
    const { promise, resolve } = Promise.withResolvers<void>();
    const req = http.request("http://localhost:4545/", (res) => {
      res.on("data", () => {});
      res.on("end", () => {
        resolve();
      });
    });
    // @ts-expect-error - null is not a valid header value
    req.setHeader("foo", null);
    req.end();

    await promise;
  }
});

Deno.test("[node/http] ServerResponse getHeader", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((_req, res) => {
    res.setHeader("foo", "bar");
    assertEquals(res.getHeader("foo"), "bar");
    assertEquals(res.getHeader("ligma"), undefined);
    res.end("Hello World");
  });

  server.listen(async () => {
    const { port } = server.address() as { port: number };
    const res = await fetch(`http://localhost:${port}`);
    assertEquals(await res.text(), "Hello World");
    server.close(() => {
      resolve();
    });
  });

  await promise;
});

Deno.test("[node/http] ServerResponse appendHeader", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((_req, res) => {
    res.setHeader("foo", "bar");
    res.appendHeader("foo", "baz");
    res.appendHeader("foo", ["qux"]);
    res.appendHeader("foo", ["quux"]);
    res.appendHeader("Set-Cookie", "a=b");
    res.appendHeader("Set-Cookie", ["c=d", "e=f"]);
    res.end("Hello World");
  });

  server.listen(async () => {
    const { port } = server.address() as { port: number };
    const res = await fetch(`http://localhost:${port}`);
    assertEquals(res.headers.get("foo"), "bar, baz, qux, quux");
    assertEquals(res.headers.getSetCookie(), ["a=b", "c=d", "e=f"]);
    assertEquals(await res.text(), "Hello World");
    server.close(() => {
      resolve();
    });
  });

  await promise;
});

Deno.test("[node/http] ServerResponse appendHeader set-cookie", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((_req, res) => {
    res.appendHeader("Set-Cookie", "a=b");
    res.appendHeader("Set-Cookie", "c=d");
    res.end("Hello World");
  });

  server.listen(async () => {
    const { port } = server.address() as { port: number };
    const res = await fetch(`http://localhost:${port}`);
    assertEquals(res.headers.getSetCookie(), ["a=b", "c=d"]);
    assertEquals(await res.text(), "Hello World");
    server.close(() => {
      resolve();
    });
  });

  await promise;
});

Deno.test("[node/http] ServerResponse header names case insensitive", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((_req, res) => {
    res.setHeader("Content-Length", "12345");
    assert(res.hasHeader("Content-Length"));
    res.removeHeader("content-length");
    assertEquals(res.getHeader("Content-Length"), undefined);
    assert(!res.hasHeader("Content-Length"));
    res.appendHeader("content-length", "12345");
    res.removeHeader("Content-Length");
    assertEquals(res.getHeader("content-length"), undefined);
    assert(!res.hasHeader("content-length"));
    res.end("Hello World");
  });

  server.listen(async () => {
    const { port } = server.address() as { port: number };
    const res = await fetch(`http://localhost:${port}`);
    assertEquals(res.headers.get("Content-Length"), null);
    assertEquals(res.headers.get("content-length"), null);
    assertEquals(await res.text(), "Hello World");
    server.close(() => {
      resolve();
    });
  });

  await promise;
});

Deno.test("[node/http] ServerResponse .req", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((req, res) => {
    assertEquals(res.req, req);
    res.end("foo");
  });

  server.listen(async () => {
    const { port } = server.address() as { port: number };
    const res = await fetch(`http://localhost:${port}`);
    assertEquals(await res.text(), "foo");
    server.close(() => {
      resolve();
    });
  });

  await promise;
});

Deno.test("[node/http] IncomingMessage override", () => {
  const req = new http.IncomingMessage(new net.Socket());
  // https://github.com/dougmoscrop/serverless-http/blob/3aaa6d0fe241109a8752efb011c242d249f32368/lib/request.js#L20-L30
  Object.assign(req, {
    ip: "1.1.1.1",
    complete: true,
    httpVersion: "1.1",
    httpVersionMajor: "1",
    httpVersionMinor: "1",
    method: "GET",
    headers: {},
    body: "",
    url: "https://1.1.1.1",
  });
});

Deno.test("[node/http] ServerResponse assignSocket and detachSocket", () => {
  const req = new http.IncomingMessage(new net.Socket());
  const res = new http.ServerResponse(req);
  let writtenData: string | Uint8Array | undefined = undefined;
  let writtenEncoding: string | Uint8Array | undefined = undefined;
  const socket = {
    _writableState: {},
    writable: true,
    on: Function.prototype,
    removeListener: Function.prototype,
    destroy: Function.prototype,
    cork: Function.prototype,
    uncork: Function.prototype,
    write: (
      data: string | Uint8Array,
      encoding: string,
      _cb?: (err?: Error) => void,
    ) => {
      writtenData = data;
      writtenEncoding = encoding;
    },
  };
  // @ts-ignore it's a socket mock
  res.assignSocket(socket);

  res.write("Hello World!", "utf8");
  // The first write includes HTTP headers concatenated with the body.
  // Both Node.js and Deno concatenate header + data as a string.
  assert(typeof writtenData === "string");
  assert((writtenData as string).includes("Hello World!"));
  assertEquals(writtenEncoding, "utf8");

  writtenData = undefined;
  writtenEncoding = undefined;

  // TODO(@littledivy): This test never really worked
  // because there was no data being sent and it passed.
  //
  // @ts-ignore it's a socket mock
  // res.detachSocket(socket);
  // res.write("Hello World!", "utf8");
  //
  // assertEquals(writtenData, undefined);
  // assertEquals(writtenEncoding, undefined);
});

Deno.test("[node/http] ServerResponse getHeaders", () => {
  const req = new http.IncomingMessage(new net.Socket());
  const res = new http.ServerResponse(req);
  res.setHeader("foo", "bar");
  res.setHeader("bar", "baz");
  assertEquals(res.getHeaderNames(), ["foo", "bar"]);
  assertEquals(res.getHeaders(), { "foo": "bar", "bar": "baz" });
});

Deno.test("[node/http] ServerResponse default status code 200", () => {
  const req = new http.IncomingMessage(new net.Socket());
  const res = new http.ServerResponse(req);
  assertEquals(res.statusCode, 200);
});

Deno.test("[node/http] maxHeaderSize is defined", () => {
  assertEquals(http.maxHeaderSize, 16_384);
});

Deno.test("[node/http] server graceful close", async () => {
  const server = http.createServer(function (_, response) {
    response.writeHead(200, {});
    response.end("ok");
    server.close();
  });

  const { promise, resolve } = Promise.withResolvers<void>();
  server.listen(0, function () {
    // deno-lint-ignore no-explicit-any
    const port = (server.address() as any).port;
    const testURL = url.parse(
      `http://localhost:${port}`,
    );

    http.request(testURL, function (response) {
      assertEquals(response.statusCode, 200);
      response.on("data", function () {});
      response.on("end", function () {
        resolve();
      });
    }).end();
  });

  await promise;
});

Deno.test("[node/http] server closeAllConnections shutdown", async () => {
  const server = http.createServer((_req, res) => {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({
      data: "Hello World!",
    }));
  });

  server.listen(0);
  const { promise, resolve } = Promise.withResolvers<void>();
  setTimeout(() => {
    server.close(() => resolve());
    server.closeAllConnections();
  }, 2000);

  await promise;
});

Deno.test("[node/http] server closeIdleConnections shutdown", async () => {
  const server = http.createServer({ keepAliveTimeout: 60000 }, (_req, res) => {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({
      data: "Hello World!",
    }));
  });

  server.listen(0);
  const { promise, resolve } = Promise.withResolvers<void>();
  setTimeout(() => {
    server.close(() => resolve());
    server.closeIdleConnections();
  }, 2000);

  await promise;
});

Deno.test("[node/http] client closing a streaming response doesn't terminate server", async () => {
  let interval: NodeJS.Timeout;
  const server = http.createServer((req, res) => {
    res.writeHead(200, { "Content-Type": "text/plain" });
    interval = setInterval(() => {
      res.write("Hello, world!\n");
    }, 100);
    req.on("end", () => {
      clearInterval(interval);
      res.end();
    });
    req.on("error", (err) => {
      console.error("Request error:", err);
      clearInterval(interval);
      res.end();
    });
  });

  const deferred1 = Promise.withResolvers<void>();
  server.listen(0, () => {
    // deno-lint-ignore no-explicit-any
    const port = (server.address() as any).port;

    // Create a client connection to the server
    const client = net.createConnection({ port }, () => {
      console.log("Client connected to server");

      // Write data to the server
      client.write("GET / HTTP/1.1\r\n");
      client.write("Host: localhost\r\n");
      client.write("Connection: close\r\n");
      client.write("\r\n");

      // End the client connection prematurely while reading data
      client.on("data", (data) => {
        assert(data.length > 0);
        client.end();
        setTimeout(() => deferred1.resolve(), 100);
      });
    });
  });

  await deferred1.promise;
  assertEquals(server.listening, true);
  server.close();
  assertEquals(server.listening, false);
  clearInterval(interval!);
});

Deno.test("[node/http] client closing a streaming request doesn't terminate server", async () => {
  let interval: NodeJS.Timeout;
  let uploadedData = "";
  let requestError: Error | null = null;
  const deferred1 = Promise.withResolvers<void>();
  const server = http.createServer((req, res) => {
    res.writeHead(200, { "Content-Type": "text/plain" });
    interval = setInterval(() => {
      res.write("Hello, world!\n");
    }, 100);
    req.on("data", (chunk) => {
      uploadedData += chunk.toString();
    });
    req.on("end", () => {
      clearInterval(interval);
    });
    req.on("error", (err) => {
      deferred1.resolve();
      requestError = err;
      clearInterval(interval);
      res.end();
    });
  });

  server.listen(0, () => {
    // deno-lint-ignore no-explicit-any
    const port = (server.address() as any).port;

    // Create a client connection to the server
    const client = net.createConnection({ port }, () => {
      const headers = [
        "POST /upload HTTP/1.1",
        "Host: localhost",
        "Content-Type: text/plain",
        "Transfer-Encoding: chunked",
        "",
        "",
      ].join("\r\n");

      client.write(headers);

      const chunk = "A".repeat(100);
      let sentChunks = 0;

      function writeChunk() {
        const chunkHeader = `${chunk.length.toString(16)}\r\n`;
        client.write(chunkHeader);
        client.write(chunk);
        client.write("\r\n");
        sentChunks++;

        if (sentChunks >= 3) {
          client.destroy();
        } else {
          setTimeout(writeChunk, 10);
        }
      }
      writeChunk();
    });
  });

  await deferred1.promise;
  assert(requestError !== null, "Server should have received an error");
  assert(
    (requestError! as Error) instanceof Error,
    `Expected Error, got ${(requestError! as Error)?.constructor?.name}`,
  );
  assert(
    (requestError! as Error)?.message.includes("aborted") ||
      (requestError! as Error)?.message.includes(
        "error reading a body from connection",
      ),
  );
  assertEquals(server.listening, true);
  server.close();
  assertEquals(server.listening, false);
  clearInterval(interval!);
});

Deno.test("[node/http] http.request() post streaming body works", async () => {
  const server = http.createServer((req, res) => {
    if (req.method === "POST") {
      let receivedBytes = 0;
      req.on("data", (chunk) => {
        receivedBytes += chunk.length;
      });
      req.on("end", () => {
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ bytes: receivedBytes }));
      });
    } else {
      res.writeHead(405, { "Content-Type": "text/plain" });
      res.end("Method Not Allowed");
    }
  });

  const responseEnded = Promise.withResolvers<void>();
  const fileClosed = Promise.withResolvers<void>();
  const timeout = setTimeout(() => {
    responseEnded.reject(new Error("timeout"));
  }, 5000);
  server.listen(0, () => {
    // deno-lint-ignore no-explicit-any
    const port = (server.address() as any).port;
    const filePath = relative(
      Deno.cwd(),
      fromFileUrl(new URL("./testdata/lorem_ipsum_512kb.txt", import.meta.url)),
    );
    const contentLength = 524289;

    const options = {
      hostname: "localhost",
      port: port,
      path: "/",
      method: "POST",
      headers: {
        "Content-Type": "application/octet-stream",
        "Content-Length": contentLength,
      },
    };

    const req = http.request(options, (res) => {
      let responseBody = "";
      res.on("data", (chunk) => {
        responseBody += chunk;
      });

      res.on("end", () => {
        const response = JSON.parse(responseBody);
        assertEquals(res.statusCode, 200);
        assertEquals(response.bytes, contentLength);
        responseEnded.resolve();
      });
    });

    req.on("error", (e) => {
      console.error(`Problem with request: ${e.message}`);
    });

    const readStream = fs.createReadStream(filePath);
    readStream.pipe(req);
    readStream.on("close", fileClosed.resolve);
  });
  await responseEnded.promise;
  await fileClosed.promise;
  assertEquals(server.listening, true);
  server.close();
  clearTimeout(timeout);
  assertEquals(server.listening, false);
});

// https://github.com/denoland/deno/issues/24239
Deno.test("[node/http] ServerResponse write transfer-encoding chunked", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((_req, res) => {
    res.setHeader("Content-Type", "text/event-stream");
    res.setHeader("Cache-Control", "no-cache");
    res.setHeader("Connection", "keep-alive");
    res.setHeader("Transfer-Encoding", "chunked");
    res.setHeader("Access-Control-Allow-Origin", "*");

    res.writeHead(200, {
      "Other-Header": "value",
    });
    res.write("");
  });

  server.listen(async () => {
    const { port } = server.address() as { port: number };
    const res = await fetch(`http://localhost:${port}`);
    assertEquals(res.status, 200);
    assertEquals(res.headers.get("content-type"), "text/event-stream");
    assertEquals(res.headers.get("Other-Header"), "value");
    await res.body!.cancel();

    server.close(() => {
      resolve();
    });
  });

  await promise;
});

Deno.test("[node/http] Server.address() can be null", () => {
  const server = http.createServer((_req, res) => res.end("it works"));
  assertEquals(server.address(), null);
});

Deno.test("[node/http] ClientRequest PUT subarray", async () => {
  const buffer = Buffer.from("hello world");
  const payload = buffer.subarray(6, 11);
  let body = "";
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const req = http.request("http://localhost:4545/echo_server", {
    method: "PUT",
  }, (resp) => {
    resp.on("data", (chunk) => {
      body += chunk;
    });

    resp.on("end", () => {
      resolve();
    });
  });
  req.once("error", (e) => reject(e));
  req.end(payload);
  await promise;
  assertEquals(body, "world");
});

Deno.test("[node/http] req.url equals pathname + search", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();

  const server = http.createServer((req, res) => res.end(req.url));
  server.listen(async () => {
    const { port } = server.address() as net.AddressInfo;
    const res = await fetch(`http://localhost:${port}/foo/bar?baz=1`);
    const text = await res.text();
    assertEquals(text, "/foo/bar?baz=1");

    server.close(() => {
      resolve();
    });
  });

  await promise;
});

Deno.test("[node/http] ClientRequest content-disposition header works", async () => {
  const payload = Buffer.from("hello world");
  let body = "";
  let headers = {} as http.IncomingHttpHeaders;
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const req = http.request("http://localhost:4545/echo_server", {
    method: "PUT",
    headers: {
      "content-disposition": "attachment",
    },
  }, (resp) => {
    headers = resp.headers;
    resp.on("data", (chunk) => {
      body += chunk;
    });

    resp.on("end", () => {
      resolve();
    });
  });
  req.once("error", (e) => reject(e));
  req.end(payload);
  await promise;
  assertEquals(body, "hello world");
  assertEquals(headers["content-disposition"], "attachment");
});

Deno.test("[node/http] In ClientRequest, option.hostname has precedence over options.host", async () => {
  const responseReceived = Promise.withResolvers<void>();

  new http.ClientRequest({
    hostname: "localhost",
    host: "invalid-hostname.test",
    port: 4545,
    path: "/http_version",
  }).on("response", async (res) => {
    assertEquals(res.statusCode, 200);
    assertEquals(await text(res), "HTTP/1.1");
    responseReceived.resolve();
  }).end();

  await responseReceived.promise;
});

Deno.test("[node/http] upgraded socket closes when the server closed without closing handshake", async () => {
  const clientSocketClosed = Promise.withResolvers<void>();
  const serverProcessClosed = Promise.withResolvers<void>();

  // Uses the server in different process to shutdown it without closing handshake
  const server = `
    Deno.serve({ port: 1337 }, (req) => {
      if (req.headers.get("upgrade") != "websocket") {
        return new Response("ok");
      }
      console.log("upgrade on server");
      const { socket, response } = Deno.upgradeWebSocket(req);
      socket.addEventListener("message", (event) => {
        console.log("server received", event.data);
        socket.send("pong");
      });
      return response;
    });
  `;

  const p = new Deno.Command("deno", { args: ["eval", server] }).spawn();

  // Wait for the server to respond
  await retry(async () => {
    const resp = await fetch("http://localhost:1337");
    const _text = await resp.text();
  });

  const options = {
    port: 1337,
    host: "127.0.0.1",
    headers: {
      "Connection": "Upgrade",
      "Upgrade": "websocket",
      "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
    },
  };

  http.request(options).on("upgrade", (_res, socket) => {
    socket.on("close", () => {
      console.log("client socket closed");
      clientSocketClosed.resolve();
    });

    socket.on("error", (e) => {
      if (
        !("code" in e) ||
        (e.code !== "ECONNRESET" && e.code !== "EINVAL")
      ) {
        throw e;
      }
      console.log("client socket closed");
      clientSocketClosed.resolve();
    });

    socket.on("data", async (data) => {
      // receives pong message
      assertEquals(data, Buffer.from("8104706f6e67", "hex"));

      p.kill();
      await p.status;

      console.log("process closed");
      serverProcessClosed.resolve();
    });

    // sending ping message
    socket.write(Buffer.from("81847de88e01", "hex"));
    socket.write(Buffer.from("0d81e066", "hex"));
  }).end();

  await clientSocketClosed.promise;
  await serverProcessClosed.promise;
});

// deno-lint-ignore require-await
Deno.test("[node/http] ServerResponse.call()", async () => {
  function Wrapper(this: unknown, req: IncomingMessage) {
    ServerResponse.call(this, req);
  }
  Object.setPrototypeOf(Wrapper.prototype, ServerResponse.prototype);

  // deno-lint-ignore no-explicit-any
  const wrapper = new (Wrapper as any)(new IncomingMessage(new Socket()));

  assert(wrapper instanceof ServerResponse);
});

Deno.test("[node/http] ServerResponse _header", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((_req, res) => {
    assert(Object.hasOwn(res, "_header"));
    res.end();
  });

  server.listen(async () => {
    const { port } = server.address() as { port: number };
    const res = await fetch(`http://localhost:${port}`);
    await res.body?.cancel();
    server.close(() => {
      resolve();
    });
  });

  await promise;
});

Deno.test("[node/http] ServerResponse connection", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((_req, res) => {
    assert("connection" in res);
    assert(res.connection instanceof Socket);
    res.end();
  });

  server.listen(async () => {
    const { port } = server.address() as { port: number };
    const res = await fetch(`http://localhost:${port}`);
    await res.body?.cancel();
    server.close(() => {
      resolve();
    });
  });

  await promise;
});

Deno.test("[node/http] ServerResponse socket", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((_req, res) => {
    assert("socket" in res);
    assert(res.socket instanceof Socket);
    res.end();
  });

  server.listen(async () => {
    const { port } = server.address() as { port: number };
    const res = await fetch(`http://localhost:${port}`);
    await res.body?.cancel();
    server.close(() => {
      resolve();
    });
  });

  await promise;
});

Deno.test("[node/http] decompress brotli response", {
  permissions: { net: true },
}, async () => {
  const ac = new AbortController();
  const server = Deno.serve({
    port: 0,
    signal: ac.signal,
    onListen: undefined,
  }, (_req) => {
    return Response.json([
      ["accept-language", "*"],
      ["host", "localhost:3000"],
      ["user-agent", "Deno/2.1.1"],
    ], {});
  });
  const port = server.addr.port;
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  let body = "";

  const request = http.get(
    `http://localhost:${port}/`,
    {
      headers: {
        "accept-encoding": "gzip, deflate, br, zstd",
      },
    },
    (resp) => {
      const encoding = resp.headers["content-encoding"];
      if (encoding === "br") {
        // Server compressed with brotli - decompress
        const decompress = zlib.createBrotliDecompress();
        resp.on("data", (chunk) => decompress.write(chunk));
        resp.on("end", () => decompress.end());
        decompress.on("data", (chunk) => {
          body += chunk;
        });
        decompress.on("end", () => resolve());
      } else {
        // Server did not compress - read directly
        resp.on("data", (chunk) => {
          body += chunk;
        });
        resp.on("end", () => resolve());
      }
    },
  );
  request.on("error", reject);

  await promise;
  ac.abort();
  await server.finished;

  assertEquals(JSON.parse(body), [["accept-language", "*"], [
    "host",
    "localhost:3000",
  ], ["user-agent", "Deno/2.1.1"]]);
});

Deno.test("[node/http] an error with DNS propagates to request object", async () => {
  const { resolve, promise } = Promise.withResolvers<void>();
  const req = http.request("http://invalid-hostname.test", () => {});
  req.on("error", (err) => {
    assertEquals(err.name, "Error");
    assertEquals(err.message, "getaddrinfo ENOTFOUND invalid-hostname.test");
    resolve();
  });
  await promise;
});

Deno.test("[node/http] supports proxy http request", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = Deno.serve({ port: 0, onListen }, (req) => {
    console.log("server received", req.url);
    assertEquals(req.url, "http://example.com/");
    return new Response("ok");
  });

  function onListen({ port }: { port: number }) {
    http.request({
      host: "localhost",
      port,
      path: "http://example.com",
    }, async (res) => {
      assertEquals(res.statusCode, 200);
      assertEquals(await text(res), "ok");
      resolve();
      server.shutdown();
    }).end();
  }
  await promise;
  await server.finished;
});

Deno.test("[node/http] `request` requires net permission to host and port", {
  permissions: { net: ["localhost:4545"] },
}, async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  http.request("http://localhost:4545/echo.ts", async (res) => {
    assertEquals(res.statusCode, 200);
    assertStringIncludes(await text(res), "function echo(");
    resolve();
  }).end();
  await promise;
});

const ca = await Deno.readTextFile("tests/testdata/tls/RootCA.pem");

Deno.test({
  name: "[node/https] `request` requires net permission to host and port",
  permissions: { net: ["localhost:5545"] },
  async fn() {
    const { promise, resolve } = Promise.withResolvers<void>();
    https.request("https://localhost:5545/echo.ts", { ca }, async (res) => {
      assertEquals(res.statusCode, 200);
      assertStringIncludes(await text(res), "function echo(");
      resolve();
    }).end();
    await promise;
  },
});

Deno.test(
  "[node/http] `request` errors with EPERM error when permission is not granted",
  { permissions: { net: ["localhost:4321"] } }, // wrong permission
  async () => {
    const { promise, resolve } = Promise.withResolvers<void>();
    http.request("http://localhost:4545/echo.ts", async () => {})
      .on("error", (e) => {
        assertEquals(e.message, "getaddrinfo EPERM localhost");
        // deno-lint-ignore no-explicit-any
        assertEquals((e as any).code, "EPERM");
        resolve();
      }).end();
    await promise;
  },
);

Deno.test("[node/http] 'close' event is emitted when request finished", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();

  const req = http.request("http://localhost:4545/echo.ts", async (res) => {
    res.on("close", resolve);
    await text(res);
  });
  req.end();
  await promise;
});

Deno.test("[node/http] 'close' event is emitted on ServerResponse object when the client aborted the request in the middle", async () => {
  let responseCloseEmitted = false;
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((req, res) => {
    res.on("close", () => {
      responseCloseEmitted = true;
      res.end();
    });

    // Streams thre response body
    res.writeHead(200, { "Content-Type": "text/plain" });
    const interval = setInterval(() => {
      res.write("Hello, world!\n");
    }, 100);

    req.on("error", () => {
      clearInterval(interval);
      resolve();
    });
  });

  server.listen(0, () => {
    const { port } = server.address() as { port: number };
    const client = net.createConnection({ port });
    client.write("GET / HTTP/1.1\r\n");
    client.write("Host: localhost\r\n");
    client.write("Connection: close\r\n");
    client.write("\r\n");
    client.on("data", () => {
      // Client aborts the request in the middle
      client.end();
    });
  });

  await promise;
  await new Promise((resolve) => server.close(resolve));
  assert(responseCloseEmitted);
});

Deno.test("[node/http] rawHeaders are in flattened format", async () => {
  const getHeader = (req: IncomingMessage, name: string) => {
    const idx = req.rawHeaders.indexOf(name);
    if (idx < 0) {
      throw new Error(`Header ${name} not found`);
    }
    return [name, req.rawHeaders[idx + 1]];
  };
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((req, res) => {
    resolve();
    // TODO(nathanwhit): the raw headers should not be lowercased, they should be
    // exactly as they appeared in the request
    assertEquals(getHeader(req, "content-type"), [
      "content-type",
      "text/plain",
    ]);
    assertEquals(getHeader(req, "set-cookie"), [
      "set-cookie",
      "foo=bar",
    ]);
    res.end();
  });

  server.listen(0, async () => {
    const { port } = server.address() as { port: number };
    const response = await fetch(`http://localhost:${port}`, {
      headers: {
        "Set-Cookie": "foo=bar",
        "Content-Type": "text/plain",
      },
    });
    await response.body?.cancel();
  });

  await promise;
  await new Promise((resolve) => server.close(resolve));
});

Deno.test("[node/http] request header values trim trailing OWS", async () => {
  const parsed = Promise.withResolvers<void>();
  const server = http.createServer((req, res) => {
    try {
      assertEquals(req.headers["x-ows"], "value");
      const idx = req.rawHeaders.findIndex((header) =>
        header.toLowerCase() === "x-ows"
      );
      assert(idx >= 0);
      assertEquals(req.rawHeaders[idx + 1], "value");
      res.end();
      parsed.resolve();
    } catch (err) {
      parsed.reject(err);
      res.destroy(err as Error);
    }
  });

  await new Promise<void>((resolve) => {
    server.listen(0, "127.0.0.1", resolve);
  });

  const client = net.createConnection(
    (server.address() as AddressInfo).port,
    "127.0.0.1",
    () => {
      client.end(
        "GET / HTTP/1.1\r\n" +
          "Host: localhost\r\n" +
          "X-OWS:\t value \t \r\n" +
          "Connection: close\r\n\r\n",
      );
    },
  );
  client.resume();
  client.on("error", parsed.reject);

  try {
    await parsed.promise;
  } finally {
    client.destroy();
    await new Promise((resolve) => server.close(resolve));
  }
});

// TODO(@bartlomieju): re-enable once server-side HTTP also uses llhttp
// (currently the Deno.serve-based server path still needs RID access)
Deno.test("[node/http] client http over unix socket works", async () => {
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  // On Windows, IPC uses named pipes; on Unix, use a domain socket path.
  const socketPath = Deno.build.os === "windows"
    ? `\\\\?\\pipe\\deno-test-${crypto.randomUUID()}`
    : Deno.makeTempDirSync() + "/server.sock";
  const server = http.createServer((_req, res) => {
    res.end("ok");
  });
  server.on("error", (e: Error) => {
    // Unix sockets may not work on all platforms (e.g. Windows)
    server.close();
    reject(e);
  });
  server.listen(socketPath, () => {
    const options = {
      socketPath,
      path: "/",
      method: "GET",
    };
    http.request(options, async (res) => {
      assertEquals(res.statusCode, 200);
      assertEquals(await text(res), "ok");
      server.close(() => resolve());
    }).end();
  });
  await promise;
});

Deno.test({
  name: "[node/https] null ca, key and cert req options",
  permissions: { net: ["localhost:5545"] },
  async fn() {
    const { promise, resolve } = Promise.withResolvers<void>();
    https.request("https://localhost:5545/echo.ts", {
      ca,
      // @ts-expect-error - key can be null at runtime
      key: null,
      // @ts-expect-error - cert can be null at runtime
      cert: null,
    }, async (res) => {
      assertEquals(res.statusCode, 200);
      assertStringIncludes(await text(res), "function echo(");
      resolve();
    }).end();
    await promise;
  },
});

Deno.test("[node/http] server.listen respects signal option", async () => {
  const [exitCode, _output] = await execCode(`
    import { createServer } from 'node:http';

    const abortController = new AbortController();

    const server = createServer((_req, res) => {
      res.writeHead(404).end();
    }).on('listening', () => {
      // Precedes setTimeout and exits with 0
      abortController.abort();
      setTimeout(() => process.exit(1), 1000);
    }).on('close', () => {
      process.exit(0);
    });

    server.listen({
      host: 'localhost',
      port: 0,
      signal: abortController.signal
    });
  `);
  assertEquals(exitCode, 0);
});

// Test for empty chunk in chunked POST request
// Regression test for: https://github.com/denoland/deno/issues/31056
Deno.test("[node/http] client request with empty write in chunked POST completes", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  let requestBody = "";

  const server = http.createServer((req, res) => {
    let body = "";
    req.on("data", (chunk) => {
      body += chunk.toString();
    });

    req.on("end", () => {
      requestBody = body;
      res.statusCode = 200;
      res.end("OK");
    });
  });

  server.listen(() => {
    const { port } = server.address() as { port: number };

    const req = http.request({
      hostname: "localhost",
      port,
      path: "/",
      method: "POST",
    }, (res) => {
      res.on("data", () => {});
      res.on("end", () => {
        server.close(() => resolve());
      });
    });

    // This should complete successfully even with an empty write
    req.write("");
    req.end();
  });

  await promise;
  assertEquals(requestBody, "");
});

Deno.test("[node/http] Server.address() includes family property", async () => {
  // Test IPv4
  {
    const { promise, resolve } = Promise.withResolvers<void>();
    const server = http.createServer((_req, res) => res.end("ok"));

    server.listen(0, "127.0.0.1", () => {
      const addr = server.address();
      assert(addr !== null && typeof addr === "object");
      assertEquals(addr.address, "127.0.0.1");
      assertEquals(addr.family, "IPv4");
      assertEquals(typeof addr.port, "number");
      server.close(() => resolve());
    });

    await promise;
  }

  // Test IPv6
  {
    const { promise, resolve } = Promise.withResolvers<void>();
    const server = http.createServer((_req, res) => res.end("ok"));

    server.listen(0, "::1", () => {
      const addr = server.address();
      assert(addr !== null && typeof addr === "object");
      assertEquals(addr.address, "::1");
      assertEquals(addr.family, "IPv6");
      assertEquals(typeof addr.port, "number");
      server.close(() => resolve());
    });

    await promise;
  }
});

// https://github.com/denoland/deno/issues/31758
Deno.test("[node/http] address() returns assigned port immediately after listen()", async () => {
  const server = http.createServer();
  server.listen(0);

  // address() should return the real port synchronously, not 0
  const addr = server.address()!;
  assert(typeof addr === "object");
  assert(typeof addr.port === "number");
  assert(addr.port > 0, `Expected port > 0, got ${addr.port}`);

  const { promise, resolve } = Promise.withResolvers<void>();
  server.close(() => resolve());
  await promise;
});

Deno.test("[node/http] ServerResponse.writeEarlyHints", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((_req, res) => {
    res.writeEarlyHints(
      { link: "</styles.css>; rel=preload; as=style" },
      () => {
        res.writeHead(200);
        res.end("ok");
      },
    );
  });
  server.listen(0, async () => {
    const addr = server.address() as { port: number };
    const res = await fetch(`http://localhost:${addr.port}`);
    assertEquals(await res.text(), "ok");
    server.close(() => resolve());
  });
  await promise;
});

// https://github.com/denoland/deno/issues/32780
Deno.test({
  name: "[node/http] keep-alive request close fires before socket free",
  sanitizeResources: false,
  async fn() {
    const { promise, resolve: done } = Promise.withResolvers<void>();

    const agent = new http.Agent({
      keepAlive: true,
      maxSockets: 1,
      maxFreeSockets: 1,
    });

    const server = http.createServer((_req, res) => {
      res.end("ok");
    });

    // Capture the shared socket so we can check its listener count
    // even after res.socket is nulled during the keep-alive handoff.
    let sharedSocket: Socket | null = null;

    function makeRequest(path: string): Promise<void> {
      return new Promise((resolve, reject) => {
        const req = http.get(
          {
            host: "127.0.0.1",
            port: (server.address() as { port: number }).port,
            agent,
            path,
          },
          (res) => {
            const sock = res.socket!;
            if (!sharedSocket) sharedSocket = sock;

            // Attach a per-request listener on the socket, mimicking what
            // node-fetch and similar libraries do.
            const onData = () => {};
            sock.on("data", onData);

            // Clean it up on request close — this must fire BEFORE the
            // agent reuses the socket, otherwise the listener leaks.
            req.on("close", () => {
              sock.removeListener("data", onData);
            });

            res.on("data", () => {});
            res.on("end", () => resolve());
          },
        );
        req.on("error", reject);
      });
    }

    server.listen(0, async () => {
      for (let i = 0; i < 15; i++) {
        await makeRequest(`/req-${i}`);
        await new Promise((resolve) => setTimeout(resolve, 5));
      }

      // Without the fix, req "close" never fires in the keep-alive path,
      // so all 15 "data" listeners accumulate on the socket.
      // With the fix, each listener is cleaned up before socket reuse.
      const leakedListeners = sharedSocket!.listenerCount("data");
      assert(
        leakedListeners === 0,
        `Expected 0 "data" listeners on the socket, but found ${leakedListeners}`,
      );

      agent.destroy();
      server.close(() => done());
    });

    await promise;
  },
});

Deno.test("[node/http] keep-alive timer is suspended during active request", async () => {
  const server = http.createServer(
    { keepAliveTimeout: 10 },
    async (req, res) => {
      if (req.url === "/slow") {
        await new Promise((resolve) => setTimeout(resolve, 1200));
      }
      res.end(req.url);
    },
  );

  await new Promise<void>((resolve) => server.listen(0, resolve));
  const port = (server.address() as AddressInfo).port;
  const socket = net.createConnection(port, "127.0.0.1");
  await once(socket, "connect");

  let received = "";
  socket.on("data", (chunk) => {
    received += chunk;
  });

  async function readBody(body: string) {
    const deadline = Date.now() + 4000;
    while (!received.includes(body)) {
      if (Date.now() > deadline) {
        throw new Error(`Timed out waiting for response body ${body}`);
      }
      await new Promise((resolve) => setTimeout(resolve, 10));
    }
    received = "";
  }

  try {
    socket.write(
      "GET /first HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n",
    );
    await readBody("/first");
    socket.write(
      "GET /slow HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
    );
    await readBody("/slow");
  } finally {
    socket.destroy();
    await new Promise<void>((resolve) => server.close(() => resolve()));
  }
});

Deno.test("[node/http] AsyncLocalStorage propagates into request handler", async () => {
  const storage = new AsyncLocalStorage<string>();
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const responseDone = Promise.withResolvers<void>();
  const requestStart = channel("http.server.request.start");
  const subscriber = () => storage.enterWith("request-context");
  const server = http.createServer((_req, res) => {
    try {
      assertEquals(storage.getStore(), "request-context");
      resolve();
    } catch (err) {
      reject(err);
    } finally {
      res.end("ok");
    }
  });

  requestStart.subscribe(subscriber);
  await new Promise<void>((resolve) => server.listen(0, resolve));
  try {
    const port = (server.address() as AddressInfo).port;
    const req = http.get(`http://127.0.0.1:${port}`, (res) => {
      res.resume();
      res.on("end", responseDone.resolve);
      res.on("error", responseDone.reject);
    });
    req.on("error", reject);
    await Promise.all([promise, responseDone.promise]);
  } finally {
    requestStart.unsubscribe(subscriber);
    storage.disable();
    await new Promise<void>((resolve) => server.close(() => resolve()));
  }
});

Deno.test("[node/http] AsyncLocalStorage enterWith in request handler is isolated", async () => {
  const storage = new AsyncLocalStorage<string>();
  const firstDone = Promise.withResolvers<void>();
  const secondDone = Promise.withResolvers<void>();
  let requests = 0;
  const server = http.createServer((_req, res) => {
    try {
      requests++;
      if (requests === 1) {
        assertEquals(storage.getStore(), undefined);
        storage.enterWith("first-request");
        assertEquals(storage.getStore(), "first-request");
      } else {
        assertEquals(storage.getStore(), undefined);
      }
      res.end("ok");
      if (requests === 1) {
        firstDone.resolve();
      } else {
        secondDone.resolve();
      }
    } catch (err) {
      firstDone.reject(err);
      secondDone.reject(err);
      res.destroy(err as Error);
    }
  });

  await new Promise<void>((resolve) => server.listen(0, resolve));
  try {
    const port = (server.address() as AddressInfo).port;
    const request = () =>
      new Promise<void>((resolve, reject) => {
        const req = http.get(`http://127.0.0.1:${port}`, (res) => {
          res.resume();
          res.on("end", resolve);
          res.on("error", reject);
        });
        req.on("error", reject);
      });

    await request();
    await firstDone.promise;
    await request();
    await secondDone.promise;
  } finally {
    storage.disable();
    await new Promise<void>((resolve) => server.close(() => resolve()));
  }
});

Deno.test("[node/http] async_hooks observes request execution resource", async () => {
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  const responseDone = Promise.withResolvers<void>();
  const hook = createHook({
    before() {},
  });
  const server = http.createServer((req, res) => {
    try {
      assertEquals(executionAsyncResource(), req);
      res.end("ok");
      resolve();
    } catch (err) {
      reject(err);
    }
  });

  hook.enable();
  try {
    await new Promise<void>((resolve) => server.listen(0, resolve));
    const port = (server.address() as AddressInfo).port;
    const req = http.get(`http://127.0.0.1:${port}`, (res) => {
      res.resume();
      res.on("end", responseDone.resolve);
      res.on("error", responseDone.reject);
    });
    req.on("error", reject);
    await Promise.all([promise, responseDone.promise]);
  } finally {
    hook.disable();
    await new Promise<void>((resolve) => server.close(() => resolve()));
  }
});

Deno.test("[node/http] abandoned suspended keep-alive timer emits async_hooks destroy", async () => {
  const server = http.createServer(
    { keepAliveTimeout: 10 },
    (_req, res) => res.end("ok"),
  );

  await new Promise<void>((resolve) => server.listen(0, resolve));
  const port = (server.address() as AddressInfo).port;
  const socket = net.createConnection(port, "127.0.0.1");
  await once(socket, "connect");

  let received = "";
  socket.on("data", (chunk) => {
    received += chunk;
  });

  async function readText(text: string) {
    while (!received.includes(text)) {
      await once(socket, "data");
    }
    received = "";
  }

  const timeoutIds = new Set<number>();
  const destroyedIds = new Set<number>();
  const hook = createHook({
    init(asyncId, type, _triggerAsyncId, resource) {
      const timeout = resource as { _idleTimeout?: number };
      if (type === "Timeout" && timeout._idleTimeout === 1010) {
        timeoutIds.add(asyncId);
      }
    },
    destroy(asyncId) {
      destroyedIds.add(asyncId);
    },
  });

  hook.enable();
  let keepAliveTimerIds: number[] = [];
  try {
    socket.write(
      "GET /first HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n",
    );
    await readText("ok");
    keepAliveTimerIds = Array.from(timeoutIds);
    assert(keepAliveTimerIds.length > 0);

    socket.write(
      "GET /close HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
    );
    await readText("ok");
    await once(socket, "close");
  } finally {
    socket.destroy();
    await new Promise<void>((resolve) => server.close(() => resolve()));
    hook.disable();
  }

  for (const asyncId of keepAliveTimerIds) {
    assert(
      destroyedIds.has(asyncId),
      `Timeout asyncId ${asyncId} did not emit destroy`,
    );
  }
});

Deno.test("[node/http] user socket timeout still applies after keep-alive reuse", async () => {
  let timeoutCount = 0;
  const serverTimeout = Promise.withResolvers<void>();
  const server = http.createServer(
    { keepAliveTimeout: 10 },
    async (req, res) => {
      if (req.url === "/timeout") {
        req.socket.setTimeout(50);
        await serverTimeout.promise;
      }
      res.end(req.url);
    },
  );
  server.on("timeout", (socket) => {
    timeoutCount++;
    socket.setTimeout(0);
    serverTimeout.resolve();
  });

  await new Promise<void>((resolve) => server.listen(0, resolve));
  const port = (server.address() as AddressInfo).port;
  const socket = net.createConnection(port, "127.0.0.1");
  await once(socket, "connect");

  let received = "";
  socket.on("data", (chunk) => {
    received += chunk;
  });

  async function readBody(body: string) {
    const deadline = Date.now() + 4000;
    while (!received.includes(body)) {
      if (Date.now() > deadline) {
        throw new Error(`Timed out waiting for response body ${body}`);
      }
      await new Promise((resolve) => setTimeout(resolve, 10));
    }
    received = "";
  }

  try {
    socket.write(
      "GET /first HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n",
    );
    await readBody("/first");
    socket.write(
      "GET /timeout HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
    );
    await readBody("/timeout");
    assertEquals(timeoutCount, 1);
  } finally {
    socket.destroy();
    await new Promise<void>((resolve) => server.close(() => resolve()));
  }
});

Deno.test("[node/http] custom server timeout still applies after keep-alive reuse", async () => {
  let timeoutCount = 0;
  const serverTimeout = Promise.withResolvers<void>();
  const server = http.createServer(
    { keepAliveTimeout: 10 },
    async (req, res) => {
      if (req.url === "/timeout") {
        await serverTimeout.promise;
      }
      res.end(req.url);
    },
  );
  server.setTimeout(50);
  server.on("timeout", (socket) => {
    timeoutCount++;
    socket.setTimeout(0);
    serverTimeout.resolve();
  });

  await new Promise<void>((resolve) => server.listen(0, resolve));
  const port = (server.address() as AddressInfo).port;
  const socket = net.createConnection(port, "127.0.0.1");
  await once(socket, "connect");

  let received = "";
  socket.on("data", (chunk) => {
    received += chunk;
  });

  async function readBody(body: string) {
    const deadline = Date.now() + 4000;
    while (!received.includes(body)) {
      if (Date.now() > deadline) {
        throw new Error(`Timed out waiting for response body ${body}`);
      }
      await new Promise((resolve) => setTimeout(resolve, 10));
    }
    received = "";
  }

  try {
    socket.write(
      "GET /first HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n",
    );
    await readBody("/first");
    socket.write(
      "GET /timeout HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
    );
    await readBody("/timeout");
    assertEquals(timeoutCount, 1);
  } finally {
    socket.destroy();
    await new Promise<void>((resolve) => server.close(() => resolve()));
  }
});

Deno.test("[node/http] upgrade detaches reused keep-alive socket timeout", async () => {
  const upgraded = Promise.withResolvers<void>();
  let upgradedSocket: Duplex | undefined;
  const server = http.createServer(
    { keepAliveTimeout: 10 },
    (_req, res) => res.end("ok"),
  );
  server.on("upgrade", (_req, socket) => {
    upgradedSocket = socket;
    upgraded.resolve();
    socket.write(
      "HTTP/1.1 101 Switching Protocols\r\n" +
        "Connection: Upgrade\r\n" +
        "Upgrade: test\r\n" +
        "\r\n",
    );
    setTimeout(() => socket.write("detached"), 1200);
  });

  await new Promise<void>((resolve) => server.listen(0, resolve));
  const port = (server.address() as AddressInfo).port;
  const socket = net.createConnection(port, "127.0.0.1");
  await once(socket, "connect");

  let received = "";
  socket.on("data", (chunk) => {
    received += chunk;
  });

  async function readText(text: string) {
    const deadline = Date.now() + 4000;
    while (!received.includes(text)) {
      if (Date.now() > deadline) {
        throw new Error(`Timed out waiting for ${text}`);
      }
      await new Promise((resolve) => setTimeout(resolve, 10));
    }
    received = "";
  }

  try {
    socket.write(
      "GET /first HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n",
    );
    await readText("ok");
    socket.write(
      "GET /upgrade HTTP/1.1\r\n" +
        "Host: localhost\r\n" +
        "Connection: Upgrade\r\n" +
        "Upgrade: test\r\n" +
        "\r\n",
    );
    await upgraded.promise;
    await readText("101 Switching Protocols");
    await readText("detached");
  } finally {
    upgradedSocket?.destroy();
    socket.destroy();
    server.close();
  }
});

// https://github.com/denoland/deno/issues/32311
Deno.test("[node/http] upgrade request can be rejected with non-101 status", async () => {
  const { promise, resolve } = Promise.withResolvers<void>();
  const server = http.createServer((_req, res) => res.end("ok"));

  server.on("upgrade", (_req, socket) => {
    const msg = "HTTP/1.1 401 Unauthorized\r\n" +
      "Connection: close\r\n" +
      "Content-Length: 0\r\n" +
      "\r\n";
    socket.end(msg);
  });

  server.listen(0, async () => {
    const addr = server.address() as { port: number };

    // Upgrade request should get 401
    const res1 = await fetch(`http://127.0.0.1:${addr.port}/`, {
      headers: {
        Connection: "Upgrade",
        Upgrade: "websocket",
        "Sec-WebSocket-Version": "13",
        "Sec-WebSocket-Key": "QUFBQUFBQUFBQUFBQUFBQQ==",
      },
    });
    assertEquals(res1.status, 401);
    await res1.body?.cancel();

    // Normal request should still work
    const res2 = await fetch(`http://127.0.0.1:${addr.port}/`);
    assertEquals(await res2.text(), "ok");

    server.close(() => resolve());
  });

  await promise;
});

// Regression test for https://github.com/denoland/deno/issues/32857
// h2c upgrade requests with an upgrade listener should trigger the
// upgrade event (matching Node.js behavior) and not hang.
Deno.test(
  "[node/http] h2c upgrade does not hang when upgrade listener exists",
  { permissions: { net: true } },
  async () => {
    const { promise, resolve } = Promise.withResolvers<void>();
    let upgradeHandlerCalled = false;
    const server = http.createServer((_req, res) => {
      res.writeHead(200, { "Content-Type": "text/plain" });
      res.end("ok");
    });
    server.on("upgrade", (_req, socket) => {
      // Node.js fires the upgrade event for h2c requests when a
      // listener exists (via shouldUpgradeCallback).
      upgradeHandlerCalled = true;
      socket.end();
    });

    server.listen(0, "127.0.0.1", () => {
      const addr = server.address() as { port: number };
      // Use raw socket to send h2c upgrade request
      const client = net.createConnection(
        { host: "127.0.0.1", port: addr.port },
        () => {
          client.write(
            "GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: Upgrade, HTTP2-Settings\r\nUpgrade: h2c\r\n\r\n",
          );
        },
      );
      client.on("close", () => {
        assert(upgradeHandlerCalled, "upgrade handler should have been called");
        server.close(() => resolve());
      });
    });

    await promise;
  },
);

// Regression test: a long-running request must not trigger a spurious
// ERR_HTTP_REQUEST_TIMEOUT. The ConnectionsList watchdog
// (headersTimeout/requestTimeout) was never told that headers had been
// parsed or that the request had finished, so connections sat with
// headersCompleted=false forever and the watchdog fired ~headersTimeout
// after the connection was accepted, causing Fastify/etc. to return 400.
// https://github.com/denoland/deno/issues/34297
Deno.test(
  "[node/http] long-running request does not trigger spurious headersTimeout",
  async () => {
    const { promise, resolve, reject } = Promise.withResolvers<void>();

    const server = http.createServer({
      headersTimeout: 300,
      connectionsCheckingInterval: 50,
    }, (_req, res) => {
      // Sleep longer than headersTimeout to verify the watchdog doesn't
      // fire mid-request even though headers have already been parsed.
      setTimeout(() => res.end("done"), 800);
    });

    server.on("clientError", (err: Error & { code?: string }) => {
      reject(new Error(`unexpected clientError: ${err.code ?? err.message}`));
    });

    server.listen(0, () => {
      const port = (server.address() as AddressInfo).port;
      const req = http.request(
        { port, host: "127.0.0.1", path: "/" },
        (res) => {
          let data = "";
          res.on("data", (d) => data += d);
          res.on("end", () => {
            assertEquals(res.statusCode, 200);
            assertEquals(data, "done");
            server.close(() => resolve());
          });
        },
      );
      req.on("error", reject);
      req.end();
    });

    await promise;
  },
);

// Regression test: requestTimeout only covers receiving the full request from
// the client, not the response lifetime. A response that streams for longer
// than requestTimeout (SSE/proxy) must not be aborted, mirroring Node which
// stops the requestTimeout clock once the request message is fully received.
// Previously the ConnectionsList watchdog kept firing requestTimeout against
// the active entry until the response finished, killing long-lived streams.
// https://github.com/denoland/deno/issues/35289
Deno.test(
  "[node/http] streaming response does not trigger spurious requestTimeout",
  async () => {
    const { promise, resolve, reject } = Promise.withResolvers<void>();

    const writeCount = 20;
    const writeInterval = 50;
    // requestTimeout is far shorter than the total streaming duration
    // (20 * 50ms = ~1s) so the bug would abort the response mid-stream.
    const server = http.createServer({
      requestTimeout: 300,
      connectionsCheckingInterval: 50,
    }, (_req, res) => {
      res.writeHead(200, { "content-type": "text/plain" });
      let n = 0;
      const interval = setInterval(() => {
        n++;
        res.write(`chunk ${n}\n`);
        if (n === writeCount) {
          clearInterval(interval);
          res.end("done\n");
        }
      }, writeInterval);
      res.on("close", () => clearInterval(interval));
    });

    server.on("clientError", (err: Error & { code?: string }) => {
      reject(new Error(`unexpected clientError: ${err.code ?? err.message}`));
    });

    server.listen(0, () => {
      const port = (server.address() as AddressInfo).port;
      const req = http.get({ port, host: "127.0.0.1", path: "/" }, (res) => {
        let data = "";
        res.setEncoding("utf8");
        res.on("data", (d) => data += d);
        res.on("aborted", () => reject(new Error("response was aborted")));
        res.on("end", () => {
          assertEquals(res.statusCode, 200);
          // All chunks plus the final "done" line must have arrived intact.
          assertEquals(
            data.trim().split("\n").filter(Boolean).length,
            writeCount + 1,
          );
          assertStringIncludes(data, "done");
          server.close(() => resolve());
        });
      });
      req.on("error", reject);
    });

    await promise;
  },
);

// Regression test for pipelined requests: when request 2's response finishes
// after request 1's, resOnFinish for request 1 must not delete the active
// ConnectionsList entry that now belongs to request 2 (otherwise
// headersTimeout/requestTimeout silently stop covering request 2).
// Verifies both pipelined responses succeed without spurious clientError.
Deno.test(
  "[node/http] pipelined requests don't drop timeout tracking",
  async () => {
    const { promise, resolve, reject } = Promise.withResolvers<void>();

    let req1Finished = false;
    let req2Finished = false;
    const server = http.createServer({
      headersTimeout: 2000,
      requestTimeout: 2000,
      connectionsCheckingInterval: 50,
    }, (req, res) => {
      if (req.url === "/1") {
        // Finish first response quickly so resOnFinish for req1 runs while
        // request 2 may still be in flight on the parser.
        setTimeout(() => res.end("first"), 50);
      } else {
        // Hold second response until well after the first has finished so
        // the pipelined entry-swap window is exercised.
        setTimeout(() => res.end("second"), 300);
      }
    });

    server.on("clientError", (err: Error & { code?: string }) => {
      reject(new Error(`unexpected clientError: ${err.code ?? err.message}`));
    });

    server.listen(0, async () => {
      try {
        const port = (server.address() as AddressInfo).port;
        const conn = await Deno.connect({ port, hostname: "127.0.0.1" });
        const encoder = new TextEncoder();
        await conn.write(encoder.encode(
          "GET /1 HTTP/1.1\r\nHost: localhost\r\n\r\n" +
            "GET /2 HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
        ));
        const decoder = new TextDecoder();
        const buf = new Uint8Array(8192);
        let received = "";
        while (true) {
          const n = await conn.read(buf);
          if (n === null) break;
          received += decoder.decode(buf.subarray(0, n));
        }
        try {
          conn.close();
        } catch { /* already closed */ }
        assertStringIncludes(received, "first");
        assertStringIncludes(received, "second");
        req1Finished = true;
        req2Finished = true;
        server.close(() => resolve());
      } catch (e) {
        reject(e);
      }
    });

    await promise;
    assertEquals(req1Finished, true);
    assertEquals(req2Finished, true);
  },
);

// Regression test: oversized headers must trigger HPE_HEADER_OVERFLOW on the
// server's clientError event, and the default handler should respond with 431.
// Previously maxHeaderSize was tracked but never enforced.
// https://github.com/denoland/deno/issues/33060
Deno.test(
  "[node/http] server emits HPE_HEADER_OVERFLOW for oversized headers",
  async () => {
    const { promise, resolve } = Promise.withResolvers<void>();

    const server = http.createServer((_req, res) => {
      res.end("should not reach");
    });

    let gotClientError = false;
    server.on("clientError", (err: Error & { code?: string }, socket) => {
      gotClientError = true;
      assertEquals(err.code, "HPE_HEADER_OVERFLOW");
      assertStringIncludes(err.message, "Header overflow");
      if (socket.writable) {
        socket.end(
          "HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n",
        );
      }
    });

    server.listen(0, () => {
      const port = (server.address() as AddressInfo).port;
      // Send a request with a header exceeding the 16KB default limit
      const hugeHeader = "x".repeat(16384 + 1);
      const sock = net.createConnection(port, "127.0.0.1", () => {
        sock.write(
          `GET / HTTP/1.1\r\nHost: localhost\r\nX-Huge: ${hugeHeader}\r\n\r\n`,
        );
      });
      sock.on("data", () => {});
      sock.on("close", () => {
        assert(gotClientError, "clientError should have been emitted");
        server.close(() => resolve());
      });
      sock.on("error", () => {
        server.close(() => resolve());
      });
    });

    await promise;
  },
);

// Boundary test: header_nread tracks bytes from the URL, header fields,
// and header values (not the framing \r\n separators). A request that
// pushes header_nread to exactly maxHeaderSize must be rejected (>=).
Deno.test(
  "[node/http] header overflow boundary: exactly maxHeaderSize is rejected",
  async () => {
    const { promise, resolve } = Promise.withResolvers<void>();
    // Use a small limit so we can construct precise test cases.
    // header_nread counts: URL path + header field names + header values
    // (NOT the "GET ", " HTTP/1.1\r\n", ": ", or "\r\n" framing).
    // For "GET /xx HTTP/1.1\r\nH: V\r\n\r\n":
    //   header_nread = len("/xx") + len("H") + len("V") = 3 + 1 + 1 = 5
    const LIMIT = 100;

    const server = http.createServer({
      maxHeaderSize: LIMIT,
    }, (_req, res) => {
      res.end("should not reach");
    });

    let gotClientError = false;
    server.on("clientError", (err: Error & { code?: string }, socket) => {
      gotClientError = true;
      assertEquals(err.code, "HPE_HEADER_OVERFLOW");
      if (socket.writable) {
        socket.end("HTTP/1.1 431 Too Large\r\n\r\n");
      }
    });

    server.listen(0, () => {
      const port = (server.address() as AddressInfo).port;
      // header_nread = len("/") + len("Host") + len("x") + len("X-Pad") + len(value)
      //             = 1 + 4 + 1 + 5 + value_len
      //             = 11 + value_len
      // We need 11 + value_len = LIMIT, so value_len = LIMIT - 11
      const valueLen = LIMIT - 11;
      const sock = net.createConnection(port, "127.0.0.1", () => {
        sock.write(
          `GET / HTTP/1.1\r\nHost: x\r\nX-Pad: ${"a".repeat(valueLen)}\r\n\r\n`,
        );
      });
      sock.on("data", () => {});
      sock.on("close", () => {
        assert(
          gotClientError,
          "exactly maxHeaderSize should trigger overflow",
        );
        server.close(() => resolve());
      });
      sock.on("error", () => {
        server.close(() => resolve());
      });
    });

    await promise;
  },
);

Deno.test(
  "[node/http] header overflow boundary: maxHeaderSize - 1 is accepted",
  async () => {
    const { promise, resolve } = Promise.withResolvers<void>();
    const LIMIT = 100;

    const server = http.createServer({
      maxHeaderSize: LIMIT,
    }, (_req, res) => {
      res.end("accepted");
    });

    server.on("clientError", () => {
      throw new Error("should not get clientError for headers under limit");
    });

    server.listen(0, () => {
      const port = (server.address() as AddressInfo).port;
      // header_nread = 11 + value_len; need < LIMIT, so value_len = LIMIT - 12
      const valueLen = LIMIT - 12;
      const sock = net.createConnection(port, "127.0.0.1", () => {
        sock.write(
          `GET / HTTP/1.1\r\nHost: x\r\nX-Pad: ${"a".repeat(valueLen)}\r\n\r\n`,
        );
      });
      let data = "";
      sock.on("data", (d) => {
        data += d.toString();
      });
      sock.on("close", () => {
        assertStringIncludes(data, "accepted");
        server.close(() => resolve());
      });
      sock.on("error", () => {
        server.close(() => resolve());
      });
    });

    await promise;
  },
);

// Regression test: socket.write() + socket.end() in an upgrade handler
// must not crash. Previously, llhttp_finish() was called without setting
// up the ExecuteContext, causing a null pointer dereference when the
// client-side HTTP parser processed the rejected-upgrade response.
// https://github.com/denoland/deno/issues/28654
Deno.test(
  "[node/http] upgrade rejection via socket.write + socket.end does not crash",
  async () => {
    const { promise, resolve } = Promise.withResolvers<void>();

    const server = http.createServer();

    server.on("upgrade", (_req, socket, _head) => {
      socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
      socket.end();
    });

    server.listen(0, () => {
      const port = (server.address() as AddressInfo).port;

      const req = http.request({
        port,
        host: "127.0.0.1",
        path: "/",
        headers: {
          Connection: "Upgrade",
          Upgrade: "websocket",
          "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
          "Sec-WebSocket-Version": "13",
        },
      });

      req.on("response", (res) => {
        assertEquals(res.statusCode, 404);
        req.destroy();
        server.close(() => resolve());
      });

      req.on("error", () => {
        // Connection reset is acceptable
        server.close(() => resolve());
      });

      req.end();
    });

    await promise;
  },
);

// Regression test: a `node:http` IncomingMessage used as the body of a
// `Request` must produce a byte `ReadableStream`, so that
// `getReader({ mode: "byob" })` works.
// https://github.com/denoland/deno/issues/33392
Deno.test(
  "[node/http] IncomingMessage as Request body supports BYOB reader",
  async () => {
    const { promise, resolve } = Promise.withResolvers<void>();

    const server = http.createServer(async (req, res) => {
      const request = new Request("http://localhost/", {
        method: req.method,
        headers: req.headers as HeadersInit,
        body: req as unknown as BodyInit,
        duplex: "half",
        // deno-lint-ignore no-explicit-any
      } as any);

      const reader = request.body!.getReader({ mode: "byob" });
      const buf = new Uint8Array(32);
      const { value, done } = await reader.read(buf);
      assertEquals(done, false);
      assertEquals(new TextDecoder().decode(value), "hello world");

      res.end("OK");
      server.close(() => resolve());
    });

    server.listen(0, async () => {
      const port = (server.address() as AddressInfo).port;
      const res = await fetch(`http://localhost:${port}/`, {
        method: "POST",
        body: "hello world",
      });
      assertEquals(await res.text(), "OK");
    });

    await promise;
  },
);

// https://github.com/denoland/deno/issues/33567
Deno.test(
  "[node/http] cancelling Readable.toWeb(req) does not destroy the socket",
  async () => {
    const { Readable } = await import("node:stream");
    const { promise, resolve, reject } = Promise.withResolvers<void>();

    const server = http.createServer(async (req, res) => {
      try {
        const body = Readable.toWeb(req) as ReadableStream<Uint8Array>;
        const reader = body.getReader();
        await reader.read();
        await reader.cancel();

        await new Promise((r) => setTimeout(r, 50));

        res.end("OK");
      } catch (e) {
        reject(e);
      }
    });

    server.listen(0, async () => {
      try {
        const port = (server.address() as AddressInfo).port;
        const pendingTimers = new Set<NodeJS.Timeout>();
        const res = await fetch(`http://127.0.0.1:${port}/`, {
          method: "POST",
          duplex: "half",
          body: new ReadableStream({
            async pull(controller) {
              await new Promise<void>((r) => {
                const t = setTimeout(() => {
                  pendingTimers.delete(t);
                  r();
                }, 50);
                pendingTimers.add(t);
              });
              controller.enqueue(new TextEncoder().encode("hello"));
            },
            cancel() {
              for (const t of pendingTimers) clearTimeout(t);
              pendingTimers.clear();
            },
          }),
        } as RequestInit);

        assertEquals(res.status, 200);
        assertEquals(await res.text(), "OK");
      } finally {
        server.close(() => resolve());
      }
    });

    await promise;
  },
);

// deno-lint-ignore no-explicit-any
type ProxyAgentLike = any;
// deno-lint-ignore no-explicit-any
type HttpWithProxy = any;

Deno.test("[node/http] setGlobalProxyFromEnv validates input", () => {
  for (const bad of [42, "string", null, [], true]) {
    let err: { code?: string } | undefined;
    try {
      (http as HttpWithProxy).setGlobalProxyFromEnv(bad);
    } catch (e) {
      err = e as { code?: string };
    }
    assert(err, `expected throw for ${typeof bad}`);
    assertEquals(err!.code, "ERR_INVALID_ARG_TYPE");
  }
});

Deno.test("[node/http] setGlobalProxyFromEnv rejects malformed proxy URLs", () => {
  for (
    const cfg of [{ http_proxy: "not a url" }, { https_proxy: "not a url" }]
  ) {
    let err: { code?: string } | undefined;
    try {
      (http as HttpWithProxy).setGlobalProxyFromEnv(cfg);
    } catch (e) {
      err = e as { code?: string };
    }
    assert(err);
    assertEquals(err!.code, "ERR_PROXY_INVALID_CONFIG");
  }
});

Deno.test("[node/http] setGlobalProxyFromEnv returns a restore function", () => {
  const restore = (http as HttpWithProxy).setGlobalProxyFromEnv({
    http_proxy: "http://127.0.0.1:9999",
  });
  assertEquals(typeof restore, "function");
  restore();
  // calling twice is a no-op
  restore();
});

Deno.test("[node/http] Agent proxyEnv rejects CRLF-injected proxy URLs", () => {
  for (
    const proxyUrl of [
      "http://user\r:pass@proxy.example.com:8080",
      "http://user\n:pass@proxy.example.com:8080",
      "http://user:pass\r@proxy.example.com:8080",
      "http://user:pass\n@proxy.example.com:8080",
      "http://user\r\nHost: example.com:pass@proxy.example.com:8080",
    ]
  ) {
    let err: { code?: string } | undefined;
    try {
      new http.Agent({
        proxyEnv: { HTTP_PROXY: proxyUrl },
      } as ProxyAgentLike);
    } catch (e) {
      err = e as { code?: string };
    }
    assert(err, `expected throw for ${JSON.stringify(proxyUrl)}`);
    assertEquals(err!.code, "ERR_PROXY_INVALID_CONFIG");
  }
});

Deno.test(
  "[node/http] http.request through HTTP_PROXY rewrites to absolute URL",
  async () => {
    // Verifies the proxy receives the full URL form (GET http://target/path)
    // and a Proxy-Connection header, then forwards the body back.
    const { promise, resolve, reject } = Promise.withResolvers<void>();
    const proxy = http.createServer((req, res) => {
      try {
        assertEquals(req.method, "GET");
        assert(req.url!.startsWith("http://"));
        assertEquals(req.headers["proxy-connection"], "keep-alive");
        assertEquals(req.headers["connection"], "keep-alive");
      } catch (e) {
        reject(e);
        res.statusCode = 500;
        res.end("test-fail");
        return;
      }
      res.end("via-proxy");
    });
    proxy.listen(0, () => {
      const proxyPort = (proxy.address() as AddressInfo).port;
      // unreachable target - the proxy intercepts and short-circuits.
      const req = http.request({
        hostname: "127.0.0.1",
        port: 1,
        path: "/foo",
        agent: new http.Agent({
          keepAlive: true,
          proxyEnv: {
            HTTP_PROXY: `http://127.0.0.1:${proxyPort}`,
          },
        } as ProxyAgentLike),
      }, (res) => {
        const chunks: Buffer[] = [];
        res.on("data", (c) => chunks.push(c));
        res.on("end", () => {
          try {
            assertEquals(Buffer.concat(chunks).toString(), "via-proxy");
            proxy.close();
            resolve();
          } catch (e) {
            reject(e);
          }
        });
      });
      req.on("error", reject);
      req.end();
    });
    await promise;
  },
);

Deno.test(
  "[node/http] NO_PROXY bypasses configured HTTP_PROXY",
  async () => {
    // If NO_PROXY matches the target, the request should hit the origin
    // directly rather than the configured (and unreachable) proxy.
    const { promise, resolve, reject } = Promise.withResolvers<void>();
    const origin = http.createServer((_req, res) => res.end("direct"));
    origin.listen(0, () => {
      const port = (origin.address() as AddressInfo).port;
      const req = http.request({
        hostname: "127.0.0.1",
        port,
        path: "/",
        agent: new http.Agent({
          proxyEnv: {
            HTTP_PROXY: "http://10.255.255.1:1",
            NO_PROXY: "127.0.0.1",
          },
        } as ProxyAgentLike),
      }, (res) => {
        const chunks: Buffer[] = [];
        res.on("data", (c) => chunks.push(c));
        res.on("end", () => {
          try {
            assertEquals(Buffer.concat(chunks).toString(), "direct");
            origin.close();
            resolve();
          } catch (e) {
            reject(e);
          }
        });
      });
      req.on("error", reject);
      req.end();
    });
    await promise;
  },
);
