Skip to content

Commit c22b79b

Browse files
committed
feat(handler): response interceptor
1 parent dbdad8a commit c22b79b

File tree

12 files changed

+407
-2
lines changed

12 files changed

+407
-2
lines changed
772 KB
Loading

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## next
4+
5+
- feat(handler): response interceptor
6+
37
## [v1.1.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v1.1.1)
48

59
- fix(error handler): re-throw http-proxy missing target error ([#517](https://github.com/chimurai/http-proxy-middleware/pull/517))

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ _All_ `http-proxy` [options](https://github.com/nodejitsu/node-http-proxy#option
6969
- [app.use\(path, proxy\)](#appusepath-proxy)
7070
- [WebSocket](#websocket)
7171
- [External WebSocket upgrade](#external-websocket-upgrade)
72+
- [Intercept and manipulate responses](#intercept-and-manipulate-responses)
7273
- [Working examples](#working-examples)
7374
- [Recipes](#recipes)
7475
- [Compatible servers](#compatible-servers)
@@ -481,6 +482,39 @@ const server = app.listen(3000);
481482
server.on('upgrade', wsProxy.upgrade); // <-- subscribe to http 'upgrade'
482483
```
483484

485+
## Intercept and manipulate responses
486+
487+
Intercept responses from upstream with `responseInterceptor`. (Make sure to set `selfHandleResponse: true`)
488+
489+
Responses which are compressed with `brotli`, `gzip` and `deflate` will be decompressed automatically. The response will be returned as `buffer` ([docs](https://nodejs.org/api/buffer.html)) which you can manipulate.
490+
491+
With `buffer`, response manipulation is not limited to text responses (html/css/js, etc...); image manipulation will be possible too. ([example](https://github.com/chimurai/http-proxy-middleware/blob/response-interceptor/recipes/response-interceptor.md#manipulate-image-response))
492+
493+
NOTE: `responseInterceptor` disables streaming of target's response.
494+
495+
Example:
496+
497+
```javascript
498+
const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware');
499+
500+
const proxy = createProxyMiddleware({
501+
/**
502+
* IMPORTANT: avoid res.end being called automatically
503+
**/
504+
selfHandleResponse: true, // res.end() will be called internally by responseInterceptor()
505+
506+
/**
507+
* Intercept response and replace 'Hello' with 'Goodbye'
508+
**/
509+
onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => {
510+
const response = responseBuffer.toString('utf-8'); // convert buffer to string
511+
return response.replace('Hello', 'Goodbye'); // manipulate response and return the result
512+
}),
513+
});
514+
```
515+
516+
Check out [interception recipes](https://github.com/chimurai/http-proxy-middleware/blob/response-interceptor/recipes/response-interceptor.md#readme) for more examples.
517+
484518
## Working examples
485519

486520
View and play around with [working examples](https://github.com/chimurai/http-proxy-middleware/tree/master/examples).
@@ -489,6 +523,7 @@ View and play around with [working examples](https://github.com/chimurai/http-pr
489523
- express ([example source](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/express/index.js))
490524
- connect ([example source](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/connect/index.js))
491525
- WebSocket ([example source](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/websocket/index.js))
526+
- Response Manipulation ([example source](https://github.com/chimurai/http-proxy-middleware/blob/master/response-interceptor/examples/response-interceptor/index.js))
492527

493528
## Recipes
494529

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Module dependencies.
3+
*/
4+
const express = require('express');
5+
const { createProxyMiddleware, responseInterceptor } = require('../../dist'); // require('http-proxy-middleware');
6+
7+
// test with double-byte characters
8+
const favoriteFoods = [
9+
{
10+
country: 'NL',
11+
food: 'Kroket',
12+
},
13+
{
14+
country: 'HK',
15+
food: '叉燒包',
16+
},
17+
{
18+
country: 'US',
19+
food: 'Hamburger',
20+
},
21+
{
22+
country: 'TH',
23+
food: 'ส้มตำไทย',
24+
},
25+
{
26+
country: 'IN',
27+
food: 'बटर चिकन',
28+
},
29+
];
30+
31+
/**
32+
* Configure proxy middleware
33+
*/
34+
const jsonPlaceholderProxy = createProxyMiddleware({
35+
target: 'http://jsonplaceholder.typicode.com',
36+
router: {
37+
'/users': 'http://jsonplaceholder.typicode.com',
38+
'/brotli': 'http://httpbin.org',
39+
'/gzip': 'http://httpbin.org',
40+
'/deflate': 'http://httpbin.org',
41+
},
42+
changeOrigin: true, // for vhosted sites, changes host header to match to target's host
43+
selfHandleResponse: true, // manually call res.end(); IMPORTANT: res.end() is called internally by responseInterceptor()
44+
onProxyRes: responseInterceptor(async (buffer, proxyRes, req, res) => {
45+
// log original request and proxied request info
46+
const exchange = `[DEBUG] ${req.method} ${req.path} -> ${proxyRes.req.protocol}//${proxyRes.req.host}${proxyRes.req.path} [${proxyRes.statusCode}]`;
47+
console.log(exchange);
48+
49+
// log original response
50+
// console.log(`[DEBUG] original response:\n${buffer.toString('utf-8')}`);
51+
52+
// set response content-type
53+
res.setHeader('content-type', 'application/json; charset=utf-8');
54+
55+
// set response status code
56+
res.statusCode = 418;
57+
58+
// return a complete different response
59+
return JSON.stringify(favoriteFoods);
60+
}),
61+
logLevel: 'debug',
62+
});
63+
64+
const app = express();
65+
66+
/**
67+
* Add the proxy to express
68+
*/
69+
app.use(jsonPlaceholderProxy);
70+
71+
app.listen(3000);
72+
73+
console.log('[DEMO] Server: listening on port 3000');
74+
console.log('[DEMO] Open: http://localhost:3000/users');
75+
console.log('[DEMO] Open: http://localhost:3000/brotli');
76+
console.log('[DEMO] Open: http://localhost:3000/gzip');
77+
console.log('[DEMO] Open: http://localhost:3000/deflate');
78+
79+
require('open')('http://localhost:3000/users');

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "http-proxy-middleware",
3-
"version": "1.1.1",
3+
"version": "1.2.0-beta.2",
44
"description": "The one-liner node.js proxy middleware for connect, express and browser-sync",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

recipes/response-interceptor.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Response Interceptor
2+
3+
Intercept responses from upstream with `responseInterceptor`. (Make sure to set `selfHandleResponse: true`)
4+
5+
Responses which are compressed with `brotli`, `gzip` and `deflate` will be decompressed automatically. Response will be made available as [`buffer`](https://nodejs.org/api/buffer.html) which you can manipulate.
6+
7+
## Replace text and change http status code
8+
9+
```js
10+
const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware');
11+
12+
const proxy = createProxyMiddleware({
13+
target: 'http://www.example.com',
14+
changeOrigin: true, // for vhosted sites
15+
16+
/**
17+
* IMPORTANT: avoid res.end being called automatically
18+
**/
19+
selfHandleResponse: true, // res.end() will be called internally by responseInterceptor()
20+
21+
/**
22+
* Intercept response and replace 'Hello' with 'Teapot' with 418 http response status code
23+
**/
24+
onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => {
25+
res.statusCode = 418; // set different response status code
26+
27+
const response = responseBuffer.toString('utf-8');
28+
return response.replace('Hello', 'Teapot');
29+
}),
30+
});
31+
```
32+
33+
## Log request and response
34+
35+
```javascript
36+
const proxy = createProxyMiddleware({
37+
target: 'http://www.example.com',
38+
changeOrigin: true, // for vhosted sites
39+
40+
selfHandleResponse: true, // res.end() will be called internally by responseInterceptor()
41+
42+
onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => {
43+
// log original request and proxied request info
44+
const exchange = `[DEBUG] ${req.method} ${req.path} -> ${proxyRes.req.protocol}//${proxyRes.req.host}${proxyRes.req.path} [${proxyRes.statusCode}]`;
45+
console.log(exchange); // [DEBUG] GET / -> http://www.example.com [200]
46+
47+
// log complete response
48+
const response = responseBuffer.toString('utf-8');
49+
console.log(response); // log response body
50+
51+
return responseBuffer;
52+
}),
53+
});
54+
```
55+
56+
## Manipulate JSON responses (application/json)
57+
58+
```javascript
59+
const proxy = createProxyMiddleware({
60+
target: 'http://jsonplaceholder.typicode.com',
61+
changeOrigin: true, // for vhosted sites
62+
63+
selfHandleResponse: true, // res.end() will be called internally by responseInterceptor()
64+
65+
onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => {
66+
// detect json responses
67+
if (proxyRes.headers['content-type'] === 'application/json') {
68+
let data = JSON.parse(responseBuffer.toString('utf-8'));
69+
70+
// manipulate JSON data here
71+
data = Object.assign({}, data, { extra: 'foo bar' });
72+
73+
// return manipulated JSON
74+
return JSON.stringify(data);
75+
}
76+
77+
// return other content-types as-is
78+
return responseBuffer;
79+
}),
80+
});
81+
```
82+
83+
## Manipulate image response
84+
85+
Example [Lenna](https://en.wikipedia.org/wiki/Lenna) image: <https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png>
86+
87+
Proxy and manipulate image (flip, sepia, pixelate).
88+
89+
[![Image of Lenna](../.github/docs/response-interceptor-lenna.png)](https://codesandbox.io/s/trusting-engelbart-03rjl)
90+
91+
Check [source code](https://codesandbox.io/s/trusting-engelbart-03rjl) on codesandbox.
92+
93+
Some working examples on <https://03rjl.sse.codesandbox.io>:
94+
95+
- Lenna - ([manipulated](https://03rjl.sse.codesandbox.io/wikipedia/en/7/7d/Lenna_%28test_image%29.png)) ([original](https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png)).
96+
- Starry Night - ([manipulated](https://03rjl.sse.codesandbox.io/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/1024px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg)) ([original](https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/1024px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg)).
97+
- Mona Lisa - ([manipulated](https://03rjl.sse.codesandbox.io/wikipedia/commons/thumb/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg/800px-Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg)) ([original](https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg/800px-Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg)).
98+
99+
_You can just use any relative image path from <https://upload.wikimedia.org> and use the relative image path on <https://03rjl.sse.codesandbox.io> to see the manipulated image._
100+
101+
```javascript
102+
const Jimp = require('jimp'); // use jimp libray for image manipulation
103+
104+
const proxy = createProxyMiddleware({
105+
target: 'https://upload.wikimedia.org',
106+
changeOrigin: true, // for vhosted sites
107+
108+
selfHandleResponse: true, // res.end() will be called internally by responseInterceptor()
109+
110+
onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => {
111+
const imageTypes = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif'];
112+
113+
// detect image responses
114+
if (imageTypes.includes(proxyRes.headers['content-type'])) {
115+
try {
116+
const image = await Jimp.read(responseBuffer);
117+
image.flip(true, false).sepia().pixelate(5);
118+
return image.getBufferAsync(Jimp.AUTO);
119+
} catch (err) {
120+
console.log('image processing error: ', err);
121+
return responseBuffer;
122+
}
123+
}
124+
125+
return responseBuffer; // return other content-types as-is
126+
}),
127+
});
128+
129+
// http://localhost:3000/wikipedia/en/7/7d/Lenna\_%28test_image%29.png
130+
```

src/handlers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './public';

src/handlers/public.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { responseInterceptor } from './response-interceptor';

src/handlers/response-interceptor.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type * as http from 'http';
2+
import * as zlib from 'zlib';
3+
4+
type Interceptor = (
5+
buffer: Buffer,
6+
proxyRes: http.IncomingMessage,
7+
req: http.IncomingMessage,
8+
res: http.ServerResponse
9+
) => Promise<Buffer | string>;
10+
11+
/**
12+
* Intercept responses from upstream.
13+
* Automatically decompress (deflate, gzip, brotli).
14+
* Give developer the opportunity to modify intercepted Buffer and http.ServerResponse
15+
*
16+
* NOTE: must set options.selfHandleResponse=true (prevent automatic call of res.end())
17+
*/
18+
export function responseInterceptor(interceptor: Interceptor) {
19+
return async function proxyRes(
20+
proxyRes: http.IncomingMessage,
21+
req: http.IncomingMessage,
22+
res: http.ServerResponse
23+
): Promise<void> {
24+
const originalProxyRes = proxyRes;
25+
let buffer = Buffer.from('', 'utf8');
26+
27+
// decompress proxy response
28+
const _proxyRes = decompress(proxyRes, proxyRes.headers['content-encoding']);
29+
30+
// concat data stream
31+
_proxyRes.on('data', (chunk) => (buffer = Buffer.concat([buffer, chunk])));
32+
33+
_proxyRes.on('end', async () => {
34+
// set original content type from upstream
35+
res.setHeader('content-type', originalProxyRes.headers['content-type'] || '');
36+
37+
// call interceptor with intercepted response (buffer)
38+
const interceptedBuffer = Buffer.from(await interceptor(buffer, originalProxyRes, req, res));
39+
40+
// set correct content-length (with double byte character support)
41+
res.setHeader('content-length', Buffer.byteLength(interceptedBuffer, 'utf-8'));
42+
43+
res.write(interceptedBuffer);
44+
res.end();
45+
});
46+
47+
_proxyRes.on('error', (error) => {
48+
res.end(`Error fetching proxied request: ${error.message}`);
49+
});
50+
};
51+
}
52+
53+
/**
54+
* Streaming decompression of proxy response
55+
* source: https://github.com/apache/superset/blob/9773aba522e957ed9423045ca153219638a85d2f/superset-frontend/webpack.proxy-config.js#L116
56+
*/
57+
function decompress(proxyRes: http.IncomingMessage, contentEncoding: string) {
58+
let _proxyRes = proxyRes;
59+
let decompress;
60+
61+
switch (contentEncoding) {
62+
case 'gzip':
63+
decompress = zlib.createGunzip();
64+
break;
65+
case 'br':
66+
decompress = zlib.createBrotliDecompress();
67+
break;
68+
case 'deflate':
69+
decompress = zlib.createInflate();
70+
break;
71+
default:
72+
break;
73+
}
74+
75+
if (decompress) {
76+
_proxyRes.pipe(decompress);
77+
_proxyRes = decompress;
78+
}
79+
80+
return _proxyRes;
81+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ export function createProxyMiddleware(context: Filter | Options, options?: Optio
66
return middleware;
77
}
88

9+
export * from './handlers';
10+
911
export { Filter, Options, RequestHandler } from './types';

test/e2e/_utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as express from 'express';
22
import { Express, RequestHandler } from 'express';
33

4-
export { createProxyMiddleware } from '../../dist/index';
4+
export { createProxyMiddleware, responseInterceptor } from '../../dist/index';
55

66
export function createApp(middleware: RequestHandler): Express {
77
const app = express();

0 commit comments

Comments
 (0)