Skip to content

Commit 42d25d4

Browse files
authored
refactor(#1136): replace http-proxy w/ httpxy (#1160)
* refactor(#1136): replace `http-proxy` w/ `httpxy` * chore: make prettier happy * chore: make cspell happy * refactor: make tests passed * chore: upgrade to latest httpxy and fix more types * refactor: make types happy * chore: make lint happy * chore: drop outdated patch-package
1 parent bcf1864 commit 42d25d4

10 files changed

Lines changed: 109 additions & 91 deletions

File tree

cspell.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"typicode",
5353
"vhosted",
5454
"websockets",
55-
"xfwd"
55+
"xfwd",
56+
"httpxy"
5657
]
5758
}

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,8 @@
8989
"ws": "8.19.0"
9090
},
9191
"dependencies": {
92-
"@types/http-proxy": "^1.17.15",
9392
"debug": "^4.3.6",
94-
"http-proxy": "^1.18.1",
93+
"httpxy": "^0.2.2",
9594
"is-glob": "^4.0.3",
9695
"is-plain-object": "^5.0.0",
9796
"micromatch": "^4.0.8"

patches/http-proxy+1.18.1.patch

Lines changed: 0 additions & 23 deletions
This file was deleted.

src/handlers/response-interceptor.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const debug = Debug.extend('response-interceptor');
88

99
type Interceptor<TReq = http.IncomingMessage, TRes = http.ServerResponse> = (
1010
buffer: Buffer,
11-
proxyRes: TReq,
11+
proxyRes: http.IncomingMessage,
1212
req: TReq,
1313
res: TRes,
1414
) => Promise<Buffer | string>;
@@ -25,7 +25,7 @@ export function responseInterceptor<
2525
TRes extends http.ServerResponse = http.ServerResponse,
2626
>(interceptor: Interceptor<TReq, TRes>) {
2727
return async function proxyResResponseInterceptor(
28-
proxyRes: TReq,
28+
proxyRes: http.IncomingMessage,
2929
req: TReq,
3030
res: TRes,
3131
): Promise<void> {
@@ -34,7 +34,7 @@ export function responseInterceptor<
3434
let buffer = Buffer.from('', 'utf8');
3535

3636
// decompress proxy response
37-
const _proxyRes = decompress<TReq>(proxyRes, proxyRes.headers['content-encoding']);
37+
const _proxyRes = decompress(proxyRes, proxyRes.headers['content-encoding']);
3838

3939
// concat data stream
4040
_proxyRes.on('data', (chunk) => (buffer = Buffer.concat([buffer, chunk])));

src/http-proxy-middleware.ts

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type * as http from 'node:http';
22
import type * as https from 'node:https';
33
import type * as net from 'node:net';
44

5-
import * as httpProxy from 'http-proxy';
5+
import { type ProxyServer, createProxyServer } from 'httpxy';
66

77
import { verifyConfig } from './configuration';
88
import { Debug as debug } from './debug';
@@ -18,7 +18,7 @@ export class HttpProxyMiddleware<TReq, TRes> {
1818
private wsInternalSubscribed = false;
1919
private serverOnCloseSubscribed = false;
2020
private proxyOptions: Options<TReq, TRes>;
21-
private proxy: httpProxy<TReq, TRes>;
21+
private proxy: ProxyServer<TReq, TRes>;
2222
private pathRewriter;
2323
private logger: Logger;
2424

@@ -28,7 +28,7 @@ export class HttpProxyMiddleware<TReq, TRes> {
2828
this.logger = getLogger(options as unknown as Options);
2929

3030
debug(`create proxy server`);
31-
this.proxy = httpProxy.createProxyServer({});
31+
this.proxy = createProxyServer({});
3232

3333
this.registerPlugins(this.proxy, this.proxyOptions);
3434

@@ -46,11 +46,41 @@ export class HttpProxyMiddleware<TReq, TRes> {
4646
// https://github.com/Microsoft/TypeScript/wiki/'this'-in-TypeScript#red-flags-for-this
4747
public middleware: RequestHandler = (async (req, res, next?) => {
4848
if (this.shouldProxy(this.proxyOptions.pathFilter, req)) {
49+
let activeProxyOptions: Options<TReq, TRes>;
4950
try {
50-
const activeProxyOptions = await this.prepareProxyRequest(req);
51+
// Preparation Phase: Apply router and path rewriter.
52+
activeProxyOptions = await this.prepareProxyRequest(req);
53+
54+
// [Smoking Gun] httpxy is inconsistent with error handling:
55+
// 1. If target is missing (here), it emits 'error' but returns a boolean (bypassing our catch/next).
56+
// 2. If a network error occurs (in proxy.web), it rejects the promise but SKIPS emitting 'error'.
57+
// We manually throw here to force Case 1 into the catch block so next(err) is called for Express.
58+
if (!activeProxyOptions.target && !activeProxyOptions.forward) {
59+
throw new Error('Must provide a proper URL as target');
60+
}
61+
} catch (err) {
62+
next?.(err);
63+
return;
64+
}
65+
66+
try {
67+
// Proxying Phase: Handle the actual web request.
5168
debug(`proxy request to target: %O`, activeProxyOptions.target);
52-
this.proxy.web(req, res, activeProxyOptions);
69+
await this.proxy.web(req, res, activeProxyOptions);
5370
} catch (err) {
71+
// Manually emit 'error' event because httpxy's promise-based API does not emit it automatically.
72+
// This is crucial for backward compatibility with HPM plugins (like error-response-plugin)
73+
// and custom listeners registered via the 'on: { error: ... }' option.
74+
75+
/**
76+
* TODO: Ideally, TReq and TRes should be restricted via "TReq extends http.IncomingMessage = http.IncomingMessage"
77+
* and "TRes extends http.ServerResponse = http.ServerResponse", which allows us to avoid the "req as TReq" below.
78+
*
79+
* However, making TReq and TRes constrained types may cause a breaking change for TypeScript users downstream.
80+
* So we leave this as a TODO for now, and revisit it in a future major release.
81+
*/
82+
this.proxy.emit('error', err as Error, req as TReq, res, activeProxyOptions.target);
83+
5484
next?.(err);
5585
}
5686
} else {
@@ -70,7 +100,9 @@ export class HttpProxyMiddleware<TReq, TRes> {
70100
if (server && !this.serverOnCloseSubscribed) {
71101
server.on('close', () => {
72102
debug('server close signal received: closing proxy server');
73-
this.proxy.close();
103+
this.proxy.close(() => {
104+
debug('proxy server closed');
105+
});
74106
});
75107
this.serverOnCloseSubscribed = true;
76108
}
@@ -81,7 +113,7 @@ export class HttpProxyMiddleware<TReq, TRes> {
81113
}
82114
}) as RequestHandler;
83115

84-
private registerPlugins(proxy: httpProxy<TReq, TRes>, options: Options<TReq, TRes>) {
116+
private registerPlugins(proxy: ProxyServer<TReq, TRes>, options: Options<TReq, TRes>) {
85117
const plugins = getPlugins<TReq, TRes>(options);
86118
plugins.forEach((plugin) => {
87119
debug(`register plugin: "${getFunctionName(plugin)}"`);
@@ -103,13 +135,21 @@ export class HttpProxyMiddleware<TReq, TRes> {
103135
try {
104136
if (this.shouldProxy(this.proxyOptions.pathFilter, req)) {
105137
const activeProxyOptions = await this.prepareProxyRequest(req);
106-
this.proxy.ws(req, socket, head, activeProxyOptions);
138+
await this.proxy.ws(req, socket, activeProxyOptions, head);
107139
debug('server upgrade event received. Proxying WebSocket');
108140
}
109141
} catch (err) {
110142
// This error does not include the URL as the fourth argument as we won't
111143
// have the URL if `this.prepareProxyRequest` throws an error.
112-
this.proxy.emit('error', err, req, socket);
144+
145+
/**
146+
* TODO: Ideally, TReq and TRes should be restricted via "TReq extends http.IncomingMessage = http.IncomingMessage"
147+
* and "TRes extends http.ServerResponse = http.ServerResponse", which allows us to avoid the "req as TReq" below.
148+
*
149+
* However, making TReq and TRes constrained types may cause a breaking change for TypeScript users downstream.
150+
* So we leave this as a TODO for now, and revisit it in a future major release.
151+
*/
152+
this.proxy.emit('error', err as Error, req as TReq, socket);
113153
}
114154
};
115155

src/plugins/default/error-response-plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function isSocketLike(obj: any): obj is Socket {
1616
export const errorResponsePlugin: Plugin = (proxyServer, options) => {
1717
proxyServer.on('error', (err, req, res, target?) => {
1818
// Re-throw error. Not recoverable since req & res are empty.
19-
if (!req && !res) {
19+
if (!req || !res) {
2020
throw err; // "Error: Must provide a proper URL as target"
2121
}
2222

src/plugins/default/proxy-events.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,21 @@ const debug = Debug.extend('proxy-events-plugin');
2525
* ```
2626
*/
2727
export const proxyEventsPlugin: Plugin = (proxyServer, options) => {
28-
Object.entries(options.on || {}).forEach(([eventName, handler]) => {
29-
debug(`register event handler: "${eventName}" -> "${getFunctionName(handler)}"`);
30-
proxyServer.on(eventName, handler as (...args: unknown[]) => void);
31-
});
28+
if (!options.on) {
29+
return;
30+
}
31+
32+
// hoist variable here for better typing
33+
let eventName: keyof typeof options.on;
34+
// for in provide better typing than Object.entries()
35+
for (eventName in options.on) {
36+
if (Object.prototype.hasOwnProperty.call(options.on, eventName)) {
37+
const handler = options.on[eventName];
38+
if (!handler) {
39+
continue;
40+
}
41+
debug(`register event handler: "${eventName}" -> "${getFunctionName(handler)}"`);
42+
proxyServer.on<keyof typeof options.on>(eventName, handler as (...args: unknown[]) => void);
43+
}
44+
}
3245
};

src/types.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import type * as http from 'node:http';
66
import type * as net from 'node:net';
77

8-
import type * as httpProxy from 'http-proxy';
8+
import type { ProxyServer, ProxyServerOptions } from 'httpxy';
99

1010
export type NextFunction<T = (err?: any) => void> = T;
1111

@@ -24,25 +24,38 @@ export type Filter<TReq = http.IncomingMessage> =
2424
| ((pathname: string, req: TReq) => boolean);
2525

2626
export interface Plugin<TReq = http.IncomingMessage, TRes = http.ServerResponse> {
27-
(proxyServer: httpProxy<TReq, TRes>, options: Options<TReq, TRes>): void;
27+
(proxyServer: ProxyServer<TReq, TRes>, options: Options<TReq, TRes>): void;
2828
}
2929

3030
export interface OnProxyEvent<TReq = http.IncomingMessage, TRes = http.ServerResponse> {
31-
error?: httpProxy.ErrorCallback<Error, TReq, TRes>;
32-
proxyReq?: httpProxy.ProxyReqCallback<http.ClientRequest, TReq, TRes>;
33-
proxyReqWs?: httpProxy.ProxyReqWsCallback<http.ClientRequest, TReq>;
34-
proxyRes?: httpProxy.ProxyResCallback<TReq, TRes>;
35-
open?: httpProxy.OpenCallback;
36-
close?: httpProxy.CloseCallback<TReq>;
37-
start?: httpProxy.StartCallback<TReq, TRes>;
38-
end?: httpProxy.EndCallback<TReq, TRes>;
39-
econnreset?: httpProxy.EconnresetCallback<Error, TReq, TRes>;
31+
error?: (err: Error, req: TReq, res: TRes | net.Socket, target?: string | Partial<URL>) => void;
32+
proxyReq?: (
33+
proxyReq: http.ClientRequest,
34+
req: TReq,
35+
res: TRes,
36+
options: ProxyServerOptions,
37+
) => void;
38+
proxyReqWs?: (
39+
proxyReq: http.ClientRequest,
40+
req: TReq,
41+
socket: net.Socket,
42+
options: ProxyServerOptions,
43+
head: any,
44+
) => void;
45+
proxyRes?: (proxyRes: TReq, req: TReq, res: TRes) => void;
46+
open?: (proxySocket: net.Socket) => void;
47+
close?: (proxyRes: TReq, proxySocket: net.Socket, proxyHead: any) => void;
48+
start?: (req: TReq, res: TRes, target: string | Partial<URL>) => void;
49+
end?: (req: TReq, res: TRes, proxyRes: TReq) => void;
50+
econnreset?: (err: Error, req: TReq, res: TRes, target: string | Partial<URL>) => void;
4051
}
4152

4253
export type Logger = Pick<Console, 'info' | 'warn' | 'error'>;
4354

44-
export interface Options<TReq = http.IncomingMessage, TRes = http.ServerResponse>
45-
extends httpProxy.ServerOptions {
55+
export interface Options<
56+
TReq = http.IncomingMessage,
57+
TRes = http.ServerResponse,
58+
> extends ProxyServerOptions {
4659
/**
4760
* Narrow down requests to proxy or not.
4861
* Filter on {@link http.IncomingMessage.url `pathname`} which is relative to the proxy's "mounting" point in the server.
@@ -122,9 +135,9 @@ export interface Options<TReq = http.IncomingMessage, TRes = http.ServerResponse
122135
* @link https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/router.md
123136
*/
124137
router?:
125-
| { [hostOrPath: string]: httpProxy.ServerOptions['target'] }
126-
| ((req: TReq) => httpProxy.ServerOptions['target'])
127-
| ((req: TReq) => Promise<httpProxy.ServerOptions['target']>);
138+
| { [hostOrPath: string]: ProxyServerOptions['target'] }
139+
| ((req: TReq) => ProxyServerOptions['target'])
140+
| ((req: TReq) => Promise<ProxyServerOptions['target']>);
128141
/**
129142
* Log information from http-proxy-middleware
130143
* @example

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"incremental": true,
1010
"declaration": true,
1111
"strict": true,
12-
"noImplicitAny": false
12+
"noImplicitAny": false,
13+
"skipLibCheck": true
1314
},
1415
"include": ["./src"]
1516
}

yarn.lock

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1508,13 +1508,6 @@
15081508
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.1.tgz#20172f9578b225f6c7da63446f56d4ce108d5a65"
15091509
integrity sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==
15101510

1511-
"@types/http-proxy@^1.17.15":
1512-
version "1.17.15"
1513-
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.15.tgz#12118141ce9775a6499ecb4c01d02f90fc839d36"
1514-
integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==
1515-
dependencies:
1516-
"@types/node" "*"
1517-
15181511
"@types/is-glob@4.0.4":
15191512
version "4.0.4"
15201513
resolved "https://registry.yarnpkg.com/@types/is-glob/-/is-glob-4.0.4.tgz#1d60fa47ff70abc97b4d9ea45328747c488b3a50"
@@ -2891,11 +2884,6 @@ eventemitter3@^3.1.0:
28912884
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
28922885
integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==
28932886

2894-
eventemitter3@^4.0.0:
2895-
version "4.0.7"
2896-
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
2897-
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
2898-
28992887
eventemitter3@^5.0.1:
29002888
version "5.0.1"
29012889
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4"
@@ -3139,11 +3127,6 @@ flatted@^3.2.9:
31393127
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726"
31403128
integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==
31413129

3142-
follow-redirects@^1.0.0:
3143-
version "1.15.6"
3144-
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
3145-
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
3146-
31473130
foreground-child@^3.1.0:
31483131
version "3.3.1"
31493132
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f"
@@ -3459,15 +3442,6 @@ http-proxy-agent@^7.0.0:
34593442
agent-base "^7.1.0"
34603443
debug "^4.3.4"
34613444

3462-
http-proxy@^1.18.1:
3463-
version "1.18.1"
3464-
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
3465-
integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
3466-
dependencies:
3467-
eventemitter3 "^4.0.0"
3468-
follow-redirects "^1.0.0"
3469-
requires-port "^1.0.0"
3470-
34713445
http2-wrapper@^2.2.1:
34723446
version "2.2.1"
34733447
resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-2.2.1.tgz#310968153dcdedb160d8b72114363ef5fce1f64a"
@@ -3492,6 +3466,11 @@ https-proxy-agent@^7.0.6:
34923466
agent-base "^7.1.2"
34933467
debug "4"
34943468

3469+
httpxy@^0.2.2:
3470+
version "0.2.2"
3471+
resolved "https://registry.yarnpkg.com/httpxy/-/httpxy-0.2.2.tgz#1603165cfd12087f2039c4a8532ce61eab5d84e5"
3472+
integrity sha512-QnS6mAOqvdmfctFaD/Kp5cwRMcaUhEmIyw2rjVYnhJb8EuAyd/79biT/50HUTKvq/PwoXYTP6J3d9TUxzHFZwA==
3473+
34953474
human-signals@^2.1.0:
34963475
version "2.1.0"
34973476
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
@@ -5102,11 +5081,6 @@ require-from-string@^2.0.2:
51025081
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
51035082
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
51045083

5105-
requires-port@^1.0.0:
5106-
version "1.0.0"
5107-
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
5108-
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
5109-
51105084
resolve-alpn@^1.2.0:
51115085
version "1.2.1"
51125086
resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9"

0 commit comments

Comments
 (0)