http-proxy-middleware.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import { createProxyServer } from 'httpxy';
  2. import { verifyConfig } from './configuration.js';
  3. import { Debug as debug } from './debug.js';
  4. import { getPlugins } from './get-plugins.js';
  5. import { getLogger } from './logger.js';
  6. import { matchPathFilter } from './path-filter.js';
  7. import { createPathRewriter } from './path-rewriter.js';
  8. import { getTarget } from './router.js';
  9. import { getFunctionName } from './utils/function.js';
  10. import { normalizeIPv6LiteralTargets } from './utils/ipv6.js';
  11. export class HttpProxyMiddleware {
  12. wsInternalSubscribedServers = new WeakSet();
  13. activeServers = new Set();
  14. proxyOptions;
  15. proxy;
  16. pathRewriter;
  17. logger;
  18. constructor(options) {
  19. verifyConfig(options);
  20. this.proxyOptions = options;
  21. this.logger = getLogger(options);
  22. debug(`create proxy server`);
  23. this.proxy = createProxyServer({});
  24. this.registerPlugins(this.proxy, this.proxyOptions);
  25. this.pathRewriter = createPathRewriter(this.proxyOptions.pathRewrite); // returns undefined when "pathRewrite" is not provided
  26. // https://github.com/chimurai/http-proxy-middleware/issues/19
  27. // expose function to upgrade externally
  28. this.middleware.upgrade = (req, socket, head) => {
  29. const server = this.#getServer(req);
  30. if (server && !this.wsInternalSubscribedServers.has(server)) {
  31. this.handleUpgrade(req, socket, head);
  32. }
  33. };
  34. }
  35. #getServer(req) {
  36. return req.socket?.server;
  37. }
  38. // https://github.com/Microsoft/TypeScript/wiki/'this'-in-TypeScript#red-flags-for-this
  39. middleware = (async (req, res, next) => {
  40. if (this.shouldProxy(this.proxyOptions.pathFilter, req)) {
  41. let activeProxyOptions;
  42. try {
  43. // Preparation Phase: Apply router and path rewriter.
  44. activeProxyOptions = await this.prepareProxyRequest(req, res);
  45. // [Smoking Gun] httpxy is inconsistent with error handling:
  46. // 1. If target is missing (here), it emits 'error' but returns a boolean (bypassing our catch/next).
  47. // 2. If a network error occurs (in proxy.web), it rejects the promise but SKIPS emitting 'error'.
  48. // We manually throw here to force Case 1 into the catch block so next(err) is called for Express.
  49. if (!activeProxyOptions.target && !activeProxyOptions.forward) {
  50. throw new Error('Must provide a proper URL as target');
  51. }
  52. }
  53. catch (err) {
  54. next?.(err);
  55. return;
  56. }
  57. try {
  58. // Proxying Phase: Handle the actual web request.
  59. debug(`proxy request to target: %O`, activeProxyOptions.target);
  60. await this.proxy.web(req, res, activeProxyOptions);
  61. }
  62. catch (err) {
  63. // Manually emit 'error' event because httpxy's promise-based API does not emit it automatically.
  64. // This is crucial for backward compatibility with HPM plugins (like error-response-plugin)
  65. // and custom listeners registered via the 'on: { error: ... }' option.
  66. this.proxy.emit('error', err, req, res, activeProxyOptions.target);
  67. next?.(err);
  68. }
  69. }
  70. else {
  71. next?.();
  72. }
  73. /**
  74. * Get the server object to subscribe to server events;
  75. * 'upgrade' for websocket and 'close' for graceful shutdown
  76. */
  77. const server = this.#getServer(req);
  78. if (server && !this.activeServers.has(server)) {
  79. debug('registering server close listener');
  80. this.activeServers.add(server);
  81. server.on('close', () => {
  82. debug('server close signal received.');
  83. this.activeServers.delete(server);
  84. if (this.activeServers.size > 0) {
  85. debug(`proxy server not closed: ${this.activeServers.size} server(s) still active`);
  86. return;
  87. }
  88. else {
  89. debug('closing proxy server');
  90. this.proxy.close(() => debug('proxy server closed'));
  91. }
  92. });
  93. }
  94. if (this.proxyOptions.ws === true && server) {
  95. // use initial request to access the server object to subscribe to http upgrade event
  96. this.catchUpgradeRequest(server);
  97. }
  98. });
  99. registerPlugins(proxy, options) {
  100. const plugins = getPlugins(options);
  101. plugins.forEach((plugin) => {
  102. debug(`register plugin: "${getFunctionName(plugin)}"`);
  103. plugin(proxy, options);
  104. });
  105. }
  106. catchUpgradeRequest = (server) => {
  107. if (!this.wsInternalSubscribedServers.has(server)) {
  108. debug('subscribing to server upgrade event');
  109. server.on('upgrade', this.handleUpgrade);
  110. this.wsInternalSubscribedServers.add(server);
  111. }
  112. };
  113. handleUpgrade = async (req, socket, head) => {
  114. try {
  115. if (this.shouldProxy(this.proxyOptions.pathFilter, req)) {
  116. // No HTTP response object exists during WebSocket upgrades, so pass undefined.
  117. const activeProxyOptions = await this.prepareProxyRequest(req, undefined);
  118. await this.proxy.ws(req, socket, activeProxyOptions, head);
  119. debug('server upgrade event received. Proxying WebSocket');
  120. }
  121. }
  122. catch (err) {
  123. // This error does not include the URL as the fourth argument as we won't
  124. // have the URL if `this.prepareProxyRequest` throws an error.
  125. this.proxy.emit('error', err, req, socket);
  126. }
  127. };
  128. /**
  129. * Determine whether request should be proxied.
  130. */
  131. shouldProxy = (pathFilter, req) => {
  132. try {
  133. return matchPathFilter(pathFilter, req.url, req);
  134. }
  135. catch (err) {
  136. debug('Error: matchPathFilter() called with request url: ', `"${req.url}"`);
  137. this.logger.error(err);
  138. return false;
  139. }
  140. };
  141. /**
  142. * Apply option.router and option.pathRewrite
  143. * Order matters:
  144. * Router uses original path for routing;
  145. * NOT the modified path, after it has been rewritten by pathRewrite
  146. * @param {Object} req
  147. * @return {Object} proxy options
  148. */
  149. prepareProxyRequest = async (req, res) => {
  150. const newProxyOptions = Object.assign({}, this.proxyOptions);
  151. // Apply in order:
  152. // 1. option.router
  153. // 2. option.pathRewrite
  154. await this.applyRouter(req, res, newProxyOptions);
  155. normalizeIPv6LiteralTargets(newProxyOptions);
  156. await this.applyPathRewrite(req, res, this.pathRewriter, newProxyOptions);
  157. return newProxyOptions;
  158. };
  159. // Modify option.target when router present.
  160. applyRouter = async (req, res, options) => {
  161. let newTarget;
  162. if (options.router) {
  163. newTarget = await getTarget(req, res, options);
  164. if (newTarget) {
  165. debug('router new target: "%s"', newTarget);
  166. options.target = newTarget;
  167. }
  168. }
  169. };
  170. // rewrite path
  171. applyPathRewrite = async (req, res, pathRewriter, options) => {
  172. if (req.url && pathRewriter) {
  173. const path = await pathRewriter(req.url, req, res, options);
  174. if (typeof path === 'string') {
  175. debug('pathRewrite new path: %s', path);
  176. req.url = path;
  177. }
  178. else {
  179. debug('pathRewrite: no rewritten path found: %s', req.url);
  180. }
  181. }
  182. };
  183. }