diff --git a/CHANGELOG.md b/CHANGELOG.md index ce3d300c..92d943e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - fix(response-interceptor): reduce responseInterceptor buffer churn - fix(ws): handle multi-server upgrade subscription and safe proxy shutdown - feat(router): add 'res' and 'options' to router function +- feat(pathRewrite): add 'res' and 'options' to custom pathRewrite function ## [v4.0.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v4.0.0) diff --git a/README.md b/README.md index d65cddb7..d3071f7d 100644 --- a/README.md +++ b/README.md @@ -212,14 +212,16 @@ pathRewrite: {'^/remove/api' : ''} pathRewrite: {'^/' : '/basepath/'} // custom rewriting -pathRewrite: function (path, req) { return path.replace('/api', '/base/api') } +pathRewrite: function (path, req, res, options) { return path.replace('/api', '/base/api') } // custom rewriting, returning Promise -pathRewrite: async function (path, req) { +pathRewrite: async function (path, req, res, options) { const should_add_something = await httpRequestToDecideSomething(path); if (should_add_something) path += "something"; return path; } + +// `res` is undefined in WebSocket upgrade flows. ``` ### `router` (object/function) diff --git a/recipes/pathRewrite.md b/recipes/pathRewrite.md index 920a243e..07da0fe4 100644 --- a/recipes/pathRewrite.md +++ b/recipes/pathRewrite.md @@ -73,7 +73,7 @@ The unmodified path will be used, when rewrite function returns `undefined` ```javascript import { createProxyMiddleware } from 'http-proxy-middleware'; -const rewriteFn = function (path, req) { +const rewriteFn = function (path, req, res, options) { return path.replace('/api/foo', '/api/bar'); }; @@ -85,4 +85,6 @@ const options = { const apiProxy = createProxyMiddleware(options); // `/api/foo/lorum/ipsum` -> `http://localhost:3000/api/bar/lorum/ipsum` + +// NOTE: `res` is undefined in WebSocket upgrade flows. ``` diff --git a/src/http-proxy-middleware.ts b/src/http-proxy-middleware.ts index e785eb77..c0e55d95 100644 --- a/src/http-proxy-middleware.ts +++ b/src/http-proxy-middleware.ts @@ -23,7 +23,7 @@ export class HttpProxyMiddleware< private activeServers = new Set(); private proxyOptions: Options; private proxy: ProxyServer; - private pathRewriter: ReturnType>; + private pathRewriter: ReturnType>; private logger: Logger; constructor(options: Options) { @@ -36,7 +36,7 @@ export class HttpProxyMiddleware< this.registerPlugins(this.proxy, this.proxyOptions); - this.pathRewriter = createPathRewriter(this.proxyOptions.pathRewrite); // returns undefined when "pathRewrite" is not provided + this.pathRewriter = createPathRewriter(this.proxyOptions.pathRewrite); // returns undefined when "pathRewrite" is not provided // https://github.com/chimurai/http-proxy-middleware/issues/19 // expose function to upgrade externally @@ -186,7 +186,7 @@ export class HttpProxyMiddleware< // 2. option.pathRewrite await this.applyRouter(req, res, newProxyOptions); normalizeIPv6LiteralTargets(newProxyOptions); - await this.applyPathRewrite(req, this.pathRewriter); + await this.applyPathRewrite(req, res, this.pathRewriter, newProxyOptions); return newProxyOptions; }; @@ -208,10 +208,12 @@ export class HttpProxyMiddleware< // rewrite path private applyPathRewrite = async ( req: TReq, - pathRewriter: ReturnType>, + res: TRes | undefined, + pathRewriter: ReturnType>, + options: Options, ) => { if (req.url && pathRewriter) { - const path = await pathRewriter(req.url, req); + const path = await pathRewriter(req.url, req, res, options); if (typeof path === 'string') { debug('pathRewrite new path: %s', path); diff --git a/src/path-rewriter.ts b/src/path-rewriter.ts index 3fa23a65..78e031e7 100644 --- a/src/path-rewriter.ts +++ b/src/path-rewriter.ts @@ -1,4 +1,4 @@ -import type { IncomingMessage } from 'node:http'; +import type { IncomingMessage, ServerResponse } from 'node:http'; import isPlainObject from 'is-plain-obj'; @@ -13,9 +13,10 @@ type RewriteRule = { regex: RegExp; value: string }; /** * Create rewrite function, to cache parsed rewrite rules. */ -export function createPathRewriter( - rewriteConfig: PathRewriteConfig | undefined, -) { +export function createPathRewriter< + TReq extends IncomingMessage = IncomingMessage, + TRes extends ServerResponse = ServerResponse, +>(rewriteConfig: PathRewriteConfig | undefined) { let rulesCache: RewriteRule[]; if (!isValidRewriteConfig(rewriteConfig)) { @@ -45,9 +46,10 @@ export function createPathRewriter( - rewriteConfig: PathRewriteConfig | undefined, -): boolean { +function isValidRewriteConfig< + TReq extends IncomingMessage = IncomingMessage, + TRes extends ServerResponse = ServerResponse, +>(rewriteConfig: PathRewriteConfig | undefined): boolean { if (typeof rewriteConfig === 'function') { return true; } else if (isPlainObject(rewriteConfig)) { diff --git a/src/types.ts b/src/types.ts index 54ef769d..1df91709 100644 --- a/src/types.ts +++ b/src/types.ts @@ -61,10 +61,25 @@ export interface OnProxyEvent< export type Logger = Pick; -export type PathRewriteConfig = +export type PathRewriteConfig< + TReq extends http.IncomingMessage = http.IncomingMessage, + TRes extends http.ServerResponse = http.ServerResponse, +> = | { [regexp: string]: string } - | ((path: string, req: TReq) => string | undefined) - | ((path: string, req: TReq) => Promise); + | (( + path: string, + req: TReq, + /** `res` is undefined in WebSocket upgrade flows. */ + res?: TRes | undefined, + options?: Options, + ) => string | undefined) + | (( + path: string, + req: TReq, + /** `res` is undefined in WebSocket upgrade flows. */ + res?: TRes | undefined, + options?: Options, + ) => Promise); export interface Options< TReq extends http.IncomingMessage = http.IncomingMessage, @@ -88,9 +103,13 @@ export interface Options< * } * }); * ``` + * @since v0.15.0 + * @since v0.21.0 - support `async` function + * @since v4.1.0 - `res` and `options` parameters added to custom function + * * @link https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/pathRewrite.md */ - pathRewrite?: PathRewriteConfig; + pathRewrite?: PathRewriteConfig; /** * Access the internal `httpxy` server instance to customize behavior diff --git a/test/e2e/path-rewriter.spec.ts b/test/e2e/path-rewriter.spec.ts index 48727ee3..a2467743 100644 --- a/test/e2e/path-rewriter.spec.ts +++ b/test/e2e/path-rewriter.spec.ts @@ -60,6 +60,33 @@ describe('E2E pathRewrite', () => { const response = await agent.get('/foobar/api/lorum/ipsum').expect(200); expect(response.text).toBe('/API RESPONSE AFTER PATH REWRITE FUNCTION'); }); + + it('should expose res and options to rewrite function', async () => { + mockTargetServer + .forGet('/api/lorum/ipsum') + .thenReply(200, '/API RESPONSE AFTER PATH REWRITE FUNCTION'); + + let capturedRes: unknown; + let capturedOptions: { target?: unknown } | undefined; + + const agent = request( + createApp( + createProxyMiddleware({ + target: mockTargetServer.url, + pathRewrite(path, req, res, options) { + capturedRes = res; + capturedOptions = options; + return path; + }, + }), + ), + ); + + await agent.get('/api/lorum/ipsum').expect(200); + + expect(capturedRes).toBeDefined(); + expect(capturedOptions?.target).toBe(mockTargetServer.url); + }); }); describe('Rewrite paths with function which return undefined', () => { diff --git a/test/types.spec.ts b/test/types.spec.ts index b275b335..0d2f85e6 100644 --- a/test/types.spec.ts +++ b/test/types.spec.ts @@ -212,13 +212,16 @@ describe('http-proxy-middleware TypeScript Types', () => { middleware({ router: (req) => req.params, pathFilter: (pathname, req) => !!req.params, - pathRewrite: (path, req) => { + pathRewrite: (path, req, res, options) => { req.params; // @ts-expect-error: should error when request is typed as `any` req.unknownProperty; - return path; + // @ts-expect-error: should error when response is typed as `any` + res?.unknownProperty; + + return path + (res?.locals ?? ''); }, on: { error(error, req, res, target) { diff --git a/test/unit/path-rewriter.spec.ts b/test/unit/path-rewriter.spec.ts index 85c34525..bc5ff68e 100644 --- a/test/unit/path-rewriter.spec.ts +++ b/test/unit/path-rewriter.spec.ts @@ -1,8 +1,9 @@ -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createPathRewriter } from '../../src/path-rewriter.js'; +import type { Options } from '../../src/types.js'; import type { PathRewriteConfig } from '../../src/types.js'; -import { createMockRequest } from '../test-utils.js'; +import { createMockRequest, createMockResponse } from '../test-utils.js'; describe('Path rewriting', () => { const mockReq = createMockRequest(); @@ -115,6 +116,22 @@ describe('Path rewriting', () => { return expect(rewriter(originalRequestPath, mockReq)).resolves.toBe('/123/789'); }); + + it('should pass req, res and options to custom rewrite function', () => { + const mockRes = createMockResponse(mockReq); + const mockOptions = { target: 'http://example.org' } as Options; + const customRewriteFn = vi.fn((path) => path); + + rewriter = createPathRewriter(customRewriteFn)!; + rewriter(originalRequestPath, mockReq, mockRes, mockOptions); + + expect(customRewriteFn).toHaveBeenCalledWith( + originalRequestPath, + mockReq, + mockRes, + mockOptions, + ); + }); }); describe('Invalid configuration', () => {