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

chimurai / http-proxy-middleware / 23984008826

04 Apr 2026 05:33PM UTC coverage: 94.012% (-0.6%) from 94.595%
23984008826

push

github

web-flow
chore(package.json): bump to httpxy 0.5.0

148 of 164 branches covered (90.24%)

3 of 3 new or added lines in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

314 of 334 relevant lines covered (94.01%)

28.53 hits per line

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

96.05
/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';
6

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

17
export class HttpProxyMiddleware<
18
  TReq extends http.IncomingMessage = http.IncomingMessage,
19
  TRes extends http.ServerResponse = http.ServerResponse,
20
> {
21
  private wsInternalSubscribed = false;
80✔
22
  private serverOnCloseSubscribed = false;
80✔
23
  private proxyOptions: Options<TReq, TRes>;
24
  private proxy: ProxyServer<TReq, TRes>;
25
  private pathRewriter;
26
  private logger: Logger;
27

28
  constructor(options: Options<TReq, TRes>) {
29
    verifyConfig<TReq, TRes>(options);
80✔
30
    this.proxyOptions = options;
80✔
31
    this.logger = getLogger(options as unknown as Options);
80✔
32

33
    debug(`create proxy server`);
80✔
34
    this.proxy = createProxyServer({});
80✔
35

36
    this.registerPlugins(this.proxy, this.proxyOptions);
80✔
37

38
    this.pathRewriter = PathRewriter.createPathRewriter(this.proxyOptions.pathRewrite); // returns undefined when "pathRewrite" is not provided
80✔
39

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

49
  // https://github.com/Microsoft/TypeScript/wiki/'this'-in-TypeScript#red-flags-for-this
50
  public middleware: RequestHandler<TReq, TRes> = (async (
58✔
51
    req: TReq,
52
    res: TRes,
53
    next?: (err?: unknown) => void,
54
  ) => {
55
    if (this.shouldProxy(this.proxyOptions.pathFilter, req)) {
58✔
56
      let activeProxyOptions: Options<TReq, TRes>;
57
      try {
45✔
58
        // Preparation Phase: Apply router and path rewriter.
59
        activeProxyOptions = await this.prepareProxyRequest(req);
45✔
60

61
        // [Smoking Gun] httpxy is inconsistent with error handling:
62
        // 1. If target is missing (here), it emits 'error' but returns a boolean (bypassing our catch/next).
63
        // 2. If a network error occurs (in proxy.web), it rejects the promise but SKIPS emitting 'error'.
64
        // We manually throw here to force Case 1 into the catch block so next(err) is called for Express.
65
        if (!activeProxyOptions.target && !activeProxyOptions.forward) {
44✔
66
          throw new Error('Must provide a proper URL as target');
1✔
67
        }
68
      } catch (err) {
69
        next?.(err);
2✔
70
        return;
2✔
71
      }
72

73
      try {
43✔
74
        // Proxying Phase: Handle the actual web request.
75
        debug(`proxy request to target: %O`, activeProxyOptions.target);
43✔
76
        await this.proxy.web(req, res, activeProxyOptions);
43✔
77
      } catch (err) {
78
        // Manually emit 'error' event because httpxy's promise-based API does not emit it automatically.
79
        // This is crucial for backward compatibility with HPM plugins (like error-response-plugin)
80
        // and custom listeners registered via the 'on: { error: ... }' option.
81

82
        /**
83
         * TODO: Ideally, TReq and TRes should be restricted via "TReq extends http.IncomingMessage = http.IncomingMessage"
84
         * and "TRes extends http.ServerResponse = http.ServerResponse", which allows us to avoid the "req as TReq" below.
85
         *
86
         * However, making TReq and TRes constrained types may cause a breaking change for TypeScript users downstream.
87
         * So we leave this as a TODO for now, and revisit it in a future major release.
88
         */
UNCOV
89
        this.proxy.emit('error', err as Error, req as TReq, res, activeProxyOptions.target);
×
90

UNCOV
91
        next?.(err);
×
92
      }
93
    } else {
94
      next?.();
13✔
95
    }
96

97
    /**
98
     * Get the server object to subscribe to server events;
99
     * 'upgrade' for websocket and 'close' for graceful shutdown
100
     */
101
    const server: https.Server = (req.socket as any)?.server;
56✔
102

103
    if (server && !this.serverOnCloseSubscribed) {
58✔
104
      server.on('close', () => {
47✔
105
        debug('server close signal received: closing proxy server');
47✔
106
        this.proxy.close(() => {
47✔
107
          debug('proxy server closed');
×
108
        });
109
      });
110
      this.serverOnCloseSubscribed = true;
47✔
111
    }
112

113
    if (this.proxyOptions.ws === true && server) {
56✔
114
      // use initial request to access the server object to subscribe to http upgrade event
115
      this.catchUpgradeRequest(server);
3✔
116
    }
117
  }) as RequestHandler;
118

119
  private registerPlugins(proxy: ProxyServer<TReq, TRes>, options: Options<TReq, TRes>) {
120
    const plugins = getPlugins<TReq, TRes>(options);
80✔
121
    plugins.forEach((plugin) => {
80✔
122
      debug(`register plugin: "${getFunctionName(plugin)}"`);
317✔
123
      plugin(proxy, options);
317✔
124
    });
125
  }
126

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

137
  private handleUpgrade = async (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
5✔
138
    try {
5✔
139
      if (this.shouldProxy(this.proxyOptions.pathFilter, req)) {
5!
140
        const proxiedReq = req as TReq;
5✔
141
        const activeProxyOptions = await this.prepareProxyRequest(proxiedReq);
5✔
142
        await this.proxy.ws(proxiedReq, socket, activeProxyOptions, head);
4✔
143
        debug('server upgrade event received. Proxying WebSocket');
4✔
144
      }
145
    } catch (err) {
146
      // This error does not include the URL as the fourth argument as we won't
147
      // have the URL if `this.prepareProxyRequest` throws an error.
148

149
      /**
150
       * TODO: Ideally, TReq and TRes should be restricted via "TReq extends http.IncomingMessage = http.IncomingMessage"
151
       * and "TRes extends http.ServerResponse = http.ServerResponse", which allows us to avoid the "req as TReq" below.
152
       *
153
       * However, making TReq and TRes constrained types may cause a breaking change for TypeScript users downstream.
154
       * So we leave this as a TODO for now, and revisit it in a future major release.
155
       */
156
      this.proxy.emit('error', err as Error, req as TReq, socket);
1✔
157
    }
158
  };
159

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

176
  /**
177
   * Apply option.router and option.pathRewrite
178
   * Order matters:
179
   *    Router uses original path for routing;
180
   *    NOT the modified path, after it has been rewritten by pathRewrite
181
   * @param {Object} req
182
   * @return {Object} proxy options
183
   */
184
  private prepareProxyRequest = async (req: http.IncomingMessage) => {
50✔
185
    const newProxyOptions = Object.assign({}, this.proxyOptions);
50✔
186

187
    // Apply in order:
188
    // 1. option.router
189
    // 2. option.pathRewrite
190
    await this.applyRouter(req, newProxyOptions);
50✔
191
    await this.applyPathRewrite(req, this.pathRewriter);
48✔
192

193
    return newProxyOptions;
48✔
194
  };
195

196
  // Modify option.target when router present.
197
  private applyRouter = async (req: http.IncomingMessage, options: Options<TReq, TRes>) => {
50✔
198
    let newTarget;
199

200
    if (options.router) {
50✔
201
      newTarget = await Router.getTarget(req, options);
13✔
202

203
      if (newTarget) {
11✔
204
        debug('router new target: "%s"', newTarget);
9✔
205
        options.target = newTarget;
9✔
206
      }
207
    }
208
  };
209

210
  // rewrite path
211
  private applyPathRewrite = async (req: http.IncomingMessage, pathRewriter) => {
48✔
212
    if (pathRewriter) {
48✔
213
      const path = await pathRewriter(req.url, req);
12✔
214

215
      if (typeof path === 'string') {
12✔
216
        debug('pathRewrite new path: %s', path);
11✔
217
        req.url = path;
11✔
218
      } else {
219
        debug('pathRewrite: no rewritten path found: %s', req.url);
1✔
220
      }
221
    }
222
  };
223
}
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