From 3e41d3b1d30be2c2cc3000bbae38780ab08b597e Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sun, 8 Feb 2026 02:25:38 +0800 Subject: [PATCH 1/8] refactor(#1136): replace `http-proxy` w/ `httpxy` --- package.json | 3 +- src/http-proxy-middleware.ts | 14 ++++--- src/types.ts | 72 ++++++++++++++++++++++++++---------- tsconfig.json | 12 ++++-- yarn.lock | 36 +++--------------- 5 files changed, 76 insertions(+), 61 deletions(-) diff --git a/package.json b/package.json index 66378885..09af2995 100644 --- a/package.json +++ b/package.json @@ -89,9 +89,8 @@ "ws": "8.19.0" }, "dependencies": { - "@types/http-proxy": "^1.17.15", "debug": "^4.3.6", - "http-proxy": "^1.18.1", + "httpxy": "^0.1.7", "is-glob": "^4.0.3", "is-plain-object": "^5.0.0", "micromatch": "^4.0.8" diff --git a/src/http-proxy-middleware.ts b/src/http-proxy-middleware.ts index 6fe301c0..3fd25ff4 100644 --- a/src/http-proxy-middleware.ts +++ b/src/http-proxy-middleware.ts @@ -2,7 +2,7 @@ import type * as http from 'node:http'; import type * as https from 'node:https'; import type * as net from 'node:net'; -import * as httpProxy from 'http-proxy'; +import { type ProxyServer, createProxyServer } from 'httpxy'; import { verifyConfig } from './configuration'; import { Debug as debug } from './debug'; @@ -18,7 +18,7 @@ export class HttpProxyMiddleware { private wsInternalSubscribed = false; private serverOnCloseSubscribed = false; private proxyOptions: Options; - private proxy: httpProxy; + private proxy: ProxyServer; private pathRewriter; private logger: Logger; @@ -28,7 +28,7 @@ export class HttpProxyMiddleware { this.logger = getLogger(options as unknown as Options); debug(`create proxy server`); - this.proxy = httpProxy.createProxyServer({}); + this.proxy = createProxyServer({}); this.registerPlugins(this.proxy, this.proxyOptions); @@ -70,7 +70,9 @@ export class HttpProxyMiddleware { if (server && !this.serverOnCloseSubscribed) { server.on('close', () => { debug('server close signal received: closing proxy server'); - this.proxy.close(); + this.proxy.close(() => { + debug('proxy server closed'); + }); }); this.serverOnCloseSubscribed = true; } @@ -81,7 +83,7 @@ export class HttpProxyMiddleware { } }) as RequestHandler; - private registerPlugins(proxy: httpProxy, options: Options) { + private registerPlugins(proxy: ProxyServer, options: Options) { const plugins = getPlugins(options); plugins.forEach((plugin) => { debug(`register plugin: "${getFunctionName(plugin)}"`); @@ -103,7 +105,7 @@ export class HttpProxyMiddleware { try { if (this.shouldProxy(this.proxyOptions.pathFilter, req)) { const activeProxyOptions = await this.prepareProxyRequest(req); - this.proxy.ws(req, socket, head, activeProxyOptions); + this.proxy.ws(req, socket, activeProxyOptions, head); debug('server upgrade event received. Proxying WebSocket'); } } catch (err) { diff --git a/src/types.ts b/src/types.ts index dbddb4ec..95b1f5a3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,7 +5,7 @@ import type * as http from 'node:http'; import type * as net from 'node:net'; -import type * as httpProxy from 'http-proxy'; +import type { ProxyServer, ProxyServerOptions } from 'httpxy'; export type NextFunction void> = T; @@ -24,25 +24,59 @@ export type Filter = | ((pathname: string, req: TReq) => boolean); export interface Plugin { - (proxyServer: httpProxy, options: Options): void; + (proxyServer: ProxyServer, options: Options): void; } export interface OnProxyEvent { - error?: httpProxy.ErrorCallback; - proxyReq?: httpProxy.ProxyReqCallback; - proxyReqWs?: httpProxy.ProxyReqWsCallback; - proxyRes?: httpProxy.ProxyResCallback; - open?: httpProxy.OpenCallback; - close?: httpProxy.CloseCallback; - start?: httpProxy.StartCallback; - end?: httpProxy.EndCallback; - econnreset?: httpProxy.EconnresetCallback; + error?: ( + err: Error, + req: TReq, + res: TRes | net.Socket, + target?: string | Partial, + ) => void; + proxyReq?: (proxyReq: http.ClientRequest, req: TReq, res: TRes, options: ProxyServerOptions) => void; + proxyReqWs?: ( + proxyReq: http.ClientRequest, + req: TReq, + socket: net.Socket, + options: ProxyServerOptions, + head: any, + ) => void; + proxyRes?: ( + proxyRes: TReq, + req: TReq, + res: TRes, + ) => void; + open?: (proxySocket: net.Socket) => void; + close?: ( + proxyRes: TReq, + proxySocket: net.Socket, + proxyHead: any, + ) => void; + start?: ( + req: TReq, + res: TRes, + target: string | Partial, + ) => void; + end?: ( + req: TReq, + res: TRes, + proxyRes: TReq, + ) => void; + econnreset?: ( + err: Error, + req: TReq, + res: TRes, + target: string | Partial, + ) => void; } export type Logger = Pick; -export interface Options - extends httpProxy.ServerOptions { +export interface Options< + TReq = http.IncomingMessage, + TRes = http.ServerResponse, +> extends ProxyServerOptions { /** * Narrow down requests to proxy or not. * Filter on {@link http.IncomingMessage.url `pathname`} which is relative to the proxy's "mounting" point in the server. @@ -64,9 +98,9 @@ export interface Options string | undefined) - | ((path: string, req: TReq) => Promise); + | { [regexp: string]: string } + | ((path: string, req: TReq) => string | undefined) + | ((path: string, req: TReq) => Promise); /** * Access the internal http-proxy server instance to customize behavior * @@ -122,9 +156,9 @@ export interface Options httpProxy.ServerOptions['target']) - | ((req: TReq) => Promise); + | { [hostOrPath: string]: ProxyServerOptions['target'] } + | ((req: TReq) => ProxyServerOptions['target']) + | ((req: TReq) => Promise); /** * Log information from http-proxy-middleware * @example diff --git a/tsconfig.json b/tsconfig.json index 03827239..d05e5f7e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,14 +2,20 @@ "compilerOptions": { "rootDir": "./src", "outDir": "./dist", - "lib": ["es2021", "es2022"], + "lib": [ + "es2021", + "es2022" + ], "module": "commonjs", "moduleResolution": "node", "target": "es2021", "incremental": true, "declaration": true, "strict": true, - "noImplicitAny": false + "noImplicitAny": false, + "skipLibCheck": true }, - "include": ["./src"] + "include": [ + "./src" + ] } diff --git a/yarn.lock b/yarn.lock index 2b163f4f..f062ebb2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1508,13 +1508,6 @@ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.1.tgz#20172f9578b225f6c7da63446f56d4ce108d5a65" integrity sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ== -"@types/http-proxy@^1.17.15": - version "1.17.15" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.15.tgz#12118141ce9775a6499ecb4c01d02f90fc839d36" - integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ== - dependencies: - "@types/node" "*" - "@types/is-glob@4.0.4": version "4.0.4" resolved "https://registry.yarnpkg.com/@types/is-glob/-/is-glob-4.0.4.tgz#1d60fa47ff70abc97b4d9ea45328747c488b3a50" @@ -2891,11 +2884,6 @@ eventemitter3@^3.1.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== -eventemitter3@^4.0.0: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - eventemitter3@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" @@ -3139,11 +3127,6 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== -follow-redirects@^1.0.0: - version "1.15.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== - foreground-child@^3.1.0: version "3.3.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" @@ -3459,15 +3442,6 @@ http-proxy-agent@^7.0.0: agent-base "^7.1.0" debug "^4.3.4" -http-proxy@^1.18.1: - version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" - integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - http2-wrapper@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-2.2.1.tgz#310968153dcdedb160d8b72114363ef5fce1f64a" @@ -3492,6 +3466,11 @@ https-proxy-agent@^7.0.6: agent-base "^7.1.2" debug "4" +httpxy@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/httpxy/-/httpxy-0.1.7.tgz#02d02e57eda10e8b5c0e3f9f10860e3d7a5991a4" + integrity sha512-pXNx8gnANKAndgga5ahefxc++tJvNL87CXoRwxn1cJE2ZkWEojF3tNfQIEhZX/vfpt+wzeAzpUI4qkediX1MLQ== + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -5102,11 +5081,6 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== - resolve-alpn@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" From d6eab006a3c44202d2d05c43368eccc2885716e9 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sun, 8 Feb 2026 02:33:16 +0800 Subject: [PATCH 2/8] chore: make prettier happy --- src/types.ts | 47 +++++++++++++---------------------------------- tsconfig.json | 9 ++------- 2 files changed, 15 insertions(+), 41 deletions(-) diff --git a/src/types.ts b/src/types.ts index 95b1f5a3..9f53e10c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -28,13 +28,13 @@ export interface Plugin } export interface OnProxyEvent { - error?: ( - err: Error, + error?: (err: Error, req: TReq, res: TRes | net.Socket, target?: string | Partial) => void; + proxyReq?: ( + proxyReq: http.ClientRequest, req: TReq, - res: TRes | net.Socket, - target?: string | Partial, + res: TRes, + options: ProxyServerOptions, ) => void; - proxyReq?: (proxyReq: http.ClientRequest, req: TReq, res: TRes, options: ProxyServerOptions) => void; proxyReqWs?: ( proxyReq: http.ClientRequest, req: TReq, @@ -42,33 +42,12 @@ export interface OnProxyEvent void; - proxyRes?: ( - proxyRes: TReq, - req: TReq, - res: TRes, - ) => void; + proxyRes?: (proxyRes: TReq, req: TReq, res: TRes) => void; open?: (proxySocket: net.Socket) => void; - close?: ( - proxyRes: TReq, - proxySocket: net.Socket, - proxyHead: any, - ) => void; - start?: ( - req: TReq, - res: TRes, - target: string | Partial, - ) => void; - end?: ( - req: TReq, - res: TRes, - proxyRes: TReq, - ) => void; - econnreset?: ( - err: Error, - req: TReq, - res: TRes, - target: string | Partial, - ) => void; + close?: (proxyRes: TReq, proxySocket: net.Socket, proxyHead: any) => void; + start?: (req: TReq, res: TRes, target: string | Partial) => void; + end?: (req: TReq, res: TRes, proxyRes: TReq) => void; + econnreset?: (err: Error, req: TReq, res: TRes, target: string | Partial) => void; } export type Logger = Pick; @@ -98,9 +77,9 @@ export interface Options< * @link https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/pathRewrite.md */ pathRewrite?: - | { [regexp: string]: string } - | ((path: string, req: TReq) => string | undefined) - | ((path: string, req: TReq) => Promise); + | { [regexp: string]: string } + | ((path: string, req: TReq) => string | undefined) + | ((path: string, req: TReq) => Promise); /** * Access the internal http-proxy server instance to customize behavior * diff --git a/tsconfig.json b/tsconfig.json index d05e5f7e..d6bb9b69 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,10 +2,7 @@ "compilerOptions": { "rootDir": "./src", "outDir": "./dist", - "lib": [ - "es2021", - "es2022" - ], + "lib": ["es2021", "es2022"], "module": "commonjs", "moduleResolution": "node", "target": "es2021", @@ -15,7 +12,5 @@ "noImplicitAny": false, "skipLibCheck": true }, - "include": [ - "./src" - ] + "include": ["./src"] } From 5b2250a6cfda402df7f6d729732fc307996582c0 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sun, 8 Feb 2026 02:33:58 +0800 Subject: [PATCH 3/8] chore: make cspell happy --- cspell.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cspell.json b/cspell.json index a42ca582..95480a4e 100644 --- a/cspell.json +++ b/cspell.json @@ -52,6 +52,7 @@ "typicode", "vhosted", "websockets", - "xfwd" + "xfwd", + "httpxy" ] } From 24b66406995b80a71bbe033f2d0e1e980b95443c Mon Sep 17 00:00:00 2001 From: SukkaW Date: Sun, 8 Feb 2026 03:04:06 +0800 Subject: [PATCH 4/8] refactor: make tests passed --- src/http-proxy-middleware.ts | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/http-proxy-middleware.ts b/src/http-proxy-middleware.ts index 3fd25ff4..194455f4 100644 --- a/src/http-proxy-middleware.ts +++ b/src/http-proxy-middleware.ts @@ -46,11 +46,33 @@ export class HttpProxyMiddleware { // https://github.com/Microsoft/TypeScript/wiki/'this'-in-TypeScript#red-flags-for-this public middleware: RequestHandler = (async (req, res, next?) => { if (this.shouldProxy(this.proxyOptions.pathFilter, req)) { + let activeProxyOptions: Options; try { - const activeProxyOptions = await this.prepareProxyRequest(req); + // Preparation Phase: Apply router and path rewriter. + activeProxyOptions = await this.prepareProxyRequest(req); + + // [Smoking Gun] httpxy is inconsistent with error handling: + // 1. If target is missing (here), it emits 'error' but returns a boolean (bypassing our catch/next). + // 2. If a network error occurs (in proxy.web), it rejects the promise but SKIPS emitting 'error'. + // We manually throw here to force Case 1 into the catch block so next(err) is called for Express. + if (!activeProxyOptions.target && !activeProxyOptions.forward) { + throw new Error('Must provide a proper URL as target'); + } + } catch (err) { + next?.(err); + return; + } + + try { + // Proxying Phase: Handle the actual web request. debug(`proxy request to target: %O`, activeProxyOptions.target); - this.proxy.web(req, res, activeProxyOptions); + await this.proxy.web(req, res, activeProxyOptions); } catch (err) { + // Manually emit 'error' event because httpxy's promise-based API does not emit it automatically. + // This is crucial for backward compatibility with HPM plugins (like error-response-plugin) + // and custom listeners registered via the 'on: { error: ... }' option. + this.proxy.emit('error', err, req, res, activeProxyOptions.target); + next?.(err); } } else { @@ -105,7 +127,7 @@ export class HttpProxyMiddleware { try { if (this.shouldProxy(this.proxyOptions.pathFilter, req)) { const activeProxyOptions = await this.prepareProxyRequest(req); - this.proxy.ws(req, socket, activeProxyOptions, head); + await this.proxy.ws(req, socket, activeProxyOptions, head); debug('server upgrade event received. Proxying WebSocket'); } } catch (err) { From 56c75ea20c6e1a4f1e4fa385908d894576985ac8 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Wed, 11 Feb 2026 00:03:43 +0800 Subject: [PATCH 5/8] chore: upgrade to latest httpxy and fix more types --- package.json | 2 +- src/http-proxy-middleware.ts | 6 +++--- src/plugins/default/proxy-events.ts | 21 +++++++++++++++++---- yarn.lock | 8 ++++---- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 09af2995..e8ad914b 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ }, "dependencies": { "debug": "^4.3.6", - "httpxy": "^0.1.7", + "httpxy": "^0.2.2", "is-glob": "^4.0.3", "is-plain-object": "^5.0.0", "micromatch": "^4.0.8" diff --git a/src/http-proxy-middleware.ts b/src/http-proxy-middleware.ts index 194455f4..7170bc58 100644 --- a/src/http-proxy-middleware.ts +++ b/src/http-proxy-middleware.ts @@ -18,7 +18,7 @@ export class HttpProxyMiddleware { private wsInternalSubscribed = false; private serverOnCloseSubscribed = false; private proxyOptions: Options; - private proxy: ProxyServer; + private proxy: ProxyServer; private pathRewriter; private logger: Logger; @@ -71,7 +71,7 @@ export class HttpProxyMiddleware { // Manually emit 'error' event because httpxy's promise-based API does not emit it automatically. // This is crucial for backward compatibility with HPM plugins (like error-response-plugin) // and custom listeners registered via the 'on: { error: ... }' option. - this.proxy.emit('error', err, req, res, activeProxyOptions.target); + this.proxy.emit('error', err as Error, req, res, activeProxyOptions.target); next?.(err); } @@ -133,7 +133,7 @@ export class HttpProxyMiddleware { } catch (err) { // This error does not include the URL as the fourth argument as we won't // have the URL if `this.prepareProxyRequest` throws an error. - this.proxy.emit('error', err, req, socket); + this.proxy.emit('error', err as Error, req, socket); } }; diff --git a/src/plugins/default/proxy-events.ts b/src/plugins/default/proxy-events.ts index aa686d90..4ce3c079 100644 --- a/src/plugins/default/proxy-events.ts +++ b/src/plugins/default/proxy-events.ts @@ -25,8 +25,21 @@ const debug = Debug.extend('proxy-events-plugin'); * ``` */ export const proxyEventsPlugin: Plugin = (proxyServer, options) => { - Object.entries(options.on || {}).forEach(([eventName, handler]) => { - debug(`register event handler: "${eventName}" -> "${getFunctionName(handler)}"`); - proxyServer.on(eventName, handler as (...args: unknown[]) => void); - }); + if (!options.on) { + return; + } + + // hoist variable here for better typing + let eventName: keyof typeof options.on; + // for in provide better typing than Object.entries() + for (eventName in options.on) { + if (Object.prototype.hasOwnProperty.call(options.on, eventName)) { + const handler = options.on[eventName]; + if (!handler) { + continue; + } + debug(`register event handler: "${eventName}" -> "${getFunctionName(handler)}"`); + proxyServer.on(eventName, handler as (...args: unknown[]) => void); + } + } }; diff --git a/yarn.lock b/yarn.lock index f062ebb2..23c099e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3466,10 +3466,10 @@ https-proxy-agent@^7.0.6: agent-base "^7.1.2" debug "4" -httpxy@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/httpxy/-/httpxy-0.1.7.tgz#02d02e57eda10e8b5c0e3f9f10860e3d7a5991a4" - integrity sha512-pXNx8gnANKAndgga5ahefxc++tJvNL87CXoRwxn1cJE2ZkWEojF3tNfQIEhZX/vfpt+wzeAzpUI4qkediX1MLQ== +httpxy@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/httpxy/-/httpxy-0.2.2.tgz#1603165cfd12087f2039c4a8532ce61eab5d84e5" + integrity sha512-QnS6mAOqvdmfctFaD/Kp5cwRMcaUhEmIyw2rjVYnhJb8EuAyd/79biT/50HUTKvq/PwoXYTP6J3d9TUxzHFZwA== human-signals@^2.1.0: version "2.1.0" From 16063a6f806151f207410d3fd578796879ee4c05 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Thu, 12 Feb 2026 01:58:51 +0800 Subject: [PATCH 6/8] refactor: make types happy --- src/handlers/response-interceptor.ts | 6 +++--- src/http-proxy-middleware.ts | 22 +++++++++++++++++--- src/plugins/default/error-response-plugin.ts | 2 +- src/types.ts | 14 ++++++------- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/handlers/response-interceptor.ts b/src/handlers/response-interceptor.ts index 67a53027..94f20796 100644 --- a/src/handlers/response-interceptor.ts +++ b/src/handlers/response-interceptor.ts @@ -8,7 +8,7 @@ const debug = Debug.extend('response-interceptor'); type Interceptor = ( buffer: Buffer, - proxyRes: TReq, + proxyRes: http.IncomingMessage, req: TReq, res: TRes, ) => Promise; @@ -25,7 +25,7 @@ export function responseInterceptor< TRes extends http.ServerResponse = http.ServerResponse, >(interceptor: Interceptor) { return async function proxyResResponseInterceptor( - proxyRes: TReq, + proxyRes: http.IncomingMessage, req: TReq, res: TRes, ): Promise { @@ -34,7 +34,7 @@ export function responseInterceptor< let buffer = Buffer.from('', 'utf8'); // decompress proxy response - const _proxyRes = decompress(proxyRes, proxyRes.headers['content-encoding']); + const _proxyRes = decompress(proxyRes, proxyRes.headers['content-encoding']); // concat data stream _proxyRes.on('data', (chunk) => (buffer = Buffer.concat([buffer, chunk]))); diff --git a/src/http-proxy-middleware.ts b/src/http-proxy-middleware.ts index 7170bc58..00b4d295 100644 --- a/src/http-proxy-middleware.ts +++ b/src/http-proxy-middleware.ts @@ -71,7 +71,15 @@ export class HttpProxyMiddleware { // Manually emit 'error' event because httpxy's promise-based API does not emit it automatically. // This is crucial for backward compatibility with HPM plugins (like error-response-plugin) // and custom listeners registered via the 'on: { error: ... }' option. - this.proxy.emit('error', err as Error, req, res, activeProxyOptions.target); + + /** + * TODO: Ideally, TReq and TRes should be restricted via "TReq extends http.IncomingMessage = http.IncomingMessage" + * and "TRes extends http.ServerResponse = http.ServerResponse", which allows us to avoid the "req as TReq" below. + * + * However, making TReq and TRes constrained types may cause a breaking change for TypeScript users downstream. + * So we leave this as a TODO for now, and revisit it in a future major release. + */ + this.proxy.emit('error', err as Error, req as TReq, res, activeProxyOptions.target); next?.(err); } @@ -105,7 +113,7 @@ export class HttpProxyMiddleware { } }) as RequestHandler; - private registerPlugins(proxy: ProxyServer, options: Options) { + private registerPlugins(proxy: ProxyServer, options: Options) { const plugins = getPlugins(options); plugins.forEach((plugin) => { debug(`register plugin: "${getFunctionName(plugin)}"`); @@ -133,7 +141,15 @@ export class HttpProxyMiddleware { } catch (err) { // This error does not include the URL as the fourth argument as we won't // have the URL if `this.prepareProxyRequest` throws an error. - this.proxy.emit('error', err as Error, req, socket); + + /** + * TODO: Ideally, TReq and TRes should be restricted via "TReq extends http.IncomingMessage = http.IncomingMessage" + * and "TRes extends http.ServerResponse = http.ServerResponse", which allows us to avoid the "req as TReq" below. + * + * However, making TReq and TRes constrained types may cause a breaking change for TypeScript users downstream. + * So we leave this as a TODO for now, and revisit it in a future major release. + */ + this.proxy.emit('error', err as Error, req as TReq, socket); } }; diff --git a/src/plugins/default/error-response-plugin.ts b/src/plugins/default/error-response-plugin.ts index edddca83..fa96cbec 100644 --- a/src/plugins/default/error-response-plugin.ts +++ b/src/plugins/default/error-response-plugin.ts @@ -16,7 +16,7 @@ function isSocketLike(obj: any): obj is Socket { export const errorResponsePlugin: Plugin = (proxyServer, options) => { proxyServer.on('error', (err, req, res, target?) => { // Re-throw error. Not recoverable since req & res are empty. - if (!req && !res) { + if (!req || !res) { throw err; // "Error: Must provide a proper URL as target" } diff --git a/src/types.ts b/src/types.ts index 9f53e10c..51adf3b4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,7 +24,7 @@ export type Filter = | ((pathname: string, req: TReq) => boolean); export interface Plugin { - (proxyServer: ProxyServer, options: Options): void; + (proxyServer: ProxyServer, options: Options): void; } export interface OnProxyEvent { @@ -77,9 +77,9 @@ export interface Options< * @link https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/pathRewrite.md */ pathRewrite?: - | { [regexp: string]: string } - | ((path: string, req: TReq) => string | undefined) - | ((path: string, req: TReq) => Promise); + | { [regexp: string]: string } + | ((path: string, req: TReq) => string | undefined) + | ((path: string, req: TReq) => Promise); /** * Access the internal http-proxy server instance to customize behavior * @@ -135,9 +135,9 @@ export interface Options< * @link https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/router.md */ router?: - | { [hostOrPath: string]: ProxyServerOptions['target'] } - | ((req: TReq) => ProxyServerOptions['target']) - | ((req: TReq) => Promise); + | { [hostOrPath: string]: ProxyServerOptions['target'] } + | ((req: TReq) => ProxyServerOptions['target']) + | ((req: TReq) => Promise); /** * Log information from http-proxy-middleware * @example From 7786d53eb2d6e0d897a805cb4617251e50ccee23 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Thu, 12 Feb 2026 11:31:10 +0800 Subject: [PATCH 7/8] chore: make lint happy --- src/types.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/types.ts b/src/types.ts index 51adf3b4..fceedd9f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -77,9 +77,9 @@ export interface Options< * @link https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/pathRewrite.md */ pathRewrite?: - | { [regexp: string]: string } - | ((path: string, req: TReq) => string | undefined) - | ((path: string, req: TReq) => Promise); + | { [regexp: string]: string } + | ((path: string, req: TReq) => string | undefined) + | ((path: string, req: TReq) => Promise); /** * Access the internal http-proxy server instance to customize behavior * @@ -135,9 +135,9 @@ export interface Options< * @link https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/router.md */ router?: - | { [hostOrPath: string]: ProxyServerOptions['target'] } - | ((req: TReq) => ProxyServerOptions['target']) - | ((req: TReq) => Promise); + | { [hostOrPath: string]: ProxyServerOptions['target'] } + | ((req: TReq) => ProxyServerOptions['target']) + | ((req: TReq) => Promise); /** * Log information from http-proxy-middleware * @example From eb2449cd8cf815c60de52e7b4dc6e2938fb571d6 Mon Sep 17 00:00:00 2001 From: SukkaW Date: Fri, 3 Apr 2026 14:10:35 +0800 Subject: [PATCH 8/8] chore: drop outdated patch-package --- patches/http-proxy+1.18.1.patch | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 patches/http-proxy+1.18.1.patch diff --git a/patches/http-proxy+1.18.1.patch b/patches/http-proxy+1.18.1.patch deleted file mode 100644 index d3fecc15..00000000 --- a/patches/http-proxy+1.18.1.patch +++ /dev/null @@ -1,23 +0,0 @@ -diff --git a/node_modules/http-proxy/lib/http-proxy/common.js b/node_modules/http-proxy/lib/http-proxy/common.js -index 6513e81..d01d8db 100644 ---- a/node_modules/http-proxy/lib/http-proxy/common.js -+++ b/node_modules/http-proxy/lib/http-proxy/common.js -@@ -1,6 +1,6 @@ - var common = exports, - url = require('url'), -- extend = require('util')._extend, -+ extend = Object.assign, - required = require('requires-port'); - - var upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i, -diff --git a/node_modules/http-proxy/lib/http-proxy/index.js b/node_modules/http-proxy/lib/http-proxy/index.js -index 977a4b3..739409c 100644 ---- a/node_modules/http-proxy/lib/http-proxy/index.js -+++ b/node_modules/http-proxy/lib/http-proxy/index.js -@@ -1,5 +1,5 @@ - var httpProxy = module.exports, -- extend = require('util')._extend, -+ extend = Object.assign, - parse_url = require('url').parse, - EE3 = require('eventemitter3'), - http = require('http'),