Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion recipes/pathRewrite.md
Original file line number Diff line number Diff line change
Expand Up @@ -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');
};

Expand All @@ -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.
```
12 changes: 7 additions & 5 deletions src/http-proxy-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class HttpProxyMiddleware<
private activeServers = new Set<http.Server | https.Server>();
private proxyOptions: Options<TReq, TRes>;
private proxy: ProxyServer<TReq, TRes>;
private pathRewriter: ReturnType<typeof createPathRewriter<TReq>>;
private pathRewriter: ReturnType<typeof createPathRewriter<TReq, TRes>>;
private logger: Logger;

constructor(options: Options<TReq, TRes>) {
Expand All @@ -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<TReq, TRes>(this.proxyOptions.pathRewrite); // returns undefined when "pathRewrite" is not provided

// https://github.com/chimurai/http-proxy-middleware/issues/19
// expose function to upgrade externally
Expand Down Expand Up @@ -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;
};
Expand All @@ -208,10 +208,12 @@ export class HttpProxyMiddleware<
// rewrite path
private applyPathRewrite = async (
req: TReq,
pathRewriter: ReturnType<typeof createPathRewriter<TReq>>,
res: TRes | undefined,
pathRewriter: ReturnType<typeof createPathRewriter<TReq, TRes>>,
options: Options<TReq, TRes>,
) => {
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);
Expand Down
16 changes: 9 additions & 7 deletions src/path-rewriter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IncomingMessage } from 'node:http';
import type { IncomingMessage, ServerResponse } from 'node:http';

import isPlainObject from 'is-plain-obj';

Expand All @@ -13,9 +13,10 @@ type RewriteRule = { regex: RegExp; value: string };
/**
* Create rewrite function, to cache parsed rewrite rules.
*/
export function createPathRewriter<TReq extends IncomingMessage = IncomingMessage>(
rewriteConfig: PathRewriteConfig<TReq> | undefined,
) {
export function createPathRewriter<
TReq extends IncomingMessage = IncomingMessage,
TRes extends ServerResponse = ServerResponse,
>(rewriteConfig: PathRewriteConfig<TReq, TRes> | undefined) {
let rulesCache: RewriteRule[];

if (!isValidRewriteConfig(rewriteConfig)) {
Expand Down Expand Up @@ -45,9 +46,10 @@ export function createPathRewriter<TReq extends IncomingMessage = IncomingMessag
}
}

function isValidRewriteConfig<TReq extends IncomingMessage = IncomingMessage>(
rewriteConfig: PathRewriteConfig<TReq> | undefined,
): boolean {
function isValidRewriteConfig<
TReq extends IncomingMessage = IncomingMessage,
TRes extends ServerResponse = ServerResponse,
>(rewriteConfig: PathRewriteConfig<TReq, TRes> | undefined): boolean {
if (typeof rewriteConfig === 'function') {
return true;
} else if (isPlainObject(rewriteConfig)) {
Expand Down
27 changes: 23 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,25 @@ export interface OnProxyEvent<

export type Logger = Pick<Console, 'info' | 'warn' | 'error'>;

export type PathRewriteConfig<TReq extends http.IncomingMessage = http.IncomingMessage> =
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<string>);
| ((
path: string,
req: TReq,
/** `res` is undefined in WebSocket upgrade flows. */
res?: TRes | undefined,
options?: Options<TReq, TRes>,
) => string | undefined)
| ((
path: string,
req: TReq,
/** `res` is undefined in WebSocket upgrade flows. */
res?: TRes | undefined,
options?: Options<TReq, TRes>,
) => Promise<string>);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

export interface Options<
TReq extends http.IncomingMessage = http.IncomingMessage,
Expand All @@ -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<TReq>;
pathRewrite?: PathRewriteConfig<TReq, TRes>;

/**
* Access the internal `httpxy` server instance to customize behavior
Expand Down
27 changes: 27 additions & 0 deletions test/e2e/path-rewriter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
7 changes: 5 additions & 2 deletions test/types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
21 changes: 19 additions & 2 deletions test/unit/path-rewriter.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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', () => {
Expand Down
Loading