@ohos-ports/http-proxy-middleware 0.0.0-stage → 4.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +670 -2
- package/dist/configuration.d.ts +3 -0
- package/dist/configuration.js +6 -0
- package/dist/debug.d.ts +5 -0
- package/dist/debug.js +5 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +15 -0
- package/dist/factory-hono.d.ts +28 -0
- package/dist/factory-hono.js +45 -0
- package/dist/factory.d.ts +79 -0
- package/dist/factory.js +81 -0
- package/dist/get-plugins.d.ts +3 -0
- package/dist/get-plugins.js +10 -0
- package/dist/handlers/fix-request-body-utils/stringify-form-data.d.ts +12 -0
- package/dist/handlers/fix-request-body-utils/stringify-form-data.js +46 -0
- package/dist/handlers/fix-request-body.d.ts +22 -0
- package/dist/handlers/fix-request-body.js +78 -0
- package/dist/handlers/index.d.ts +1 -0
- package/dist/handlers/index.js +1 -0
- package/dist/handlers/public.d.ts +2 -0
- package/dist/handlers/public.js +2 -0
- package/dist/handlers/response-interceptor.d.ts +27 -0
- package/dist/handlers/response-interceptor.js +145 -0
- package/dist/http-proxy-middleware.d.ts +31 -0
- package/dist/http-proxy-middleware.js +183 -0
- package/dist/index-hono.d.ts +10 -0
- package/dist/index-hono.js +10 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +21 -0
- package/dist/path-filter.d.ts +3 -0
- package/dist/path-filter.js +76 -0
- package/dist/path-rewriter.d.ts +6 -0
- package/dist/path-rewriter.js +59 -0
- package/dist/plugins/default/debug-proxy-errors-plugin.d.ts +6 -0
- package/dist/plugins/default/debug-proxy-errors-plugin.js +77 -0
- package/dist/plugins/default/error-response-plugin.d.ts +2 -0
- package/dist/plugins/default/error-response-plugin.js +28 -0
- package/dist/plugins/default/index.d.ts +4 -0
- package/dist/plugins/default/index.js +4 -0
- package/dist/plugins/default/logger-plugin.d.ts +2 -0
- package/dist/plugins/default/logger-plugin.js +57 -0
- package/dist/plugins/default/proxy-events.d.ts +22 -0
- package/dist/plugins/default/proxy-events.js +42 -0
- package/dist/plugins/define-plugin.d.ts +25 -0
- package/dist/plugins/define-plugin.js +25 -0
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +4 -0
- package/dist/router.d.ts +3 -0
- package/dist/router.js +60 -0
- package/dist/status-code.d.ts +1 -0
- package/dist/status-code.js +23 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.js +1 -0
- package/dist/utils/create-url.d.ts +9 -0
- package/dist/utils/create-url.js +17 -0
- package/dist/utils/function.d.ts +1 -0
- package/dist/utils/function.js +4 -0
- package/dist/utils/ipv6.d.ts +20 -0
- package/dist/utils/ipv6.js +66 -0
- package/dist/utils/logger-plugin.d.ts +7 -0
- package/dist/utils/logger-plugin.js +7 -0
- package/dist/utils/sanitize.d.ts +1 -0
- package/dist/utils/sanitize.js +3 -0
- package/package.json +120 -4
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type * as http from 'node:http';
|
|
2
|
+
import type { Options, RequestHandler } from './types.js';
|
|
3
|
+
export declare class HttpProxyMiddleware<TReq extends http.IncomingMessage = http.IncomingMessage, TRes extends http.ServerResponse = http.ServerResponse> {
|
|
4
|
+
#private;
|
|
5
|
+
private wsInternalSubscribedServers;
|
|
6
|
+
private activeServers;
|
|
7
|
+
private proxyOptions;
|
|
8
|
+
private proxy;
|
|
9
|
+
private pathRewriter;
|
|
10
|
+
private logger;
|
|
11
|
+
constructor(options: Options<TReq, TRes>);
|
|
12
|
+
middleware: RequestHandler<TReq, TRes>;
|
|
13
|
+
private registerPlugins;
|
|
14
|
+
private catchUpgradeRequest;
|
|
15
|
+
private handleUpgrade;
|
|
16
|
+
/**
|
|
17
|
+
* Determine whether request should be proxied.
|
|
18
|
+
*/
|
|
19
|
+
private shouldProxy;
|
|
20
|
+
/**
|
|
21
|
+
* Apply option.router and option.pathRewrite
|
|
22
|
+
* Order matters:
|
|
23
|
+
* Router uses original path for routing;
|
|
24
|
+
* NOT the modified path, after it has been rewritten by pathRewrite
|
|
25
|
+
* @param {Object} req
|
|
26
|
+
* @return {Object} proxy options
|
|
27
|
+
*/
|
|
28
|
+
private prepareProxyRequest;
|
|
29
|
+
private applyRouter;
|
|
30
|
+
private applyPathRewrite;
|
|
31
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono-specific API entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* This is intentionally published as a dedicated subpath (`http-proxy-middleware/hono`)
|
|
5
|
+
* so the root package types do not import `hono` / `@hono/node-server`.
|
|
6
|
+
*
|
|
7
|
+
* Keeping these exports out of the root entrypoint prevents non-Hono consumers from
|
|
8
|
+
* getting TypeScript module-resolution errors for optional Hono dependencies.
|
|
9
|
+
*/
|
|
10
|
+
export { createHonoProxyMiddleware } from './factory-hono.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono-specific API entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* This is intentionally published as a dedicated subpath (`http-proxy-middleware/hono`)
|
|
5
|
+
* so the root package types do not import `hono` / `@hono/node-server`.
|
|
6
|
+
*
|
|
7
|
+
* Keeping these exports out of the root entrypoint prevents non-Hono consumers from
|
|
8
|
+
* getting TypeScript module-resolution errors for optional Hono dependencies.
|
|
9
|
+
*/
|
|
10
|
+
export { createHonoProxyMiddleware } from './factory-hono.js';
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/logger.d.ts
ADDED
package/dist/logger.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility matrix
|
|
3
|
+
*
|
|
4
|
+
| Library | log | info | warn | error | \<interpolation\> |
|
|
5
|
+
|----------|:------|:-------|:------|:--------|:------------------|
|
|
6
|
+
| console | ✅ | ✅ | ✅ | ✅ | ✅ (%s %o %O) |
|
|
7
|
+
| bunyan | ❌ | ✅ | ✅ | ✅ | ✅ (%s %o %O) |
|
|
8
|
+
| pino | ❌ | ✅ | ✅ | ✅ | ✅ (%s %o %O) |
|
|
9
|
+
| winston | ❌ | ✅ | ✅ | ✅ | ✅ (%s %o %O)^1 |
|
|
10
|
+
| log4js | ❌ | ✅ | ✅ | ✅ | ✅ (%s %o %O) |
|
|
11
|
+
*
|
|
12
|
+
* ^1: https://github.com/winstonjs/winston#string-interpolation
|
|
13
|
+
*/
|
|
14
|
+
const noopLogger = {
|
|
15
|
+
info: () => { },
|
|
16
|
+
warn: () => { },
|
|
17
|
+
error: () => { },
|
|
18
|
+
};
|
|
19
|
+
export function getLogger(options) {
|
|
20
|
+
return options.logger || noopLogger;
|
|
21
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type * as http from 'node:http';
|
|
2
|
+
import type { Filter } from './types.js';
|
|
3
|
+
export declare function matchPathFilter<TReq extends http.IncomingMessage = http.IncomingMessage>(pathFilter: Filter<TReq> | undefined, uri: string | undefined, req: http.IncomingMessage): boolean;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import isGlob from 'is-glob';
|
|
2
|
+
import micromatch from 'micromatch';
|
|
3
|
+
import { HttpProxyMiddlewareError } from './errors.js';
|
|
4
|
+
export function matchPathFilter(pathFilter = '/', uri, req) {
|
|
5
|
+
// single path
|
|
6
|
+
if (isStringPath(pathFilter)) {
|
|
7
|
+
return matchSingleStringPath(pathFilter, uri);
|
|
8
|
+
}
|
|
9
|
+
// single glob path
|
|
10
|
+
if (isGlobPath(pathFilter)) {
|
|
11
|
+
return matchSingleGlobPath(pathFilter, uri);
|
|
12
|
+
}
|
|
13
|
+
// multi path
|
|
14
|
+
if (Array.isArray(pathFilter)) {
|
|
15
|
+
if (pathFilter.every(isStringPath)) {
|
|
16
|
+
return matchMultiPath(pathFilter, uri);
|
|
17
|
+
}
|
|
18
|
+
if (pathFilter.every(isGlobPath)) {
|
|
19
|
+
return matchMultiGlobPath(pathFilter, uri);
|
|
20
|
+
}
|
|
21
|
+
throw new HttpProxyMiddlewareError('[HPM] Invalid pathFilter. Plain paths (e.g. "/api") can not be mixed with globs (e.g. "/api/**"). Expecting something like: ["/api", "/ajax"] or ["/api/**", "!**.html"].', 'HPM_INVALID_PATH_FILTER_ARRAY_CONFIG');
|
|
22
|
+
}
|
|
23
|
+
// custom matching
|
|
24
|
+
if (typeof pathFilter === 'function') {
|
|
25
|
+
const pathname = getUrlPathName(uri);
|
|
26
|
+
return Boolean(pathFilter(pathname, req));
|
|
27
|
+
}
|
|
28
|
+
throw new HttpProxyMiddlewareError('[HPM] Invalid pathFilter. Expecting something like: "/api" or ["/api", "/ajax"]', 'HPM_INVALID_PATH_FILTER_CONFIG');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* @param {String} pathFilter '/api'
|
|
32
|
+
* @param {String} uri 'http://example.org/api/b/c/d.html'
|
|
33
|
+
* @return {Boolean}
|
|
34
|
+
*/
|
|
35
|
+
function matchSingleStringPath(pathFilter, uri) {
|
|
36
|
+
const pathname = getUrlPathName(uri);
|
|
37
|
+
return pathname?.indexOf(pathFilter) === 0;
|
|
38
|
+
}
|
|
39
|
+
function matchSingleGlobPath(pattern, uri) {
|
|
40
|
+
const pathname = getUrlPathName(uri);
|
|
41
|
+
const matches = micromatch([pathname], pattern);
|
|
42
|
+
return matches && matches.length > 0;
|
|
43
|
+
}
|
|
44
|
+
function matchMultiGlobPath(patternList, uri) {
|
|
45
|
+
return matchSingleGlobPath(patternList, uri);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* @param {String} pathFilterList ['/api', '/ajax']
|
|
49
|
+
* @param {String} uri 'http://example.org/api/b/c/d.html'
|
|
50
|
+
* @return {Boolean}
|
|
51
|
+
*/
|
|
52
|
+
function matchMultiPath(pathFilterList, uri) {
|
|
53
|
+
let isMultiPath = false;
|
|
54
|
+
for (const context of pathFilterList) {
|
|
55
|
+
if (matchSingleStringPath(context, uri)) {
|
|
56
|
+
isMultiPath = true;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return isMultiPath;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Parses URI and returns RFC 3986 path
|
|
64
|
+
*
|
|
65
|
+
* @param {String} uri from req.url
|
|
66
|
+
* @return {String} RFC 3986 path
|
|
67
|
+
*/
|
|
68
|
+
function getUrlPathName(uri) {
|
|
69
|
+
return uri && new URL(uri, 'http://0.0.0.0').pathname;
|
|
70
|
+
}
|
|
71
|
+
function isStringPath(pathFilter) {
|
|
72
|
+
return typeof pathFilter === 'string' && !isGlob(pathFilter);
|
|
73
|
+
}
|
|
74
|
+
function isGlobPath(pathFilter) {
|
|
75
|
+
return isGlob(pathFilter);
|
|
76
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { PathRewriteConfig } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Create rewrite function, to cache parsed rewrite rules.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createPathRewriter<TReq extends IncomingMessage = IncomingMessage, TRes extends ServerResponse = ServerResponse>(rewriteConfig: PathRewriteConfig<TReq, TRes> | undefined): ((path: string, req: TReq, res?: TRes | undefined, options?: import("./types.js").Options<TReq, TRes> | undefined) => string | undefined) | ((path: string, req: TReq, res?: TRes | undefined, options?: import("./types.js").Options<TReq, TRes> | undefined) => Promise<string | undefined>) | undefined;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import isPlainObject from 'is-plain-obj';
|
|
2
|
+
import { Debug } from './debug.js';
|
|
3
|
+
import { HttpProxyMiddlewareError } from './errors.js';
|
|
4
|
+
const debug = Debug.extend('path-rewriter');
|
|
5
|
+
/**
|
|
6
|
+
* Create rewrite function, to cache parsed rewrite rules.
|
|
7
|
+
*/
|
|
8
|
+
export function createPathRewriter(rewriteConfig) {
|
|
9
|
+
let rulesCache;
|
|
10
|
+
if (!isValidRewriteConfig(rewriteConfig)) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (typeof rewriteConfig === 'function') {
|
|
14
|
+
const customRewriteFn = rewriteConfig;
|
|
15
|
+
return customRewriteFn;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
rulesCache = parsePathRewriteRules(rewriteConfig);
|
|
19
|
+
return rewritePath;
|
|
20
|
+
}
|
|
21
|
+
function rewritePath(path) {
|
|
22
|
+
let result = path;
|
|
23
|
+
for (const rule of rulesCache) {
|
|
24
|
+
if (rule.regex.test(path)) {
|
|
25
|
+
result = result.replace(rule.regex, rule.value);
|
|
26
|
+
debug('rewriting path from "%s" to "%s"', path, result);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function isValidRewriteConfig(rewriteConfig) {
|
|
34
|
+
if (typeof rewriteConfig === 'function') {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
else if (isPlainObject(rewriteConfig)) {
|
|
38
|
+
return Object.keys(rewriteConfig).length !== 0;
|
|
39
|
+
}
|
|
40
|
+
else if (rewriteConfig === undefined || rewriteConfig === null) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
throw new HttpProxyMiddlewareError('[HPM] Invalid pathRewrite config. Expecting object with pathRewrite config or a rewrite function', 'HPM_INVALID_PATH_REWRITER_CONFIG');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function parsePathRewriteRules(rewriteConfig) {
|
|
48
|
+
const rules = [];
|
|
49
|
+
if (isPlainObject(rewriteConfig)) {
|
|
50
|
+
for (const [key, value] of Object.entries(rewriteConfig)) {
|
|
51
|
+
rules.push({
|
|
52
|
+
regex: new RegExp(key),
|
|
53
|
+
value: value,
|
|
54
|
+
});
|
|
55
|
+
debug('rewrite rule created: "%s" ~> "%s"', key, value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return rules;
|
|
59
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Plugin } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Subscribe to {@link https://github.com/unjs/httpxy#events `httpxy` error events} to prevent server from crashing.
|
|
4
|
+
* Errors are logged with {@link https://www.npmjs.com/package/debug debug} library.
|
|
5
|
+
*/
|
|
6
|
+
export declare const debugProxyErrorsPlugin: Plugin;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { Debug } from '../../debug.js';
|
|
3
|
+
import { definePlugin } from '../define-plugin.js';
|
|
4
|
+
const debug = Debug.extend('debug-proxy-errors-plugin');
|
|
5
|
+
const BODY_PARSER_ERROR_MESSAGE = `[HPM] Connection reset (ECONNRESET) detected with non-empty "req.body" [ERR_HPM.GH40].
|
|
6
|
+
|
|
7
|
+
This usually means that the POST request body (req.body) was already parsed before reaching the proxy.
|
|
8
|
+
When bodyParser runs first, it consumes the request stream, leaving the proxy unable to forward the body data to the target server.
|
|
9
|
+
|
|
10
|
+
How to fix this issue:
|
|
11
|
+
- Option 1: Place the proxy middleware before the bodyParser middleware.
|
|
12
|
+
- Option 2: Use 'fixRequestBody()' helper to fix this issue.
|
|
13
|
+
|
|
14
|
+
For more details, see: https://github.com/chimurai/http-proxy-middleware/issues/40\n`;
|
|
15
|
+
function hasParsedBody(req) {
|
|
16
|
+
return Boolean(req && req.method === 'POST' && 'body' in req && req.body);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Subscribe to {@link https://github.com/unjs/httpxy#events `httpxy` error events} to prevent server from crashing.
|
|
20
|
+
* Errors are logged with {@link https://www.npmjs.com/package/debug debug} library.
|
|
21
|
+
*/
|
|
22
|
+
export const debugProxyErrorsPlugin = definePlugin((proxyServer, options) => {
|
|
23
|
+
/**
|
|
24
|
+
* The old `http-proxy` doesn't handle any errors by default (https://github.com/http-party/node-http-proxy#listening-for-proxy-events)
|
|
25
|
+
* > We do not do any error handling of messages passed between client and proxy, and messages passed between proxy and target, so it is recommended that you listen on errors and handle them.
|
|
26
|
+
* Subscribing to error event to prevent server from crashing
|
|
27
|
+
*/
|
|
28
|
+
proxyServer.on('error', (error, req, res, target) => {
|
|
29
|
+
debug(`httpxy error event: \n%O`, error);
|
|
30
|
+
// detect request body (when bodyParser used) and log an error message to help debugging
|
|
31
|
+
if (error.code === 'ECONNRESET' && hasParsedBody(req)) {
|
|
32
|
+
console.error(styleText('red', BODY_PARSER_ERROR_MESSAGE));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
proxyServer.on('proxyReq', (proxyReq, req, socket) => {
|
|
36
|
+
socket.on('error', (error) => {
|
|
37
|
+
debug('Socket error in proxyReq event: \n%O', error);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* Fix SSE close events
|
|
42
|
+
* @link https://github.com/chimurai/http-proxy-middleware/issues/678
|
|
43
|
+
* @link https://github.com/http-party/node-http-proxy/issues/1520#issue-877626125
|
|
44
|
+
*/
|
|
45
|
+
proxyServer.on('proxyRes', (proxyRes, req, res) => {
|
|
46
|
+
res.on('close', () => {
|
|
47
|
+
if (!res.writableEnded) {
|
|
48
|
+
debug('Destroying proxyRes in proxyRes close event');
|
|
49
|
+
proxyRes.destroy();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
/**
|
|
54
|
+
* Fix crash when target server restarts
|
|
55
|
+
* https://github.com/chimurai/http-proxy-middleware/issues/476#issuecomment-746329030
|
|
56
|
+
* https://github.com/webpack/webpack-dev-server/issues/1642#issuecomment-790602225
|
|
57
|
+
*/
|
|
58
|
+
proxyServer.on('proxyReqWs', (proxyReq, req, socket) => {
|
|
59
|
+
socket.on('error', (error) => {
|
|
60
|
+
debug('Socket error in proxyReqWs event: \n%O', error);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
proxyServer.on('open', (proxySocket) => {
|
|
64
|
+
proxySocket.on('error', (error) => {
|
|
65
|
+
debug('Socket error in open event: \n%O', error);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
proxyServer.on('close', (req, socket, head) => {
|
|
69
|
+
socket.on('error', (error) => {
|
|
70
|
+
debug('Socket error in close event: \n%O', error);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
// https://github.com/webpack/webpack-dev-server/issues/1642#issuecomment-1103136590
|
|
74
|
+
proxyServer.on('econnreset', (error, req, res, target) => {
|
|
75
|
+
debug(`httpxy econnreset event: \n%O`, error);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getStatusCode } from '../../status-code.js';
|
|
2
|
+
import { sanitize } from '../../utils/sanitize.js';
|
|
3
|
+
import { definePlugin } from '../define-plugin.js';
|
|
4
|
+
function isResponseLike(obj) {
|
|
5
|
+
return obj && typeof obj.writeHead === 'function';
|
|
6
|
+
}
|
|
7
|
+
function isSocketLike(obj) {
|
|
8
|
+
return obj && typeof obj.write === 'function' && !('writeHead' in obj);
|
|
9
|
+
}
|
|
10
|
+
export const errorResponsePlugin = definePlugin((proxyServer, options) => {
|
|
11
|
+
proxyServer.on('error', (err, req, res, target) => {
|
|
12
|
+
// Re-throw error. Not recoverable since req & res are empty.
|
|
13
|
+
if (!req || !res) {
|
|
14
|
+
throw err; // "Error: Must provide a proper URL as target"
|
|
15
|
+
}
|
|
16
|
+
if (isResponseLike(res)) {
|
|
17
|
+
if (!res.headersSent) {
|
|
18
|
+
const statusCode = getStatusCode(err.code);
|
|
19
|
+
res.writeHead(statusCode);
|
|
20
|
+
}
|
|
21
|
+
const host = req.headers && req.headers.host;
|
|
22
|
+
res.end(`Error occurred while trying to proxy: ${sanitize(host)}${sanitize(req.url)}`);
|
|
23
|
+
}
|
|
24
|
+
else if (isSocketLike(res)) {
|
|
25
|
+
res.destroy();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { URL } from 'node:url';
|
|
2
|
+
import { getLogger } from '../../logger.js';
|
|
3
|
+
import { createUrl } from '../../utils/create-url.js';
|
|
4
|
+
import { getPort } from '../../utils/logger-plugin.js';
|
|
5
|
+
import { definePlugin } from '../define-plugin.js';
|
|
6
|
+
export const loggerPlugin = definePlugin((proxyServer, options) => {
|
|
7
|
+
const logger = getLogger(options);
|
|
8
|
+
proxyServer.on('error', (err, req, res, target) => {
|
|
9
|
+
const hostname = req?.headers?.host;
|
|
10
|
+
const requestHref = `${hostname}${req?.url}`;
|
|
11
|
+
const targetHref = `${target?.href}`; // target is undefined when websocket errors
|
|
12
|
+
const errorMessage = '[HPM] Error occurred while proxying request %s to %s [%s] (%s)';
|
|
13
|
+
const errReference = 'https://nodejs.org/api/errors.html#errors_common_system_errors'; // link to Node Common Systems Errors page
|
|
14
|
+
logger.error(errorMessage, requestHref, targetHref, err.code || err, errReference);
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Log request and response
|
|
18
|
+
* @example
|
|
19
|
+
* ```shell
|
|
20
|
+
* [HPM] GET /users/ -> http://jsonplaceholder.typicode.com/users/ [304]
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
proxyServer.on('proxyRes', (proxyRes, req, res) => {
|
|
24
|
+
// BrowserSync uses req.originalUrl
|
|
25
|
+
// Next.js doesn't have req.baseUrl
|
|
26
|
+
const originalUrl = req.originalUrl ?? `${req.baseUrl || ''}${req.url}`;
|
|
27
|
+
// construct targetUrl
|
|
28
|
+
let target;
|
|
29
|
+
try {
|
|
30
|
+
const port = getPort(proxyRes.req?.agent?.sockets);
|
|
31
|
+
const { protocol, host, path } = proxyRes.req;
|
|
32
|
+
target = createUrl({ protocol, host, port, path });
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
// should not error. keeping fallback just in case
|
|
36
|
+
console.error('[HPM] Unexpected error while creating target URL', err);
|
|
37
|
+
// fallback to old implementation (less correct - without port)
|
|
38
|
+
target = new URL(options.target);
|
|
39
|
+
target.pathname = proxyRes.req.path;
|
|
40
|
+
}
|
|
41
|
+
const targetUrl = target.toString();
|
|
42
|
+
const exchange = `[HPM] ${req.method} ${originalUrl} -> ${targetUrl} [${proxyRes.statusCode}]`;
|
|
43
|
+
logger.info(exchange);
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* When client opens WebSocket connection
|
|
47
|
+
*/
|
|
48
|
+
proxyServer.on('open', (socket) => {
|
|
49
|
+
logger.info('[HPM] Client connected: %o', socket.address());
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* When client closes WebSocket connection
|
|
53
|
+
*/
|
|
54
|
+
proxyServer.on('close', (req, proxySocket, proxyHead) => {
|
|
55
|
+
logger.info('[HPM] Client disconnected: %o', proxySocket.address());
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Plugin } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Implements option.on object to subscribe to `httpxy` events.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```js
|
|
7
|
+
* createProxyMiddleware({
|
|
8
|
+
* on: {
|
|
9
|
+
* error: (error, req, res, target) => {},
|
|
10
|
+
* proxyReq: (proxyReq, req, res, options) => {},
|
|
11
|
+
* proxyReqWs: (proxyReq, req, socket, options) => {},
|
|
12
|
+
* proxyRes: (proxyRes, req, res) => {},
|
|
13
|
+
* open: (proxySocket) => {},
|
|
14
|
+
* close: (proxyRes, proxySocket, proxyHead) => {},
|
|
15
|
+
* start: (req, res, target) => {},
|
|
16
|
+
* end: (req, res, proxyRes) => {},
|
|
17
|
+
* econnreset: (error, req, res, target) => {},
|
|
18
|
+
* }
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare const proxyEventsPlugin: Plugin;
|