/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
tests/unit_node/http_test.ts
3 789 строк
108 KB
Nathan Whitaker
fix(node): retain consumed HTTP stream wrapper (#36254)
24 июл 2026, 01:43
Не верифицирован
24 июл 2026, 01:43
562ff5c
Код
Авторство
О чём код?
// 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; }, );