@@ -2,7 +2,7 @@ import type * as http from 'node:http';
22import type * as https from 'node:https' ;
33import type * as net from 'node:net' ;
44
5- import * as httpProxy from 'http-proxy ' ;
5+ import { type ProxyServer , createProxyServer } from 'httpxy ' ;
66
77import { verifyConfig } from './configuration' ;
88import { 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
0 commit comments