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

chimurai / http-proxy-middleware / 24939800669

25 Apr 2026 08:20PM UTC coverage: 94.78%. Remained the same
24939800669

push

github

web-flow
chore: improve typing 'req' (#1214)

171 of 190 branches covered (90.0%)

5 of 6 new or added lines in 1 file covered. (83.33%)

345 of 364 relevant lines covered (94.78%)

34.69 hits per line

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

96.1
/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 { createPathRewriter } from './path-rewriter.js';
13
import { getTarget } from './router.js';
14
import type { Filter, Logger, Options, RequestHandler } from './types.js';
15
import { getFunctionName } from './utils/function.js';
16
import { normalizeIPv6LiteralTargets } from './utils/ipv6.js';
17

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

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

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

37
    this.registerPlugins(this.proxy, this.proxyOptions);
100✔
38

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

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

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

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

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

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

90
    /**
91
     * Get the server object to subscribe to server events;
92
     * 'upgrade' for websocket and 'close' for graceful shutdown
93
     */
94
    const server = (req.socket as net.Socket & { server?: https.Server })?.server;
75✔
95

96
    if (server && !this.serverOnCloseSubscribed) {
78✔
97
      server.on('close', () => {
64✔
98
        debug('server close signal received: closing proxy server');
64✔
99
        this.proxy.close(() => {
64✔
100
          debug('proxy server closed');
×
101
        });
102
      });
103
      this.serverOnCloseSubscribed = true;
64✔
104
    }
105

106
    if (this.proxyOptions.ws === true && server) {
75✔
107
      // use initial request to access the server object to subscribe to http upgrade event
108
      this.catchUpgradeRequest(server);
3✔
109
    }
110
  }) as RequestHandler;
111

112
  private registerPlugins(proxy: ProxyServer<TReq, TRes>, options: Options<TReq, TRes>) {
113
    const plugins = getPlugins<TReq, TRes>(options);
100✔
114
    plugins.forEach((plugin) => {
100✔
115
      debug(`register plugin: "${getFunctionName(plugin)}"`);
397✔
116
      plugin(proxy, options);
397✔
117
    });
118
  }
119

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

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

145
  /**
146
   * Determine whether request should be proxied.
147
   */
148
  private shouldProxy = (
83✔
149
    pathFilter: Filter<TReq> | undefined,
150
    req: http.IncomingMessage,
151
  ): boolean => {
152
    try {
83✔
153
      return matchPathFilter(pathFilter, req.url, req);
83✔
154
    } catch (err) {
155
      debug('Error: matchPathFilter() called with request url: ', `"${req.url}"`);
1✔
156
      this.logger.error(err);
1✔
157
      return false;
1✔
158
    }
159
  };
160

161
  /**
162
   * Apply option.router and option.pathRewrite
163
   * Order matters:
164
   *    Router uses original path for routing;
165
   *    NOT the modified path, after it has been rewritten by pathRewrite
166
   * @param {Object} req
167
   * @return {Object} proxy options
168
   */
169
  private prepareProxyRequest = async (req: TReq) => {
69✔
170
    const newProxyOptions = Object.assign({}, this.proxyOptions);
69✔
171

172
    // Apply in order:
173
    // 1. option.router
174
    // 2. option.pathRewrite
175
    await this.applyRouter(req, newProxyOptions);
69✔
176
    normalizeIPv6LiteralTargets(newProxyOptions);
66✔
177
    await this.applyPathRewrite(req, this.pathRewriter);
66✔
178

179
    return newProxyOptions;
66✔
180
  };
181

182
  // Modify option.target when router present.
183
  private applyRouter = async (req: TReq, options: Options<TReq, TRes>) => {
69✔
184
    let newTarget;
185

186
    if (options.router) {
69✔
187
      newTarget = await getTarget(req, options);
16✔
188

189
      if (newTarget) {
13✔
190
        debug('router new target: "%s"', newTarget);
11✔
191
        options.target = newTarget;
11✔
192
      }
193
    }
194
  };
195

196
  // rewrite path
197
  private applyPathRewrite = async (
66✔
198
    req: TReq,
199
    pathRewriter: ReturnType<typeof createPathRewriter<TReq>>,
200
  ) => {
201
    if (req.url && pathRewriter) {
66✔
202
      const path = await pathRewriter(req.url, req);
12✔
203

204
      if (typeof path === 'string') {
12✔
205
        debug('pathRewrite new path: %s', path);
11✔
206
        req.url = path;
11✔
207
      } else {
208
        debug('pathRewrite: no rewritten path found: %s', req.url);
1✔
209
      }
210
    }
211
  };
212
}
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