Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 | undefined>);

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