Skip to content

Commit

Permalink
Merge branch 'master' into docs-response-interceptor-utf8
Browse files Browse the repository at this point in the history
  • Loading branch information
chimurai authored Apr 24, 2021
2 parents 69ca15e + 2831df9 commit 31aa5de
Show file tree
Hide file tree
Showing 14 changed files with 161 additions and 13 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [v1.2.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.2.1)

- fix(response interceptor): proxy original response headers ([#563](https://github.com/chimurai/http-proxy-middleware/pull/563))

## [v1.2.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.2.0)

- feat(handler): response interceptor ([#520](https://github.com/chimurai/http-proxy-middleware/pull/520))
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ _All_ `http-proxy` [options](https://github.com/nodejitsu/node-http-proxy#option
- [app.use\(path, proxy\)](#appusepath-proxy)
- [WebSocket](#websocket)
- [External WebSocket upgrade](#external-websocket-upgrade)
- [Intercept and manipulate requests](#intercept-and-manipulate-requests)
- [Intercept and manipulate responses](#intercept-and-manipulate-responses)
- [Working examples](#working-examples)
- [Recipes](#recipes)
Expand Down Expand Up @@ -482,6 +483,25 @@ const server = app.listen(3000);
server.on('upgrade', wsProxy.upgrade); // <-- subscribe to http 'upgrade'
```

## Intercept and manipulate requests

Intercept requests from downstream by defining `onProxyReq` in `createProxyMiddleware`.

Currently the only pre-provided request interceptor is `fixRequestBody`, which is used to fix proxied POST requests when `bodyParser` is applied before this middleware.

Example:

```javascript
const { createProxyMiddleware, fixRequestBody } = require('http-proxy-middleware');

const proxy = createProxyMiddleware({
/**
* Fix bodyParser
**/
onProxyReq: fixRequestBody,
});
```

## Intercept and manipulate responses

Intercept responses from upstream with `responseInterceptor`. (Make sure to set `selfHandleResponse: true`)
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "http-proxy-middleware",
"version": "1.2.0",
"version": "1.2.1",
"description": "The one-liner node.js proxy middleware for connect, express and browser-sync",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -60,6 +60,7 @@
"@types/ws": "^7.4.0",
"@typescript-eslint/eslint-plugin": "^4.19.0",
"@typescript-eslint/parser": "^4.19.0",
"body-parser": "^1.19.0",
"browser-sync": "^2.26.14",
"connect": "^3.7.0",
"eslint": "^7.23.0",
Expand Down
27 changes: 27 additions & 0 deletions src/handlers/fix-request-body.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ClientRequest } from 'http';
import type { Request } from '../types';
import * as querystring from 'querystring';

/**
* Fix proxied body if bodyParser is involved.
*/
export function fixRequestBody(proxyReq: ClientRequest, req: Request): void {
if (!req.body || !Object.keys(req.body).length) {
return;
}

const contentType = proxyReq.getHeader('Content-Type') as string;
const writeBody = (bodyData: string) => {
// deepcode ignore ContentLengthInCode: bodyParser fix
proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
proxyReq.write(bodyData);
};

if (contentType.includes('application/json')) {
writeBody(JSON.stringify(req.body));
}

if (contentType === 'application/x-www-form-urlencoded') {
writeBody(querystring.stringify(req.body));
}
}
1 change: 1 addition & 0 deletions src/handlers/public.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { responseInterceptor } from './response-interceptor';
export { fixRequestBody } from './fix-request-body';
34 changes: 32 additions & 2 deletions src/handlers/response-interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export function responseInterceptor(interceptor: Interceptor) {
_proxyRes.on('data', (chunk) => (buffer = Buffer.concat([buffer, chunk])));

_proxyRes.on('end', async () => {
// set original content type from upstream
res.setHeader('content-type', originalProxyRes.headers['content-type'] || '');
// copy original headers
copyHeaders(proxyRes, res);

// call interceptor with intercepted response (buffer)
const interceptedBuffer = Buffer.from(await interceptor(buffer, originalProxyRes, req, res));
Expand Down Expand Up @@ -79,3 +79,33 @@ function decompress(proxyRes: http.IncomingMessage, contentEncoding: string) {

return _proxyRes;
}

/**
* Copy original headers
* https://github.com/apache/superset/blob/9773aba522e957ed9423045ca153219638a85d2f/superset-frontend/webpack.proxy-config.js#L78
*/
function copyHeaders(originalResponse, response) {
response.statusCode = originalResponse.statusCode;
response.statusMessage = originalResponse.statusMessage;

if (response.setHeader) {
let keys = Object.keys(originalResponse.headers);

// ignore chunked, brotli, gzip, deflate headers
keys = keys.filter((key) => !['content-encoding', 'transfer-encoding'].includes(key));

keys.forEach((key) => {
let value = originalResponse.headers[key];

if (key === 'set-cookie') {
// remove cookie domain
value = Array.isArray(value) ? value : [value];
value = value.map((x) => x.replace(/Domain=[^;]+?/i, ''));
}

response.setHeader(key, value);
});
} else {
response.headers = originalResponse.headers;
}
}
2 changes: 1 addition & 1 deletion test/e2e/express-router.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as express from 'express';
import * as http from 'http';
import { createProxyMiddleware } from './_utils';
import { createProxyMiddleware } from './test-kit';
import { Options } from '../../src/index';

describe('Usage in Express', () => {
Expand Down
41 changes: 40 additions & 1 deletion test/e2e/http-proxy-middleware.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { createProxyMiddleware, createApp, createAppWithPath } from './_utils';
import { createProxyMiddleware, createApp, createAppWithPath, fixRequestBody } from './test-kit';
import * as request from 'supertest';
import { Mockttp, getLocal, CompletedRequest } from 'mockttp';
import { Request, Response } from '../../src/types';
import { NextFunction } from 'express';
import * as bodyParser from 'body-parser';

describe('E2E http-proxy-middleware', () => {
describe('http-proxy-middleware creation', () => {
Expand Down Expand Up @@ -78,6 +79,44 @@ describe('E2E http-proxy-middleware', () => {
});
});

describe('basic setup with configured body-parser', () => {
it('should proxy request body from form', async () => {
agent = request(
createApp(
bodyParser.urlencoded({ extended: false }),
createProxyMiddleware('/api', {
target: `http://localhost:${mockTargetServer.port}`,
onProxyReq: fixRequestBody,
})
)
);

await mockTargetServer.post('/api').thenCallback((req) => {
expect(req.body.text).toBe('foo=bar&bar=baz');
return { status: 200 };
});
await agent.post('/api').send('foo=bar').send('bar=baz').expect(200);
});

it('should proxy request body from json', async () => {
agent = request(
createApp(
bodyParser.json(),
createProxyMiddleware('/api', {
target: `http://localhost:${mockTargetServer.port}`,
onProxyReq: fixRequestBody,
})
)
);

await mockTargetServer.post('/api').thenCallback((req) => {
expect(req.body.json).toEqual({ foo: 'bar', bar: 'baz', doubleByte: '文' });
return { status: 200 };
});
await agent.post('/api').send({ foo: 'bar', bar: 'baz', doubleByte: '文' }).expect(200);
});
});

describe('custom context matcher/filter', () => {
it('should have response body: "HELLO WEB"', async () => {
const filter = (path, req) => {
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/path-rewriter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createProxyMiddleware, createApp } from './_utils';
import { createProxyMiddleware, createApp } from './test-kit';
import * as request from 'supertest';
import { getLocal, Mockttp } from 'mockttp';

Expand Down
28 changes: 27 additions & 1 deletion test/e2e/response-interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createProxyMiddleware, responseInterceptor } from '../../src';
import { createApp } from './_utils';
import { createApp } from './test-kit';
import * as request from 'supertest';

describe('responseInterceptor()', () => {
Expand Down Expand Up @@ -41,6 +41,32 @@ describe('responseInterceptor()', () => {
});
});

describe('intercept responses with original headers', () => {
beforeEach(() => {
agent = request(
createApp(
createProxyMiddleware({
target: `http://httpbin.org`,
changeOrigin: true, // for vhosted sites, changes host header to match to target's host
selfHandleResponse: true,
onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => {
return responseBuffer;
}),
})
)
);
});

it('should proxy and return original headers from http://httpbin.org/cookies/set/cookie/monster', async () => {
return agent
.get(`/cookies/set/cookie/monster`)
.expect('Access-Control-Allow-Origin', '*')
.expect('Date', /.+/)
.expect('set-cookie', /.*cookie=monster.*/)
.expect(302);
});
});

describe('intercept compressed responses', () => {
beforeEach(() => {
agent = request(
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/router.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createProxyMiddleware, createApp, createAppWithPath } from './_utils';
import { createProxyMiddleware, createApp, createAppWithPath } from './test-kit';
import { ErrorRequestHandler } from 'express';
import * as request from 'supertest';
import { getLocal, generateCACertificate, Mockttp } from 'mockttp';
Expand Down
6 changes: 3 additions & 3 deletions test/e2e/_utils.ts → test/e2e/test-kit.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import * as express from 'express';
import { Express, RequestHandler } from 'express';

export { createProxyMiddleware, responseInterceptor } from '../../dist/index';
export { createProxyMiddleware, responseInterceptor, fixRequestBody } from '../../dist/index';

export function createApp(middleware: RequestHandler): Express {
export function createApp(...middlewares: RequestHandler[]): Express {
const app = express();
app.use(middleware);
app.use(...middlewares);
return app;
}

Expand Down
2 changes: 1 addition & 1 deletion test/e2e/websocket.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as express from 'express';
import * as WebSocket from 'ws';
// tslint:disable-next-line: no-duplicate-imports
import { Server as WebSocketServer } from 'ws';
import { createProxyMiddleware } from './_utils';
import { createProxyMiddleware } from './test-kit';

describe('E2E WebSocket proxy', () => {
let proxyServer: http.Server;
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1483,7 +1483,7 @@ blob@0.0.5:
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==

body-parser@1.19.0, body-parser@^1.15.2:
body-parser@1.19.0, body-parser@^1.15.2, body-parser@^1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
Expand Down

0 comments on commit 31aa5de

Please sign in to comment.