• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

chimurai / http-proxy-middleware / 23962524961

03 Apr 2026 09:16PM UTC coverage: 96.544% (-0.6%) from 97.15%
23962524961

push

github

web-flow
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

175 of 191 branches covered (91.62%)

23 of 25 new or added lines in 4 files covered. (92.0%)

1 existing line in 1 file now uncovered.

419 of 434 relevant lines covered (96.54%)

24.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.85
/src/http-proxy-middleware.ts
1
import type * as http from 'node:http';
2
import type * as https from 'node:https';
3
import type * as net from 'node:net';
4

5
import { type ProxyServer, createProxyServer } from 'httpxy';
12✔
6

7
import { verifyConfig } from './configuration';
12✔
8
import { Debug as debug } from './debug';
12✔
9
import { getPlugins } from './get-plugins';
12✔
10
import { getLogger } from './logger';
12✔
11
import { matchPathFilter } from './path-filter';
12✔
12
import * as PathRewriter from './path-rewriter';
12✔
13
import * as Router from './router';
12✔
14
import type { Filter, Logger, Options, RequestHandler } from './types';
15
import { getFunctionName } from './utils/function';
12✔
16

17
export class HttpProxyMiddleware<TReq, TRes> {
12✔
18
  private wsInternalSubscribed = false;
75✔
19
  private serverOnCloseSubscribed = false;
75✔
20
  private proxyOptions: Options<TReq, TRes>;
21
  private proxy: ProxyServer<TReq, TRes>;
22
  private pathRewriter;
23
  private logger: Logger;
24

25
  constructor(options: Options<TReq, TRes>) {
26
    verifyConfig<TReq, TRes>(options);
75✔
27
    this.proxyOptions = options;
75✔
28
    this.logger = getLogger(options as unknown as Options);
75✔
29

30
    debug(`create proxy server`);
75✔
31
    this.proxy = createProxyServer({});
75✔
32

33
    this.registerPlugins(this.proxy, this.proxyOptions);
75✔
34

35
    this.pathRewriter = PathRewriter.createPathRewriter(this.proxyOptions.pathRewrite); // returns undefined when "pathRewrite" is not provided
75✔
36

37
    // https://github.com/chimurai/http-proxy-middleware/issues/19
38
    // expose function to upgrade externally
39
    this.middleware.upgrade = (req, socket, head) => {
75✔
40
      if (!this.wsInternalSubscribed) {
3!
41
        this.handleUpgrade(req, socket, head);
3✔
42
      }
43
    };
44
  }
45

46
  // https://github.com/Microsoft/TypeScript/wiki/'this'-in-TypeScript#red-flags-for-this
47
  public middleware: RequestHandler = (async (req, res, next?) => {
75✔
48
    if (this.shouldProxy(this.proxyOptions.pathFilter, req)) {
55✔
49
      let activeProxyOptions: Options<TReq, TRes>;
50
      try {
49✔
51
        // Preparation Phase: Apply router and path rewriter.
52
        activeProxyOptions = await this.prepareProxyRequest(req);
49✔
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) {
48✔
59
          throw new Error('Must provide a proper URL as target');
1✔
60
        }
61
      } catch (err) {
62
        next?.(err);
2✔
63
        return;
2✔
64
      }
65

66
      try {
47✔
67
        // Proxying Phase: Handle the actual web request.
68
        debug(`proxy request to target: %O`, activeProxyOptions.target);
47✔
69
        await this.proxy.web(req, res, activeProxyOptions);
47✔
70
      } 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);
4✔
83

84
        next?.(err);
4✔
85
      }
86
    } else {
87
      next?.();
6✔
88
    }
89

90
    /**
91
     * Get the server object to subscribe to server events;
92
     * 'upgrade' for websocket and 'close' for graceful shutdown
93
     *
94
     * NOTE:
95
     * req.socket: node >= 13
96
     * req.connection: node < 13 (Remove this when node 12/13 support is dropped)
97
     */
98
    const server: https.Server = ((req.socket ?? req.connection) as any)?.server;
53✔
99

100
    if (server && !this.serverOnCloseSubscribed) {
53✔
101
      server.on('close', () => {
51✔
102
        debug('server close signal received: closing proxy server');
51✔
103
        this.proxy.close(() => {
51✔
NEW
104
          debug('proxy server closed');
×
105
        });
106
      });
107
      this.serverOnCloseSubscribed = true;
51✔
108
    }
109

110
    if (this.proxyOptions.ws === true) {
53✔
111
      // use initial request to access the server object to subscribe to http upgrade event
112
      this.catchUpgradeRequest(server);
2✔
113
    }
