From 6e3d33777ae9bc3eb415fe581687ed7a2e383404 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Wed, 11 Feb 2026 01:23:19 +0800 Subject: [PATCH 01/16] refactor: simplify code --- src/middleware/web-outgoing.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/middleware/web-outgoing.ts b/src/middleware/web-outgoing.ts index 168de65..d1b17d9 100644 --- a/src/middleware/web-outgoing.ts +++ b/src/middleware/web-outgoing.ts @@ -116,13 +116,12 @@ export const writeHeaders = defineProxyOutgoingMiddleware((req, res, proxyRes, o */ export const writeStatusCode = defineProxyOutgoingMiddleware((req, res, proxyRes) => { // From Node.js docs: response.writeHead(statusCode[, statusMessage][, headers]) + + // @ts-expect-error + res.statusCode = proxyRes.statusCode; + if (proxyRes.statusMessage) { - // @ts-expect-error - res.statusCode = proxyRes.statusCode; res.statusMessage = proxyRes.statusMessage; - } else { - // @ts-expect-error - res.statusCode = proxyRes.statusCode; } }); From 3d54aaf066f0d5d02e8b086987111169477025ed Mon Sep 17 00:00:00 2001 From: SukkaW Date: Wed, 11 Feb 2026 01:24:22 +0800 Subject: [PATCH 02/16] feat: handle connection header --- src/middleware/web-outgoing.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/middleware/web-outgoing.ts b/src/middleware/web-outgoing.ts index d1b17d9..586c2cc 100644 --- a/src/middleware/web-outgoing.ts +++ b/src/middleware/web-outgoing.ts @@ -15,12 +15,17 @@ export const removeChunked = defineProxyOutgoingMiddleware((req, res, proxyRes) /** * If is a HTTP 1.0 request, set the correct connection header * or if connection header not present, then use `keep-alive` + * + * If is a HTTP/2 request, remove connection header no matter what, + * this avoids sending connection header to the underlying http2 client */ export const setConnection = defineProxyOutgoingMiddleware((req, res, proxyRes) => { if (req.httpVersion === "1.0") { proxyRes.headers.connection = req.headers.connection || "close"; - } else if (req.httpVersion !== "2.0" && !proxyRes.headers.connection) { + } else if (req.httpVersionMajor < 2 && !proxyRes.headers.connection) { proxyRes.headers.connection = req.headers.connection || "keep-alive"; + } else if (req.httpVersionMajor >= 2) { + delete proxyRes.headers.connection; } }); From fe5c360763cf83518466455257959bb2aa265c32 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Wed, 11 Feb 2026 01:29:09 +0800 Subject: [PATCH 03/16] feat: ignore conflicting http/2 pseudo headers --- src/_utils.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/_utils.ts b/src/_utils.ts index f378b67..7208eda 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -11,6 +11,20 @@ const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i; */ export const isSSL = /^https|wss/; +/** + * Node.js HTTP/2 accepts pseudo headers and it may conflict + * with request options. + * + * Let's just blacklist those potential conflicting pseudo + * headers. + */ +const HTTP2_HEADER_BLACKLIST = [ + ':method', + ':path', + ':scheme', + ':authority', +] + /** * Copies the right headers from `options` and `req` to * `outgoing` which is then used to fire the proxied @@ -68,6 +82,13 @@ export function setupOutgoing( outgoing.headers = { ...outgoing.headers, ...options.headers }; } + if (req.httpVersionMajor > 1) { + // ignore potential conflicting HTTP/2 pseudo headers + for (const header of HTTP2_HEADER_BLACKLIST) { + delete outgoing.headers[header]; + } + } + if (options.auth) { outgoing.auth = options.auth; } From 29bb3a07cee6d8f5e617962ac2dd828e3d9adef9 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Thu, 12 Feb 2026 18:25:40 +0800 Subject: [PATCH 04/16] feat: ignore statusMessage on HTTP/2 --- src/middleware/web-outgoing.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/middleware/web-outgoing.ts b/src/middleware/web-outgoing.ts index 586c2cc..565fff2 100644 --- a/src/middleware/web-outgoing.ts +++ b/src/middleware/web-outgoing.ts @@ -125,7 +125,11 @@ export const writeStatusCode = defineProxyOutgoingMiddleware((req, res, proxyRes // @ts-expect-error res.statusCode = proxyRes.statusCode; - if (proxyRes.statusMessage) { + if ( + proxyRes.statusMessage && + // Only HTTP/1.0 and HTTP/1.1 support statusMessage + req.httpVersionMajor < 2 + ) { res.statusMessage = proxyRes.statusMessage; } }); From 4f850588cd29e329db8eecee74be74848a409443 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Thu, 12 Feb 2026 17:56:53 +0800 Subject: [PATCH 05/16] feat: implement HTTP/2 listener --- src/_utils.ts | 16 ++++---- src/middleware/_utils.ts | 17 +++++---- src/server.ts | 56 +++++++++++++++++++--------- src/types.ts | 6 ++- test/middleware/web-outgoing.test.ts | 36 ++++++++++++++++++ 5 files changed, 95 insertions(+), 36 deletions(-) diff --git a/src/_utils.ts b/src/_utils.ts index 7208eda..0cbc177 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -3,6 +3,7 @@ import httpsNative from "node:https"; import net from "node:net"; import type tls from "node:tls"; import type { ProxyAddr, ProxyServerOptions, ProxyTarget, ProxyTargetDetailed } from "./types.ts"; +import type { Http2ServerRequest } from "node:http2"; const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i; @@ -18,12 +19,7 @@ export const isSSL = /^https|wss/; * Let's just blacklist those potential conflicting pseudo * headers. */ -const HTTP2_HEADER_BLACKLIST = [ - ':method', - ':path', - ':scheme', - ':authority', -] +const HTTP2_HEADER_BLACKLIST = [":method", ":path", ":scheme", ":authority"]; /** * Copies the right headers from `options` and `req` to @@ -52,7 +48,7 @@ export function setupOutgoing( ca?: string; method?: string; }, - req: httpNative.IncomingMessage, + req: httpNative.IncomingMessage | Http2ServerRequest, forward?: "forward" | "target", ): httpNative.RequestOptions | httpsNative.RequestOptions { outgoing.port = @@ -202,7 +198,7 @@ export function setupSocket(socket: net.Socket): net.Socket { * * @api private */ -export function getPort(req: httpNative.IncomingMessage): string { +export function getPort(req: httpNative.IncomingMessage | Http2ServerRequest): string { const res = req.headers.host ? req.headers.host.match(/:(\d+)/) : ""; if (res) { return res[1]!; @@ -219,7 +215,9 @@ export function getPort(req: httpNative.IncomingMessage): string { * * @api private */ -export function hasEncryptedConnection(req: httpNative.IncomingMessage): boolean { +export function hasEncryptedConnection( + req: httpNative.IncomingMessage | Http2ServerRequest, +): boolean { return Boolean( // req.connection.pair probably does not exist anymore (req.connection as tls.TLSSocket).encrypted || (req.connection as any).pair, diff --git a/src/middleware/_utils.ts b/src/middleware/_utils.ts index 3704c58..66b5ac6 100644 --- a/src/middleware/_utils.ts +++ b/src/middleware/_utils.ts @@ -2,25 +2,26 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import type { Socket } from "node:net"; import type { ProxyServer } from "../server.ts"; import type { ProxyServerOptions, ProxyTargetDetailed } from "../types.ts"; +import type { Http2ServerRequest, Http2ServerResponse } from "node:http2"; export type ResOfType = T extends "ws" ? T extends "web" - ? ServerResponse | Socket + ? ServerResponse | Http2ServerResponse | Socket : Socket : T extends "web" - ? ServerResponse + ? ServerResponse | Http2ServerResponse : never; -export type ProxyMiddleware = ( - req: IncomingMessage, +export type ProxyMiddleware = ( + req: IncomingMessage | Http2ServerRequest, res: T, opts: ProxyServerOptions & { target: URL | ProxyTargetDetailed; forward: URL; }, - server: ProxyServer, + server: ProxyServer, head?: Buffer, - callback?: (err: any, req: IncomingMessage, socket: T, url?: any) => void, + callback?: (err: any, req: IncomingMessage | Http2ServerRequest, socket: T, url?: any) => void, ) => void | true; export function defineProxyMiddleware( @@ -30,8 +31,8 @@ export function defineProxyMiddleware { error: [err: Error, req?: Req, res?: Res | net.Socket, target?: URL | ProxyTarget]; start: [req: Req, res: Res, target: URL | ProxyTarget]; @@ -32,22 +33,31 @@ export interface ProxyServerEventMap< // eslint-disable-next-line unicorn/prefer-event-target export class ProxyServer< - Req extends http.IncomingMessage = http.IncomingMessage, - Res extends http.ServerResponse = http.ServerResponse, + Req extends http.IncomingMessage | http2.Http2ServerRequest = http.IncomingMessage, + Res extends http.ServerResponse | http2.Http2ServerResponse = http.ServerResponse, > extends EventEmitter> { - private _server?: http.Server | https.Server; + // we use http2.Http2Server to handle HTTP/1.1 HTTPS as well (with allowHTTP1 enabled) + private _server?: http.Server | http2.Http2SecureServer; _webPasses: ProxyMiddleware[] = [...webIncomingMiddleware]; _wsPasses: ProxyMiddleware[] = [...websocketIncomingMiddleware]; options: ProxyServerOptions; - web: ( - req: http.IncomingMessage, - res: http.ServerResponse, - opts?: ProxyServerOptions, - head?: any, - ) => Promise; + web: { + ( + req: http.IncomingMessage, + res: http.ServerResponse, + opts?: ProxyServerOptions, + head?: any, + ): Promise; + ( + req: http2.Http2ServerRequest, + res: http2.Http2ServerResponse, + opts?: ProxyServerOptions, + head?: any, + ): Promise; + }; ws: ( req: http.IncomingMessage, @@ -76,17 +86,27 @@ export class ProxyServer< * @param hostname - The hostname to listen on */ listen(port: number, hostname?: string) { - const closure = (req: http.IncomingMessage, res: http.ServerResponse) => { - this.web(req, res); + interface ListenerCallback { + ( + req: http.IncomingMessage | http2.Http2ServerRequest, + res: http.ServerResponse | http2.Http2ServerResponse, + ): Promise; + } + + const closure: ListenerCallback = (req, res) => { + return this.web(req as any, res as any); }; - this._server = this.options.ssl - ? https.createServer(this.options.ssl, closure) - : http.createServer(closure); + if (this.options.http2) { + this._server = http2.createSecureServer({ ...this.options.ssl, allowHTTP1: true }, closure); + } else if (this.options.ssl) { + this._server = https.createServer(this.options.ssl, closure); + } else { + this._server = http.createServer(closure); + } if (this.options.ws) { this._server.on("upgrade", (req, socket, head) => { - // @ts-expect-error this.ws(req, socket, head).catch(() => {}); }); } @@ -185,7 +205,7 @@ function _createProxyFn(type: Type, server: ProxyServ type Res = ResOfType; return function ( this: ProxyServer, - req: http.IncomingMessage, + req: http.IncomingMessage | http2.Http2ServerRequest, res: Res, opts?: ProxyServerOptions, head?: any, diff --git a/src/types.ts b/src/types.ts index 5146b00..9bf4f16 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,7 +29,11 @@ export interface ProxyServerOptions { forward?: ProxyTarget; /** Object to be passed to http(s).request. */ agent?: any; - /** Object to be passed to https.createServer(). */ + /** Enable HTTP/2 listener, default is `false` */ + http2?: boolean; + /** Object to be passed to https.createServer() + * or http2.createSecureServer() if the `http2` option is enabled + */ ssl?: any; /** If you want to proxy websockets. */ ws?: boolean; diff --git a/test/middleware/web-outgoing.test.ts b/test/middleware/web-outgoing.test.ts index 593de73..0c5905b 100644 --- a/test/middleware/web-outgoing.test.ts +++ b/test/middleware/web-outgoing.test.ts @@ -274,6 +274,42 @@ describe("middleware:web-outgoing", () => { expect(proxyRes.headers.connection).to.eql("hola"); }); + it("set the right connection (HTTP/1.1) - req.connection", () => { + const proxyRes = { headers: {} as any }; + webOutgoing.setConnection( + { + httpVersion: "1.0", + httpVersionMajor: 1, + headers: { + connection: "hola", + }, + } as any, + {} as any, + proxyRes as any, + {} as any, + ); + + expect(proxyRes.headers.connection).to.eql("hola"); + }); + + it("set the right connection (HTTP/2) - req.connection", () => { + const proxyRes = { headers: {} as any }; + webOutgoing.setConnection( + { + httpVersion: "2.0", + httpVersionMajor: 2, + headers: { + connection: "hola", + }, + } as any, + {} as any, + proxyRes as any, + {} as any, + ); + + expect(proxyRes.headers.connection).to.eql(undefined); + }); + it("set the right connection - `keep-alive`", () => { const proxyRes = stubIncomingMessage({ headers: {} }); webOutgoing.setConnection( From 4e514dfd523e07882965511321cecbac0e99e7bc Mon Sep 17 00:00:00 2001 From: SukkaW Date: Thu, 12 Feb 2026 18:48:14 +0800 Subject: [PATCH 06/16] refactor: proper determine connection is encrypted --- src/_utils.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/_utils.ts b/src/_utils.ts index 0cbc177..527a2b2 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -218,10 +218,26 @@ export function getPort(req: httpNative.IncomingMessage | Http2ServerRequest): s export function hasEncryptedConnection( req: httpNative.IncomingMessage | Http2ServerRequest, ): boolean { - return Boolean( - // req.connection.pair probably does not exist anymore - (req.connection as tls.TLSSocket).encrypted || (req.connection as any).pair, - ); + // req.connection.pair probably does not exist anymore + if ("connection" in req) { + if ("encrypted" in req.connection) { + return req.connection.encrypted; + } + if ("pair" in req.connection) { + return !!req.connection.pair; + } + } + // Since Node.js v16 we now have req.socket + if ("socket" in req) { + if ("encrypted" in req.socket) { + return req.socket.encrypted; + } + if ("pair" in req.socket) { + return !!req.socket.pair; + } + } + + return false; } /** From 811a25d0b55bea74a33dc1bea92b66c32a63002f Mon Sep 17 00:00:00 2001 From: SukkaW Date: Thu, 12 Feb 2026 19:28:02 +0800 Subject: [PATCH 07/16] refactor: handle `:authority` and `transfer-encoding` --- src/_utils.ts | 8 +++++++- src/middleware/web-incoming.ts | 3 ++- src/middleware/web-outgoing.ts | 11 ++++++++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/_utils.ts b/src/_utils.ts index 527a2b2..e1a4ed5 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -74,6 +74,11 @@ export function setupOutgoing( outgoing.method = options.method || req.method; outgoing.headers = { ...req.headers }; + // before clean up HTTP/2 blacklist header, we might wanna override host first + if (req.headers?.[":authority"]) { + outgoing.headers.host = req.headers[":authority"] as string; + } + if (options.headers) { outgoing.headers = { ...outgoing.headers, ...options.headers }; } @@ -199,7 +204,8 @@ export function setupSocket(socket: net.Socket): net.Socket { * @api private */ export function getPort(req: httpNative.IncomingMessage | Http2ServerRequest): string { - const res = req.headers.host ? req.headers.host.match(/:(\d+)/) : ""; + const hostHeader = (req.headers[":authority"] as string | undefined) || req.headers.host; + const res = hostHeader ? hostHeader.match(/:(\d+)/) : ""; if (res) { return res[1]!; } diff --git a/src/middleware/web-incoming.ts b/src/middleware/web-incoming.ts index b82a142..6b38539 100644 --- a/src/middleware/web-incoming.ts +++ b/src/middleware/web-incoming.ts @@ -52,7 +52,8 @@ export const XHeaders = defineProxyMiddleware((req, res, options) => { values[header]; } - req.headers["x-forwarded-host"] = req.headers["x-forwarded-host"] || req.headers.host || ""; + req.headers["x-forwarded-host"] = + req.headers["x-forwarded-host"] || req.headers[":authority"] || req.headers.host || ""; }); /** diff --git a/src/middleware/web-outgoing.ts b/src/middleware/web-outgoing.ts index 565fff2..b7aae38 100644 --- a/src/middleware/web-outgoing.ts +++ b/src/middleware/web-outgoing.ts @@ -7,7 +7,8 @@ const redirectRegex = /^201|30([1278])$/; * If is a HTTP 1.0 request, remove chunk headers */ export const removeChunked = defineProxyOutgoingMiddleware((req, res, proxyRes) => { - if (req.httpVersion === "1.0") { + // HTTP/1.0 and HTTP/2 do not have transfer-encoding: chunked + if (req.httpVersion !== "1.1") { delete proxyRes.headers["transfer-encoding"]; } }); @@ -47,8 +48,12 @@ export const setRedirectHostRewrite = defineProxyOutgoingMiddleware( if (options.hostRewrite) { u.host = options.hostRewrite; - } else if (options.autoRewrite && req.headers.host) { - u.host = req.headers.host; + } else if (options.autoRewrite) { + if (req.headers[":authority"]) { + u.host = req.headers[":authority"] as string; + } else if (req.headers.host) { + u.host = req.headers.host; + } } if (options.protocolRewrite) { u.protocol = options.protocolRewrite; From 45d3a020ecbfd6333dc02eb82ff364603050b2b0 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Thu, 12 Feb 2026 19:48:27 +0800 Subject: [PATCH 08/16] test: add http2 listener test --- package.json | 1 + pnpm-lock.yaml | 9 +++ test/http2-proxy.test.ts | 144 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 test/http2-proxy.test.ts diff --git a/package.json b/package.json index 8139186..696e1a8 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "sse": "^0.0.8", "typescript": "^5.9.3", "unbuild": "^3.6.1", + "undici": "^7.21.0", "vitest": "^4.0.18", "ws": "^8.19.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e120945..6050230 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: unbuild: specifier: ^3.6.1 version: 3.6.1(typescript@5.9.3) + undici: + specifier: ^7.21.0 + version: 7.21.0 vitest: specifier: ^4.0.18 version: 4.0.18(@types/node@25.2.3)(jiti@2.6.1) @@ -2439,6 +2442,10 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@7.21.0: + resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==} + engines: {node: '>=20.18.1'} + unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -4967,6 +4974,8 @@ snapshots: undici-types@7.16.0: {} + undici@7.21.0: {} + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 diff --git a/test/http2-proxy.test.ts b/test/http2-proxy.test.ts new file mode 100644 index 0000000..1b8b92d --- /dev/null +++ b/test/http2-proxy.test.ts @@ -0,0 +1,144 @@ +import * as http from "node:http"; +import * as https from "node:https"; +import * as httpProxy from "../src"; +import * as path from "node:path"; +import * as fs from "node:fs"; +import { describe, it, expect, beforeAll, afterAll } from "vitest"; + +import { Agent, fetch } from "undici"; + +let initialPort = 4096; +const getPort = () => initialPort++; + +const http1Agent = new Agent({ + allowH2: false, + connect: { + // Allow to use SSL self signed + rejectUnauthorized: false, + }, +}); +const http2Agent = new Agent({ + allowH2: true, + connect: { + // Allow to use SSL self signed + rejectUnauthorized: false, + }, +}); + +describe("http/2 listener", () => { + describe("http2 -> http", () => { + const httpPort = getPort(); + const proxyPort = getPort(); + + const source = http + .createServer((_req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.write("hello httpxy\n"); + res.end(); + }) + .listen(httpPort); + + const proxy = httpProxy + .createProxyServer({ + target: { + host: "localhost", + port: httpPort, + }, + ssl: { + key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), + cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), + }, + http2: true, + // Allow to use SSL self signed + secure: false, + }) + .listen(proxyPort); + + it("target http server should be working", async () => { + const r = await ( + await fetch(`http://localhost:${httpPort}`, { dispatcher: http1Agent }) + ).text(); + expect(r).toContain("hello httpxy"); + }); + + it("fetch proxy server over http1", async () => { + const r = await ( + await fetch(`https://localhost:${proxyPort}`, { dispatcher: http1Agent }) + ).text(); + expect(r).toContain("hello httpxy"); + }); + + it("fetch proxy server over http2", async () => { + const resp = await fetch(`https://localhost:${proxyPort}`, { dispatcher: http2Agent }); + const r = await resp.text(); + expect(r).toContain("hello httpxy"); + }); + + afterAll(async () => { + // cleans up + await new Promise((resolve) => proxy.close(resolve)); + source.close(); + }); + }); + + // TODO: fix this test + describe.skip("http2 -> https", () => { + const httpsPort = getPort(); + const proxyPort = getPort(); + + const source = https + .createServer( + { + key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), + cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), + }, + function (req, res) { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("hello httpxy"); + }, + ) + .listen(httpsPort); + + const proxy = httpProxy + .createProxyServer({ + target: { + host: "localhost", + port: httpsPort, + }, + ssl: { + key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), + cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), + }, + http2: true, + // Allow to use SSL self signed + secure: false, + }) + .listen(proxyPort); + + it("target https server should be working", async () => { + const r = await ( + await fetch(`https://localhost:${httpsPort}`, { dispatcher: http1Agent }) + ).text(); + expect(r).toContain("hello httpxy"); + }); + + it("fetch proxy server over http1", async () => { + const r = await ( + await fetch(`https://localhost:${proxyPort}`, { dispatcher: http1Agent }) + ).text(); + expect(r).toContain("hello httpxy"); + }); + + it("fetch proxy server over http2", async () => { + const resp = await fetch(`https://localhost:${proxyPort}`, { dispatcher: http2Agent }); + const r = await resp.text(); + expect(r).toContain("hello httpxy"); + }); + + afterAll(async () => { + // cleans up + await new Promise((resolve) => proxy.close(resolve)); + source.close(); + }); + }); +}); From 9a92849ed756afa20da1d24e50978b2fff8d5a41 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Fri, 20 Feb 2026 18:09:41 +0800 Subject: [PATCH 09/16] refactor: types and test --- src/server.ts | 42 ++++++++++------------------ test/_stubs.ts | 11 ++++++-- test/http2-proxy.test.ts | 2 +- test/middleware/web-outgoing.test.ts | 2 ++ 4 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/server.ts b/src/server.ts index 131bf50..bb2f20a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -44,27 +44,9 @@ export class ProxyServer< options: ProxyServerOptions; - web: { - ( - req: http.IncomingMessage, - res: http.ServerResponse, - opts?: ProxyServerOptions, - head?: any, - ): Promise; - ( - req: http2.Http2ServerRequest, - res: http2.Http2ServerResponse, - opts?: ProxyServerOptions, - head?: any, - ): Promise; - }; + web: (req: Req, res: Res, opts?: ProxyServerOptions, head?: any) => Promise; - ws: ( - req: http.IncomingMessage, - socket: net.Socket, - opts: ProxyServerOptions, - head?: any, - ) => Promise; + ws: (req: Req, socket: net.Socket, opts: ProxyServerOptions, head?: any) => Promise; /** * Creates the proxy server with specified options. @@ -201,12 +183,15 @@ export function createProxyServer(options: ProxyServerOptions = {}) { // --- Internal --- -function _createProxyFn(type: Type, server: ProxyServer) { - type Res = ResOfType; +function _createProxyFn< + Type extends "web" | "ws", + ProxyServerReq extends http.IncomingMessage | http2.Http2ServerRequest, + ProxyServerRes extends http.ServerResponse | http2.Http2ServerResponse, +>(type: Type, server: ProxyServer) { return function ( - this: ProxyServer, - req: http.IncomingMessage | http2.Http2ServerRequest, - res: Res, + this: ProxyServer, + req: ProxyServerReq, + res: ResOfType, opts?: ProxyServerOptions, head?: any, ): Promise { @@ -242,11 +227,14 @@ function _createProxyFn(type: Type, server: ProxyServ req, res, requestOptions as ProxyServerOptions & { target: URL; forward: URL }, - server, + server as ProxyServer< + http.IncomingMessage | http2.Http2ServerRequest, + http.ServerResponse | http2.Http2ServerResponse + >, head, (error) => { if (server.listenerCount("error") > 0) { - server.emit("error", error, req, res); + server.emit("error", error, req, res as ProxyServerRes | net.Socket); _resolve(); } else { _reject(error); diff --git a/test/_stubs.ts b/test/_stubs.ts index 2623a4f..5f1ed01 100644 --- a/test/_stubs.ts +++ b/test/_stubs.ts @@ -22,7 +22,14 @@ export function createOutgoing(): OutgoingOptions { // --- IncomingMessage stubs --- export function stubIncomingMessage(overrides: Record = {}): IncomingMessage { - return { method: "GET", url: "/", headers: {}, ...overrides } as unknown as IncomingMessage; + return { + method: "GET", + url: "/", + headers: {}, + httpVersion: "1.1", + httpVersionMajor: 1, + ...overrides, + } as unknown as IncomingMessage; } // --- ServerResponse stub --- @@ -50,6 +57,6 @@ export function stubMiddlewareOptions(overrides: Record = {}): // --- ProxyServer stub --- -export function stubProxyServer(overrides: Record = {}): ProxyServer { +export function stubProxyServer(overrides: Record = {}): ProxyServer { return overrides as unknown as ProxyServer; } diff --git a/test/http2-proxy.test.ts b/test/http2-proxy.test.ts index 1b8b92d..76a9564 100644 --- a/test/http2-proxy.test.ts +++ b/test/http2-proxy.test.ts @@ -1,6 +1,6 @@ import * as http from "node:http"; import * as https from "node:https"; -import * as httpProxy from "../src"; +import * as httpProxy from "../src/index.ts"; import * as path from "node:path"; import * as fs from "node:fs"; import { describe, it, expect, beforeAll, afterAll } from "vitest"; diff --git a/test/middleware/web-outgoing.test.ts b/test/middleware/web-outgoing.test.ts index 0c5905b..083a0fa 100644 --- a/test/middleware/web-outgoing.test.ts +++ b/test/middleware/web-outgoing.test.ts @@ -330,6 +330,7 @@ describe("middleware:web-outgoing", () => { webOutgoing.setConnection( stubIncomingMessage({ httpVersion: "2.0", + httpVersionMajor: 2, headers: { connection: "namstey" }, }), stubServerResponse(), @@ -345,6 +346,7 @@ describe("middleware:web-outgoing", () => { webOutgoing.setConnection( stubIncomingMessage({ httpVersion: "2.0", + httpVersionMajor: 2, headers: {}, }), stubServerResponse(), From 9d401acaa6154331c9414159dfadb6c245fea148 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Fri, 20 Feb 2026 18:15:57 +0800 Subject: [PATCH 10/16] test: update test to re-use util --- test/http2-proxy.test.ts | 178 ++++++++++++++++++++------------------- test/https-proxy.test.ts | 6 +- 2 files changed, 96 insertions(+), 88 deletions(-) diff --git a/test/http2-proxy.test.ts b/test/http2-proxy.test.ts index 76a9564..37488fc 100644 --- a/test/http2-proxy.test.ts +++ b/test/http2-proxy.test.ts @@ -3,12 +3,11 @@ import * as https from "node:https"; import * as httpProxy from "../src/index.ts"; import * as path from "node:path"; import * as fs from "node:fs"; -import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { describe, it, expect, afterAll } from "vitest"; import { Agent, fetch } from "undici"; - -let initialPort = 4096; -const getPort = () => initialPort++; +import { listenOn, proxyListen } from "./https-proxy.test.ts"; +import { inspect } from "node:util"; const http1Agent = new Agent({ allowH2: false, @@ -26,52 +25,55 @@ const http2Agent = new Agent({ }); describe("http/2 listener", () => { - describe("http2 -> http", () => { - const httpPort = getPort(); - const proxyPort = getPort(); - - const source = http - .createServer((_req, res) => { - res.writeHead(200, { "Content-Type": "text/plain" }); - res.write("hello httpxy\n"); - res.end(); - }) - .listen(httpPort); - - const proxy = httpProxy - .createProxyServer({ - target: { - host: "localhost", - port: httpPort, - }, - ssl: { - key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), - cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), - }, - http2: true, - // Allow to use SSL self signed - secure: false, - }) - .listen(proxyPort); + describe("http2 -> http", async () => { + const source = http.createServer((_req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("Hello from " + sourcePort); + }); + const sourcePort = await listenOn(source); + + const proxy = httpProxy.createProxyServer({ + target: "http://127.0.0.1:" + sourcePort, + ssl: { + key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), + cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), + }, + http2: true, + // Allow to use SSL self signed + secure: false, + }); + const proxyPort = await proxyListen(proxy); it("target http server should be working", async () => { - const r = await ( - await fetch(`http://localhost:${httpPort}`, { dispatcher: http1Agent }) - ).text(); - expect(r).toContain("hello httpxy"); + try { + const r = await ( + await fetch(`http://127.0.0.1:${sourcePort}`, { dispatcher: http1Agent }) + ).text(); + expect(r).to.eql("Hello from " + sourcePort); + } catch (err) { + expect.fail("Failed to fetch target server: " + inspect(err)); + } }); it("fetch proxy server over http1", async () => { - const r = await ( - await fetch(`https://localhost:${proxyPort}`, { dispatcher: http1Agent }) - ).text(); - expect(r).toContain("hello httpxy"); + try { + const r = await ( + await fetch(`https://127.0.0.1:${proxyPort}`, { dispatcher: http1Agent }) + ).text(); + expect(r).to.eql("Hello from " + sourcePort); + } catch (err) { + expect.fail("Failed to fetch target server: " + inspect(err)); + } }); it("fetch proxy server over http2", async () => { - const resp = await fetch(`https://localhost:${proxyPort}`, { dispatcher: http2Agent }); - const r = await resp.text(); - expect(r).toContain("hello httpxy"); + try { + const resp = await fetch(`https://127.0.0.1:${proxyPort}`, { dispatcher: http2Agent }); + const r = await resp.text(); + expect(r).to.eql("Hello from " + sourcePort); + } catch (err) { + expect.fail("Failed to fetch target server: " + inspect(err)); + } }); afterAll(async () => { @@ -81,58 +83,62 @@ describe("http/2 listener", () => { }); }); - // TODO: fix this test - describe.skip("http2 -> https", () => { - const httpsPort = getPort(); - const proxyPort = getPort(); - - const source = https - .createServer( - { - key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), - cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), - }, - function (req, res) { - res.writeHead(200, { "Content-Type": "text/plain" }); - res.end("hello httpxy"); - }, - ) - .listen(httpsPort); - - const proxy = httpProxy - .createProxyServer({ - target: { - host: "localhost", - port: httpsPort, - }, - ssl: { - key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), - cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), - }, - http2: true, - // Allow to use SSL self signed - secure: false, - }) - .listen(proxyPort); + describe("http2 -> https", async () => { + const source = https.createServer( + { + key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), + cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), + ciphers: "AES128-GCM-SHA256", + }, + function (req, res) { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("Hello from " + sourcePort); + }, + ); + const sourcePort = await listenOn(source); + + const proxy = httpProxy.createProxyServer({ + target: "https://127.0.0.1:" + sourcePort, + ssl: { + key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), + cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), + }, + http2: true, + // Allow to use SSL self signed + secure: false, + }); + const proxyPort = await proxyListen(proxy); it("target https server should be working", async () => { - const r = await ( - await fetch(`https://localhost:${httpsPort}`, { dispatcher: http1Agent }) - ).text(); - expect(r).toContain("hello httpxy"); + try { + const r = await ( + await fetch(`https://127.0.0.1:${sourcePort}`, { dispatcher: http1Agent }) + ).text(); + expect(r).to.eql("Hello from " + sourcePort); + } catch (err) { + expect.fail("Failed to fetch target server: " + inspect(err)); + } }); it("fetch proxy server over http1", async () => { - const r = await ( - await fetch(`https://localhost:${proxyPort}`, { dispatcher: http1Agent }) - ).text(); - expect(r).toContain("hello httpxy"); + try { + const r = await ( + await fetch(`https://127.0.0.1:${proxyPort}`, { dispatcher: http1Agent }) + ).text(); + expect(r).to.eql("Hello from " + sourcePort); + } catch (err) { + expect.fail("Failed to fetch target server: " + inspect(err)); + } }); it("fetch proxy server over http2", async () => { - const resp = await fetch(`https://localhost:${proxyPort}`, { dispatcher: http2Agent }); - const r = await resp.text(); - expect(r).toContain("hello httpxy"); + try { + const resp = await fetch(`https://127.0.0.1:${proxyPort}`, { dispatcher: http2Agent }); + const r = await resp.text(); + expect(r).to.eql("Hello from " + sourcePort); + } catch (err) { + expect.fail("Failed to fetch target server: " + inspect(err)); + } }); afterAll(async () => { diff --git a/test/https-proxy.test.ts b/test/https-proxy.test.ts index 71aba33..873b867 100644 --- a/test/https-proxy.test.ts +++ b/test/https-proxy.test.ts @@ -9,7 +9,7 @@ import type { AddressInfo } from "node:net"; // Source: https://github.com/http-party/node-http-proxy/blob/master/test/lib-https-proxy-test.js -function listenOn(server: http.Server | https.Server | net.Server): Promise { +export function listenOn(server: http.Server | https.Server | net.Server): Promise { return new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", () => { @@ -18,7 +18,9 @@ function listenOn(server: http.Server | https.Server | net.Server): Promise): Promise { +export function proxyListen( + proxy: ReturnType, +): Promise { return new Promise((resolve, reject) => { proxy.listen(0, "127.0.0.1"); const server = (proxy as any)._server as net.Server; From 84d4c02e111a4b170492c1f837be47153ae808db Mon Sep 17 00:00:00 2001 From: SukkaW Date: Fri, 20 Feb 2026 18:59:16 +0800 Subject: [PATCH 11/16] test: improve coverage --- src/_utils.ts | 1 - test/middleware/web-outgoing.test.ts | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/_utils.ts b/src/_utils.ts index e1a4ed5..4047aa7 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -1,7 +1,6 @@ import httpNative from "node:http"; import httpsNative from "node:https"; import net from "node:net"; -import type tls from "node:tls"; import type { ProxyAddr, ProxyServerOptions, ProxyTarget, ProxyTargetDetailed } from "./types.ts"; import type { Http2ServerRequest } from "node:http2"; diff --git a/test/middleware/web-outgoing.test.ts b/test/middleware/web-outgoing.test.ts index 083a0fa..77152b8 100644 --- a/test/middleware/web-outgoing.test.ts +++ b/test/middleware/web-outgoing.test.ts @@ -104,6 +104,7 @@ describe("middleware:web-outgoing", () => { describe("rewrites location host with autoRewrite", () => { beforeEach(() => { ctx.options.autoRewrite = true; + delete ctx.req.headers[":authority"]; }); for (const code of [201, 301, 302, 307, 308]) { it("on " + code, () => { @@ -118,6 +119,17 @@ describe("middleware:web-outgoing", () => { }); } + it("with HTTP/2 :authority", () => { + ctx.req.headers[":authority"] = "ext-auto.com"; + webOutgoing.setRedirectHostRewrite( + ctx.req, + stubServerResponse(), + ctx.proxyRes, + ctx.options, + ); + expect(ctx.proxyRes.headers.location).to.eql("http://ext-auto.com/"); + }); + it("not on 200", () => { ctx.proxyRes.statusCode = 200; webOutgoing.setRedirectHostRewrite( From 68a2f02ef36a7711341f5cb84d00c7382fca8e3c Mon Sep 17 00:00:00 2001 From: SukkaW Date: Fri, 20 Feb 2026 19:05:56 +0800 Subject: [PATCH 12/16] chore: add comment --- src/_utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_utils.ts b/src/_utils.ts index 4047aa7..72e505b 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -77,6 +77,7 @@ export function setupOutgoing( if (req.headers?.[":authority"]) { outgoing.headers.host = req.headers[":authority"] as string; } + // host override must happen before composing/merging the final outgoing headers if (options.headers) { outgoing.headers = { ...outgoing.headers, ...options.headers }; From ebfd52dc8bccefc420c383c7c4fb88a3b69dd303 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Tue, 24 Feb 2026 00:17:37 +0800 Subject: [PATCH 13/16] test: fix describe --- test/http2-proxy.test.ts | 96 +++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/test/http2-proxy.test.ts b/test/http2-proxy.test.ts index 37488fc..1259bfd 100644 --- a/test/http2-proxy.test.ts +++ b/test/http2-proxy.test.ts @@ -3,7 +3,7 @@ import * as https from "node:https"; import * as httpProxy from "../src/index.ts"; import * as path from "node:path"; import * as fs from "node:fs"; -import { describe, it, expect, afterAll } from "vitest"; +import { describe, it, expect, afterAll, beforeAll } from "vitest"; import { Agent, fetch } from "undici"; import { listenOn, proxyListen } from "./https-proxy.test.ts"; @@ -26,23 +26,30 @@ const http2Agent = new Agent({ describe("http/2 listener", () => { describe("http2 -> http", async () => { - const source = http.createServer((_req, res) => { - res.writeHead(200, { "Content-Type": "text/plain" }); - res.end("Hello from " + sourcePort); - }); - const sourcePort = await listenOn(source); - - const proxy = httpProxy.createProxyServer({ - target: "http://127.0.0.1:" + sourcePort, - ssl: { - key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), - cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), - }, - http2: true, - // Allow to use SSL self signed - secure: false, + let source: http.Server; + let sourcePort: number; + let proxy: httpProxy.ProxyServer; + let proxyPort: number; + + beforeAll(async () => { + source = http.createServer((_req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("Hello from " + sourcePort); + }); + sourcePort = await listenOn(source); + + proxy = httpProxy.createProxyServer({ + target: "http://127.0.0.1:" + sourcePort, + ssl: { + key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), + cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), + }, + http2: true, + // Allow to use SSL self signed + secure: false, + }); + proxyPort = await proxyListen(proxy); }); - const proxyPort = await proxyListen(proxy); it("target http server should be working", async () => { try { @@ -84,30 +91,39 @@ describe("http/2 listener", () => { }); describe("http2 -> https", async () => { - const source = https.createServer( - { - key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), - cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), - ciphers: "AES128-GCM-SHA256", - }, - function (req, res) { - res.writeHead(200, { "Content-Type": "text/plain" }); - res.end("Hello from " + sourcePort); - }, - ); - const sourcePort = await listenOn(source); - - const proxy = httpProxy.createProxyServer({ - target: "https://127.0.0.1:" + sourcePort, - ssl: { - key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), - cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), - }, - http2: true, - // Allow to use SSL self signed - secure: false, + let source: https.Server; + let sourcePort: number; + let proxy: httpProxy.ProxyServer; + let proxyPort: number; + + beforeAll(async () => { + source = https.createServer( + { + key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), + cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), + ciphers: "AES128-GCM-SHA256", + }, + function (req, res) { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("Hello from " + sourcePort); + }, + ); + + sourcePort = await listenOn(source); + + proxy = httpProxy.createProxyServer({ + target: "https://127.0.0.1:" + sourcePort, + ssl: { + key: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-key.pem")), + cert: fs.readFileSync(path.join(__dirname, "fixtures", "agent2-cert.pem")), + }, + http2: true, + // Allow to use SSL self signed + secure: false, + }); + + proxyPort = await proxyListen(proxy); }); - const proxyPort = await proxyListen(proxy); it("target https server should be working", async () => { try { From 795359df64bf848664de6d91502a7948e935330d Mon Sep 17 00:00:00 2001 From: SukkaW Date: Tue, 24 Feb 2026 00:21:56 +0800 Subject: [PATCH 14/16] test: extract util --- test/_utils.ts | 27 +++++++++++++++++++++++++++ test/http-proxy.test.ts | 21 +-------------------- test/http2-proxy.test.ts | 3 ++- test/https-proxy.test.ts | 25 +------------------------ test/middleware/web-incoming.test.ts | 11 +---------- 5 files changed, 32 insertions(+), 55 deletions(-) create mode 100644 test/_utils.ts diff --git a/test/_utils.ts b/test/_utils.ts new file mode 100644 index 0000000..74aeb73 --- /dev/null +++ b/test/_utils.ts @@ -0,0 +1,27 @@ +import http from "node:http"; +import https from "node:https"; +import net from "node:net"; +import type { AddressInfo } from "node:net"; +import * as httpProxy from "../src/index.ts"; + +export function listenOn(server: http.Server | https.Server | net.Server): Promise { + return new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + resolve((server.address() as AddressInfo).port); + }); + }); +} + +export function proxyListen( + proxy: ReturnType, +): Promise { + return new Promise((resolve, reject) => { + proxy.listen(0, "127.0.0.1"); + const server = (proxy as any)._server as net.Server; + server.once("error", reject); + server.once("listening", () => { + resolve((server.address() as AddressInfo).port); + }); + }); +} diff --git a/test/http-proxy.test.ts b/test/http-proxy.test.ts index d5b8c77..7c6a79d 100644 --- a/test/http-proxy.test.ts +++ b/test/http-proxy.test.ts @@ -7,29 +7,10 @@ import * as io from "socket.io"; import SSE from "sse"; import ioClient from "socket.io-client"; import type { AddressInfo } from "node:net"; +import { listenOn, proxyListen } from "./_utils.ts"; // Source: https://github.com/http-party/node-http-proxy/blob/master/test/lib-http-proxy-test.js -function listenOn(server: http.Server | net.Server): Promise { - return new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, "127.0.0.1", () => { - resolve((server.address() as AddressInfo).port); - }); - }); -} - -function proxyListen(proxy: ReturnType): Promise { - return new Promise((resolve, reject) => { - proxy.listen(0, "127.0.0.1"); - const server = (proxy as any)._server as net.Server; - server.once("error", reject); - server.once("listening", () => { - resolve((server.address() as AddressInfo).port); - }); - }); -} - describe("http-proxy", () => { describe("#createProxyServer", () => { it.skip("should throw without options", () => { diff --git a/test/http2-proxy.test.ts b/test/http2-proxy.test.ts index 1259bfd..024fc63 100644 --- a/test/http2-proxy.test.ts +++ b/test/http2-proxy.test.ts @@ -6,7 +6,8 @@ import * as fs from "node:fs"; import { describe, it, expect, afterAll, beforeAll } from "vitest"; import { Agent, fetch } from "undici"; -import { listenOn, proxyListen } from "./https-proxy.test.ts"; +import { listenOn, proxyListen } from "./_utils.ts"; + import { inspect } from "node:util"; const http1Agent = new Agent({ diff --git a/test/https-proxy.test.ts b/test/https-proxy.test.ts index 873b867..d9e6c4d 100644 --- a/test/https-proxy.test.ts +++ b/test/https-proxy.test.ts @@ -2,35 +2,12 @@ import { describe, it, expect } from "vitest"; import * as httpProxy from "../src/index.ts"; import http from "node:http"; import https from "node:https"; -import net from "node:net"; import path from "node:path"; import fs from "node:fs"; -import type { AddressInfo } from "node:net"; +import { listenOn, proxyListen } from "./_utils.ts"; // Source: https://github.com/http-party/node-http-proxy/blob/master/test/lib-https-proxy-test.js -export function listenOn(server: http.Server | https.Server | net.Server): Promise { - return new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, "127.0.0.1", () => { - resolve((server.address() as AddressInfo).port); - }); - }); -} - -export function proxyListen( - proxy: ReturnType, -): Promise { - return new Promise((resolve, reject) => { - proxy.listen(0, "127.0.0.1"); - const server = (proxy as any)._server as net.Server; - server.once("error", reject); - server.once("listening", () => { - resolve((server.address() as AddressInfo).port); - }); - }); -} - describe("lib/http-proxy.js", () => { describe("HTTPS #createProxyServer", () => { describe("HTTPS to HTTP", () => { diff --git a/test/middleware/web-incoming.test.ts b/test/middleware/web-incoming.test.ts index 5407992..2260ce3 100644 --- a/test/middleware/web-incoming.test.ts +++ b/test/middleware/web-incoming.test.ts @@ -4,25 +4,16 @@ import * as webPasses from "../../src/middleware/web-incoming.ts"; import * as httpProxy from "../../src/index.ts"; import concat from "concat-stream"; import http from "node:http"; -import type { AddressInfo } from "node:net"; import { stubIncomingMessage, stubServerResponse, stubMiddlewareOptions, stubProxyServer, } from "../_stubs.ts"; +import { listenOn } from "../_utils.ts"; // Source: https://github.com/http-party/node-http-proxy/blob/master/test/lib-http-proxy-passes-web-incoming-test.js -function listenOn(server: http.Server): Promise { - return new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, "127.0.0.1", () => { - resolve((server.address() as AddressInfo).port); - }); - }); -} - describe("middleware:web-incoming", () => { describe("#deleteLength", () => { it("should change `content-length` for DELETE requests", () => { From 50c9d9e25ee44a04888becb5a7751ca4db55b891 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Tue, 24 Feb 2026 00:23:50 +0800 Subject: [PATCH 15/16] fix: ws listen upgrade --- src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index bb2f20a..e8d8283 100644 --- a/src/server.ts +++ b/src/server.ts @@ -89,7 +89,7 @@ export class ProxyServer< if (this.options.ws) { this._server.on("upgrade", (req, socket, head) => { - this.ws(req, socket, head).catch(() => {}); + this.ws(req, socket, this.options, head).catch(() => {}); }); } From 91b63fbc6c9fe3598b472385c249618dd42c3e77 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Mon, 2 Mar 2026 04:54:07 +0800 Subject: [PATCH 16/16] test: fix coverage --- src/_utils.ts | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/_utils.ts b/src/_utils.ts index 72e505b..05e3672 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -224,23 +224,37 @@ export function getPort(req: httpNative.IncomingMessage | Http2ServerRequest): s export function hasEncryptedConnection( req: httpNative.IncomingMessage | Http2ServerRequest, ): boolean { - // req.connection.pair probably does not exist anymore - if ("connection" in req) { - if ("encrypted" in req.connection) { - return req.connection.encrypted; - } - if ("pair" in req.connection) { - return !!req.connection.pair; - } - } // Since Node.js v16 we now have req.socket if ("socket" in req) { + /* v8 ignore start */ + + // encrypted is only present in TLS sockets, not plain net sockets if ("encrypted" in req.socket) { return req.socket.encrypted; } + + // "pair" is deprecated and is not typed by @types/node, but it actually hasn't been removed yet + // we can still fall back to "pair" for backward compatibility, but normally not reachable if ("pair" in req.socket) { return !!req.socket.pair; } + /* v8 ignore stop */ + } + + if ("connection" in req) { + /* v8 ignore start */ + + // encrypted is only present in TLS sockets, not plain net sockets + if ("encrypted" in req.connection) { + return req.connection.encrypted; + } + + // "pair" is deprecated and is not typed by @types/node, but it actually hasn't been removed yet + // we can still fall back to "pair" for backward compatibility, but normally not reachable + if ("pair" in req.connection) { + return !!req.connection.pair; + } + /* v8 ignore stop */ } return false;