-
Notifications
You must be signed in to change notification settings - Fork 835
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
407 additions
and
2 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/** | ||
* Module dependencies. | ||
*/ | ||
const express = require('express'); | ||
const { createProxyMiddleware, responseInterceptor } = require('../../dist'); // require('http-proxy-middleware'); | ||
|
||
// test with double-byte characters | ||
const favoriteFoods = [ | ||
{ | ||
country: 'NL', | ||
food: 'Kroket', | ||
}, | ||
{ | ||
country: 'HK', | ||
food: '叉燒包', | ||
}, | ||
{ | ||
country: 'US', | ||
food: 'Hamburger', | ||
}, | ||
{ | ||
country: 'TH', | ||
food: 'ส้มตำไทย', | ||
}, | ||
{ | ||
country: 'IN', | ||
food: 'बटर चिकन', | ||
}, | ||
]; | ||
|
||
/** | ||
* Configure proxy middleware | ||
*/ | ||
const jsonPlaceholderProxy = createProxyMiddleware({ | ||
target: 'http://jsonplaceholder.typicode.com', | ||
router: { | ||
'/users': 'http://jsonplaceholder.typicode.com', | ||
'/brotli': 'http://httpbin.org', | ||
'/gzip': 'http://httpbin.org', | ||
'/deflate': 'http://httpbin.org', | ||
}, | ||
changeOrigin: true, // for vhosted sites, changes host header to match to target's host | ||
selfHandleResponse: true, // manually call res.end(); IMPORTANT: res.end() is called internally by responseInterceptor() | ||
onProxyRes: responseInterceptor(async (buffer, proxyRes, req, res) => { | ||
// log original request and proxied request info | ||
const exchange = `[DEBUG] ${req.method} ${req.path} -> ${proxyRes.req.protocol}//${proxyRes.req.host}${proxyRes.req.path} [${proxyRes.statusCode}]`; | ||
console.log(exchange); | ||
|
||
// log original response | ||
// console.log(`[DEBUG] original response:\n${buffer.toString('utf-8')}`); | ||
|
||
// set response content-type | ||
res.setHeader('content-type', 'application/json; charset=utf-8'); | ||
|
||
// set response status code | ||
res.statusCode = 418; | ||
|
||
// return a complete different response | ||
return JSON.stringify(favoriteFoods); | ||
}), | ||
logLevel: 'debug', | ||
}); | ||
|
||
const app = express(); | ||
|
||
/** | ||
* Add the proxy to express | ||
*/ | ||
app.use(jsonPlaceholderProxy); | ||
|
||
app.listen(3000); | ||
|
||
console.log('[DEMO] Server: listening on port 3000'); | ||
console.log('[DEMO] Open: http://localhost:3000/users'); | ||
console.log('[DEMO] Open: http://localhost:3000/brotli'); | ||
console.log('[DEMO] Open: http://localhost:3000/gzip'); | ||
console.log('[DEMO] Open: http://localhost:3000/deflate'); | ||
|
||
require('open')('http://localhost:3000/users'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
# Response Interceptor | ||
|
||
Intercept responses from upstream with `responseInterceptor`. (Make sure to set `selfHandleResponse: true`) | ||
|
||
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. | ||
|
||
## Replace text and change http status code | ||
|
||
```js | ||
const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware'); | ||
|
||
const proxy = createProxyMiddleware({ | ||
target: 'http://www.example.com', | ||
changeOrigin: true, // for vhosted sites | ||
|
||
/** | ||
* IMPORTANT: avoid res.end being called automatically | ||
**/ | ||
selfHandleResponse: true, // res.end() will be called internally by responseInterceptor() | ||
|
||
/** | ||
* Intercept response and replace 'Hello' with 'Teapot' with 418 http response status code | ||
**/ | ||
onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { | ||
res.statusCode = 418; // set different response status code | ||
|
||
const response = responseBuffer.toString('utf-8'); | ||
return response.replace('Hello', 'Teapot'); | ||
}), | ||
}); | ||
``` | ||
|
||
## Log request and response | ||
|
||
```javascript | ||
const proxy = createProxyMiddleware({ | ||
target: 'http://www.example.com', | ||
changeOrigin: true, // for vhosted sites | ||
|
||
selfHandleResponse: true, // res.end() will be called internally by responseInterceptor() | ||
|
||
onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { | ||
// log original request and proxied request info | ||
const exchange = `[DEBUG] ${req.method} ${req.path} -> ${proxyRes.req.protocol}//${proxyRes.req.host}${proxyRes.req.path} [${proxyRes.statusCode}]`; | ||
console.log(exchange); // [DEBUG] GET / -> http://www.example.com [200] | ||
|
||
// log complete response | ||
const response = responseBuffer.toString('utf-8'); | ||
console.log(response); // log response body | ||
|
||
return responseBuffer; | ||
}), | ||
}); | ||
``` | ||
|
||
## Manipulate JSON responses (application/json) | ||
|
||
```javascript | ||
const proxy = createProxyMiddleware({ | ||
target: 'http://jsonplaceholder.typicode.com', | ||
changeOrigin: true, // for vhosted sites | ||
|
||
selfHandleResponse: true, // res.end() will be called internally by responseInterceptor() | ||
|
||
onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { | ||
// detect json responses | ||
if (proxyRes.headers['content-type'] === 'application/json') { | ||
let data = JSON.parse(responseBuffer.toString('utf-8')); | ||
|
||
// manipulate JSON data here | ||
data = Object.assign({}, data, { extra: 'foo bar' }); | ||
|
||
// return manipulated JSON | ||
return JSON.stringify(data); | ||
} | ||
|
||
// return other content-types as-is | ||
return responseBuffer; | ||
}), | ||
}); | ||
``` | ||
|
||
## Manipulate image response | ||
|
||
Example [Lenna](https://en.wikipedia.org/wiki/Lenna) image: <https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png> | ||
|
||
Proxy and manipulate image (flip, sepia, pixelate). | ||
|
||
[![Image of Lenna](../.github/docs/response-interceptor-lenna.png)](https://codesandbox.io/s/trusting-engelbart-03rjl) | ||
|
||
Check [source code](https://codesandbox.io/s/trusting-engelbart-03rjl) on codesandbox. | ||
|
||
Some working examples on <https://03rjl.sse.codesandbox.io>: | ||
|
||
- 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)). | ||
- 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)). | ||
- 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)). | ||
|
||
_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._ | ||
|
||
```javascript | ||
const Jimp = require('jimp'); // use jimp libray for image manipulation | ||
|
||
const proxy = createProxyMiddleware({ | ||
target: 'https://upload.wikimedia.org', | ||
changeOrigin: true, // for vhosted sites | ||
|
||
selfHandleResponse: true, // res.end() will be called internally by responseInterceptor() | ||
|
||
onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => { | ||
const imageTypes = ['image/png', 'image/jpg', 'image/jpeg', 'image/gif']; | ||
|
||
// detect image responses | ||
if (imageTypes.includes(proxyRes.headers['content-type'])) { | ||
try { | ||
const image = await Jimp.read(responseBuffer); | ||
image.flip(true, false).sepia().pixelate(5); | ||
return image.getBufferAsync(Jimp.AUTO); | ||
} catch (err) { | ||
console.log('image processing error: ', err); | ||
return responseBuffer; | ||
} | ||
} | ||
|
||
return responseBuffer; // return other content-types as-is | ||
}), | ||
}); | ||
|
||
// http://localhost:3000/wikipedia/en/7/7d/Lenna\_%28test_image%29.png | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './public'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { responseInterceptor } from './response-interceptor'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import type * as http from 'http'; | ||
import * as zlib from 'zlib'; | ||
|
||
type Interceptor = ( | ||
buffer: Buffer, | ||
proxyRes: http.IncomingMessage, | ||
req: http.IncomingMessage, | ||
res: http.ServerResponse | ||
) => Promise<Buffer | string>; | ||
|
||
/** | ||
* Intercept responses from upstream. | ||
* Automatically decompress (deflate, gzip, brotli). | ||
* Give developer the opportunity to modify intercepted Buffer and http.ServerResponse | ||
* | ||
* NOTE: must set options.selfHandleResponse=true (prevent automatic call of res.end()) | ||
*/ | ||
export function responseInterceptor(interceptor: Interceptor) { | ||
return async function proxyRes( | ||
proxyRes: http.IncomingMessage, | ||
req: http.IncomingMessage, | ||
res: http.ServerResponse | ||
): Promise<void> { | ||
const originalProxyRes = proxyRes; | ||
let buffer = Buffer.from('', 'utf8'); | ||
|
||
// decompress proxy response | ||
const _proxyRes = decompress(proxyRes, proxyRes.headers['content-encoding']); | ||
|
||
// concat data stream | ||
_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'] || ''); | ||
|
||
// call interceptor with intercepted response (buffer) | ||
const interceptedBuffer = Buffer.from(await interceptor(buffer, originalProxyRes, req, res)); | ||
|
||
// set correct content-length (with double byte character support) | ||
res.setHeader('content-length', Buffer.byteLength(interceptedBuffer, 'utf8')); | ||
|
||
res.write(interceptedBuffer); | ||
res.end(); | ||
}); | ||
|
||
_proxyRes.on('error', (error) => { | ||
res.end(`Error fetching proxied request: ${error.message}`); | ||
}); | ||
}; | ||
} | ||
|
||
/** | ||
* Streaming decompression of proxy response | ||
* source: https://github.com/apache/superset/blob/9773aba522e957ed9423045ca153219638a85d2f/superset-frontend/webpack.proxy-config.js#L116 | ||
*/ | ||
function decompress(proxyRes: http.IncomingMessage, contentEncoding: string) { | ||
let _proxyRes = proxyRes; | ||
let decompress; | ||
|
||
switch (contentEncoding) { | ||
case 'gzip': | ||
decompress = zlib.createGunzip(); | ||
break; | ||
case 'br': | ||
decompress = zlib.createBrotliDecompress(); | ||
break; | ||
case 'deflate': | ||
decompress = zlib.createInflate(); | ||
break; | ||
default: | ||
break; | ||
} | ||
|
||
if (decompress) { | ||
_proxyRes.pipe(decompress); | ||
_proxyRes = decompress; | ||
} | ||
|
||
return _proxyRes; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.