114
  }) as RequestHandler;
115

116
  private registerPlugins(proxy: ProxyServer<TReq, TRes>, options: Options<TReq, TRes>) {
117
    const plugins = getPlugins<TReq, TRes>(options);
75✔
118
    plugins.forEach((plugin) => {
75✔
119
      debug(`register plugin: "${getFunctionName(plugin)}"`);
296✔
120
      plugin(proxy, options);
296✔
121
    });
122
  }
123

124
  private catchUpgradeRequest = (server: https.Server) => {
75✔
125
    if (!this.wsInternalSubscribed) {
2✔
126
      debug('subscribing to server upgrade event');
1✔
127
      server.on('upgrade', this.handleUpgrade);
1✔
128
      // prevent duplicate upgrade handling;
129
      // in case external upgrade is also configured
130
      this.wsInternalSubscribed = true;
1✔
131
    }
132
  };
133

134
  private handleUpgrade = async (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
75✔
135
    try {
4✔
136
      if (this.shouldProxy(this.proxyOptions.pathFilter, req)) {
4!
137
        const activeProxyOptions = await this.prepareProxyRequest(req);
4✔
138
        await this.proxy.ws(req, socket, activeProxyOptions, head);
3✔
139
        debug('server upgrade event received. Proxying WebSocket');
3✔
140
      }
141
    } catch (err) {
142
      // This error does not include the URL as the fourth argument as we won't
143
      // have the URL if `this.prepareProxyRequest` throws an error.
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);
1✔
153
    }
154
  };
155

156
  /**
157
   * Determine whether request should be proxied.
158
   */
159
  private shouldProxy = (
75✔
160
    pathFilter: Filter<TReq> | undefined,
161
    req: http.IncomingMessage,
162
  ): boolean => {
163
    try {
59✔
164
      return matchPathFilter(pathFilter, req.url, req);
59✔
165
    } catch (err) {
166
      debug('Error: matchPathFilter() called with request url: ', `"${req.url}"`);
1✔
167
      this.logger.error(err);
1✔
168
      return false;
1✔
169
    }
170
  };
171

172
  /**
173
   * Apply option.router and option.pathRewrite
174
   * Order matters:
175
   *    Router uses original path for routing;
176
   *    NOT the modified path, after it has been rewritten by pathRewrite
177
   * @param {Object} req
178
   * @return {Object} proxy options
179
   */
180
  private prepareProxyRequest = async (req: http.IncomingMessage) => {
75✔
181
    /**
182
     * Incorrect usage confirmed: https://github.com/expressjs/express/issues/4854#issuecomment-1066171160
183
     * Temporary restore req.url patch for {@link src/legacy/create-proxy-middleware.ts legacyCreateProxyMiddleware()}
184
     * FIXME: remove this patch in future release
185
     */
186
    if ((this.middleware as unknown as any).__LEGACY_HTTP_PROXY_MIDDLEWARE__) {
53✔
187
      req.url = (req as unknown as any).originalUrl || req.url;
5!
188
    }
189

190
    const newProxyOptions = Object.assign({}, this.proxyOptions);
53✔
191

192
    // Apply in order:
193
    // 1. option.router
194
    // 2. option.pathRewrite
195
    await this.applyRouter(req, newProxyOptions);
53✔
196
    await this.applyPathRewrite(req, this.pathRewriter);
51✔
197

198
    return newProxyOptions;
51✔
199
  };
200

201
  // Modify option.target when router present.
202
  private applyRouter = async (req: http.IncomingMessage, options: Options<TReq, TRes>) => {
75✔
203
    let newTarget;
204

205
    if (options.router) {
53✔
206
      newTarget = await Router.getTarget(req, options);
13✔
207

208
      if (newTarget) {
11✔
209
        debug('router new target: "%s"', newTarget);
9✔
210
        options.target = newTarget;
9✔
211
      }
212
    }
213
  };
214

215
  // rewrite path
216
  private applyPathRewrite = async (req: http.IncomingMessage, pathRewriter) => {
75✔
217
    if (pathRewriter) {
51✔
218
      const path = await pathRewriter(req.url, req);
10✔
219

220
      if (typeof path === 'string') {
10✔
221
        debug('pathRewrite new path: %s', req.url);
9✔
222
        req.url = path;
9✔
223
      } else {
224
        debug('pathRewrite: no rewritten path found: %s', req.url);
1✔
225
      }
226
    }
227
  };
228
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc