├── index.js ├── test ├── unit │ ├── _libs.js │ ├── handlers.spec.js │ ├── path-rewriter.spec.js │ ├── router.spec.js │ ├── config-factory.spec.js │ ├── logger.spec.js │ └── context-matcher.spec.js ├── e2e │ ├── _utils.js │ ├── express-router.spec.js │ ├── path-rewriter.spec.js │ ├── router.spec.js │ ├── websocket.spec.js │ └── http-proxy-middleware.spec.js └── lint.js ├── .editorconfig ├── .travis.yml ├── ISSUE_TEMPLATE.md ├── lib ├── errors.js ├── router.js ├── path-rewriter.js ├── handlers.js ├── context-matcher.js ├── config-factory.js ├── logger.js └── index.js ├── recipes ├── virtual-hosts.md ├── corporate-proxy.md ├── logLevel.md ├── https.md ├── basic.md ├── websocket.md ├── delay.md ├── shorthand.md ├── logProvider.md ├── pathRewrite.md ├── proxy-events.md ├── router.md ├── context-matching.md ├── modify-post.md ├── README.md └── servers.md ├── .gitignore ├── examples ├── express │ └── index.js ├── connect │ └── index.js ├── browser-sync │ └── index.js ├── README.md └── websocket │ ├── index.js │ └── index.html ├── LICENSE ├── package.json ├── CONTRIBUTING.md ├── CHANGELOG.md └── README.md /index.js: -------------------------------------------------------------------------------- 1 | var HPM = require('./lib') 2 | 3 | module.exports = function (context, opts) { 4 | return new HPM(context, opts) 5 | } 6 | -------------------------------------------------------------------------------- /test/unit/_libs.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | configFactory: require('../../lib/config-factory'), 3 | contextMatcher: require('../../lib/context-matcher'), 4 | handlers: require('../../lib/handlers'), 5 | Logger: require('../../lib/logger'), 6 | pathRewriter: require('../../lib/path-rewriter'), 7 | router: require('../../lib/router') 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = space 9 | indent_size = 2 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | # Matches the exact files either package.json or .travis.yml 17 | [{package.json,.travis.yml}] 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | - '8' 5 | - '7' 6 | - '6' 7 | - '5' 8 | - '4' 9 | before_install: 10 | # https://github.com/npm/npm/issues/11283 11 | - npm set progress=false 12 | after_success: npm run coveralls 13 | env: 14 | global: 15 | # COVERALLS_REPO_TOKEN= 16 | secure: "I8mDH0n2DQHAPvUFEV/ZNmrMNYTJxgywg8+P3yuCAWwQkZeXQi0DWG+6VUpOylaRhL/4kCdZK9gnJD2MfrqvNqVLDUqBK3tTmZVoyqqJEdE0UdEHcPncAPmxWrG98sDjqFN9r4nWeizHvyA1fNRQHRN56Zy+DcQWjgEhHJaYeNA=" 17 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected behavior 2 | 3 | ### Actual behavior 4 | 5 | ### Setup 6 | 7 | * http-proxy-middleware: _version_ 8 | * server: _connect/express/browser-sync..._ + _version_ 9 | * other relevant modules 10 | 11 | #### proxy middleware configuration 12 | ```javascript 13 | var apiProxy = proxy('/api', {target:'http://www.example.org', changeOrigin:true}); 14 | ``` 15 | #### server mounting 16 | ```javascript 17 | var app = express(); 18 | 19 | app.use(apiProxy); 20 | app.listen(3000); 21 | ``` 22 | -------------------------------------------------------------------------------- /test/e2e/_utils.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var proxyMiddleware = require('../../index') 3 | 4 | module.exports = { 5 | createServer: createServer, 6 | proxyMiddleware: proxyMiddleware 7 | } 8 | 9 | function createServer (portNumber, middleware, path) { 10 | var app = express() 11 | 12 | if (middleware && path) { 13 | app.use(path, middleware) 14 | } else if (middleware) { 15 | app.use(middleware) 16 | } 17 | 18 | var server = app.listen(portNumber) 19 | 20 | return server 21 | } 22 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | 3 | module.exports = { 4 | ERR_CONFIG_FACTORY_TARGET_MISSING: '[HPM] Missing "target" option. Example: {target: "http://www.example.org"}', 5 | ERR_CONTEXT_MATCHER_GENERIC: '[HPM] Invalid context. Expecting something like: "/api" or ["/api", "/ajax"]', 6 | ERR_CONTEXT_MATCHER_INVALID_ARRAY: '[HPM] Invalid context. Expecting something like: ["/api", "/ajax"] or ["/api/**", "!**.html"]', 7 | ERR_PATH_REWRITER_CONFIG: '[HPM] Invalid pathRewrite config. Expecting object with pathRewrite config or a rewrite function' 8 | } 9 | -------------------------------------------------------------------------------- /recipes/virtual-hosts.md: -------------------------------------------------------------------------------- 1 | # Name-based Virtual Hosts 2 | 3 | This example will create a basic proxy middleware for [virtual hosted sites](https://en.wikipedia.org/wiki/Virtual_hosting#Name-based). 4 | 5 | When `changeOrigin` is set to `true`; Host [HTTP header](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields) will be set to match target's host. 6 | 7 | The `changeOrigin` option is provided by [http-proxy](https://github.com/nodejitsu/node-http-proxy). 8 | 9 | ```javascript 10 | var proxy = require("http-proxy-middleware"); 11 | 12 | var options = { 13 | target: 'http://localhost:3000', 14 | changeOrigin:true 15 | }; 16 | 17 | var apiProxy = proxy('/api', options); 18 | ``` 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /recipes/corporate-proxy.md: -------------------------------------------------------------------------------- 1 | # Corporate Proxy Support 2 | 3 | This example will create a basic proxy middleware with corporate proxy support. 4 | 5 | Provide a custom `http.agent` with [https-proxy-agent](https://github.com/TooTallNate/node-https-proxy-agent) to connect to the corporate proxy server. 6 | 7 | ```javascript 8 | var HttpsProxyAgent = require('https-proxy-agent'); 9 | var proxy = require("http-proxy-middleware"); 10 | 11 | // corporate proxy to connect to 12 | var proxyServer = process.env.HTTPS_PROXY || 13 | process.env.HTTP_PROXY; 14 | 15 | var options = { 16 | target: 'http://localhost:3000', 17 | agent: new HttpsProxyAgent(proxyServer) 18 | }; 19 | 20 | var apiProxy = proxy('/api', options); 21 | ``` 22 | -------------------------------------------------------------------------------- /test/lint.js: -------------------------------------------------------------------------------- 1 | // Skip StandardJS on older node versions: < node@4.0.0 2 | // https://travis-ci.org/chimurai/http-proxy-middleware/builds/212791414 3 | 4 | var execSync = require('child_process').execSync 5 | var command = 'standard -v' 6 | 7 | if (!process.mainModule.children.length) { // workaround? prevent duplicate linting... 8 | if (isLegacyNodeJs()) { 9 | console.log('StandardJS: Skipping StandardJS on older Node versions') 10 | } else { 11 | execSync(command, {stdio: [0, 1, 2]}) // https://stackoverflow.com/a/31104898/3841188 12 | } 13 | } 14 | 15 | function isLegacyNodeJs () { 16 | var majorVersion = parseInt(process.versions.node[0], 10) 17 | var isModernNodeJs = (majorVersion > 0) 18 | return !isModernNodeJs 19 | } 20 | -------------------------------------------------------------------------------- /recipes/logLevel.md: -------------------------------------------------------------------------------- 1 | # Log Level 2 | 3 | Control the amount of logging of http-proxy-middleware. 4 | 5 | Possible values: 6 | * `debug` 7 | * `info` 8 | * `warn` (default) 9 | * `error` 10 | * `silent` 11 | 12 | ## Level: debug 13 | 14 | Log everyting. 15 | 16 | ```javascript 17 | var proxy = require("http-proxy-middleware"); 18 | 19 | var options = { 20 | target: 'http://localhost:3000', 21 | logLevel: 'debug' 22 | }; 23 | 24 | var apiProxy = proxy('/api', options); 25 | ``` 26 | 27 | ## Level: silent 28 | 29 | Suppress all logging. 30 | 31 | ```javascript 32 | var proxy = require("http-proxy-middleware"); 33 | 34 | var options = { 35 | target: 'http://localhost:3000', 36 | logLevel: 'silent' 37 | }; 38 | 39 | var apiProxy = proxy('/api', options); 40 | ``` 41 | -------------------------------------------------------------------------------- /examples/express/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var express = require('express') 5 | var proxy = require('../../index') // require('http-proxy-middleware'); 6 | 7 | /** 8 | * Configure proxy middleware 9 | */ 10 | var jsonPlaceholderProxy = proxy({ 11 | target: 'http://jsonplaceholder.typicode.com', 12 | changeOrigin: true, // for vhosted sites, changes host header to match to target's host 13 | logLevel: 'debug' 14 | }) 15 | 16 | var app = express() 17 | 18 | /** 19 | * Add the proxy to express 20 | */ 21 | app.use('/users', jsonPlaceholderProxy) 22 | 23 | app.listen(3000) 24 | 25 | console.log('[DEMO] Server: listening on port 3000') 26 | console.log('[DEMO] Opening: http://localhost:3000/users') 27 | 28 | require('opn')('http://localhost:3000/users') 29 | -------------------------------------------------------------------------------- /examples/connect/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var http = require('http') 5 | var connect = require('connect') 6 | var proxy = require('../../index') // require('http-proxy-middleware'); 7 | 8 | /** 9 | * Configure proxy middleware 10 | */ 11 | var jsonPlaceholderProxy = proxy({ 12 | target: 'http://jsonplaceholder.typicode.com', 13 | changeOrigin: true, // for vhosted sites, changes host header to match to target's host 14 | logLevel: 'debug' 15 | }) 16 | 17 | var app = connect() 18 | 19 | /** 20 | * Add the proxy to connect 21 | */ 22 | app.use('/users', jsonPlaceholderProxy) 23 | 24 | http.createServer(app).listen(3000) 25 | 26 | console.log('[DEMO] Server: listening on port 3000') 27 | console.log('[DEMO] Opening: http://localhost:3000/users') 28 | 29 | require('opn')('http://localhost:3000/users') 30 | -------------------------------------------------------------------------------- /examples/browser-sync/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var browserSync = require('browser-sync').create() 5 | var proxy = require('../../index') // require('http-proxy-middleware'); 6 | 7 | /** 8 | * Configure proxy middleware 9 | */ 10 | var jsonPlaceholderProxy = proxy('/users', { 11 | target: 'http://jsonplaceholder.typicode.com', 12 | changeOrigin: true, // for vhosted sites, changes host header to match to target's host 13 | logLevel: 'debug' 14 | }) 15 | 16 | /** 17 | * Add the proxy to browser-sync 18 | */ 19 | browserSync.init({ 20 | server: { 21 | baseDir: './', 22 | port: 3000, 23 | middleware: [jsonPlaceholderProxy] 24 | }, 25 | startPath: '/users' 26 | }) 27 | 28 | console.log('[DEMO] Server: listening on port 3000') 29 | console.log('[DEMO] Opening: http://localhost:3000/users') 30 | -------------------------------------------------------------------------------- /recipes/https.md: -------------------------------------------------------------------------------- 1 | # HTTPS 2 | 3 | How to proxy requests to various types of HTTPS servers. 4 | 5 | All options are provided by [http-proxy](https://github.com/nodejitsu/node-http-proxy). 6 | 7 | ## Basic proxy to an HTTPS server 8 | 9 | ```javascript 10 | var proxy = require('http-proxy-middleware'); 11 | 12 | var apiProxy = proxy('/api', { 13 | target: 'https://example.org', 14 | changeOrigin: true, 15 | }); 16 | ``` 17 | 18 | ## Proxy to an HTTPS server using a PKCS12 client certificate 19 | 20 | ```javascript 21 | var fs = require('fs'); 22 | var proxy = require('http-proxy-middleware'); 23 | 24 | var apiProxy = proxy('/api', { 25 | target: { 26 | protocol: 'https:', 27 | host: 'example.org', 28 | port: 443, 29 | pfx: fs.readFileSync('path/to/certificate.p12'), 30 | passphrase: 'password', 31 | }, 32 | changeOrigin: true, 33 | }); 34 | ``` 35 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | View working examples of `http-proxy-middleware` implemented in popular servers. 4 | 5 | To run and view the [examples](https://github.com/chimurai/http-proxy-middleware/tree/master/examples); Clone the `http-proxy-middleware` repo and install the dependencies: 6 | 7 | ```bash 8 | $ git clone https://github.com/chimurai/http-proxy-middleware.git 9 | $ cd http-proxy-middleware 10 | $ npm install 11 | ``` 12 | 13 | Run the example from root folder: 14 | 15 | ```bash 16 | $ node examples/browser-sync 17 | ``` 18 | 19 | ```bash 20 | $ node examples/connect 21 | ``` 22 | 23 | ```bash 24 | $ node examples/express 25 | ``` 26 | 27 | ```bash 28 | $ node examples/websocket 29 | ``` 30 | 31 | ## Server recipes 32 | 33 | You can find more server implementations with `http-proxy-middleware` in the [server recipes](https://github.com/chimurai/http-proxy-middleware/tree/master/recipes/servers.md) 34 | -------------------------------------------------------------------------------- /recipes/basic.md: -------------------------------------------------------------------------------- 1 | # Basic usage 2 | 3 | This example will create a basic proxy middleware. 4 | 5 | ```javascript 6 | var proxy = require("http-proxy-middleware"); 7 | 8 | var apiProxy = proxy('/api', {target: 'http://localhost:3000'}); 9 | // \____/ \________________________________/ 10 | // | | 11 | // context options 12 | ``` 13 | 14 | ## Alternative configuration 15 | 16 | The proxy behavior of the following examples are **exactly** the same; Just different ways to configure it. 17 | 18 | ```javascript 19 | app.use(proxy('/api', {target: 'http://localhost:3000', changeOrigin:true})); 20 | ``` 21 | 22 | ```javascript 23 | app.use(proxy('http://localhost:3000/api', {changeOrigin:true})); 24 | ``` 25 | 26 | ```javascript 27 | app.use('/api', proxy('http://localhost:3000', {changeOrigin:true})); 28 | ``` 29 | 30 | ```javascript 31 | app.use('/api', proxy({target: 'http://localhost:3000', changeOrigin:true})); 32 | ``` 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Steven Chim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /recipes/websocket.md: -------------------------------------------------------------------------------- 1 | # WebSocket 2 | 3 | This example will create a proxy middleware with websocket support. 4 | 5 | ```javascript 6 | var proxy = require("http-proxy-middleware"); 7 | 8 | var socketProxy = proxy('/socket', {target: 'http://localhost:3000', ws: true}); 9 | ``` 10 | 11 | ## WebSocket - Path Rewrite 12 | 13 | This example will create a proxy middleware with websocket support and pathRewrite. 14 | 15 | ```javascript 16 | var proxy = require("http-proxy-middleware"); 17 | 18 | var options = { 19 | target: 'http://localhost:3000', 20 | ws: true, 21 | pathRewrite: { 22 | '^/socket' : '' 23 | } 24 | }; 25 | 26 | var socketProxy = proxy('/socket', options); 27 | ``` 28 | 29 | ## WebSocket - Server update subscription 30 | 31 | This example will create a proxy middleware with websocket support. 32 | 33 | Subscribe to server's upgrade event. 34 | 35 | ```javascript 36 | var proxy = require("http-proxy-middleware"); 37 | 38 | var socketProxy = proxy('/socket', {target: 'http://localhost:3000', ws: true}); 39 | 40 | server.on('upgrade', proxy.upgrade); // <-- subscribe to http 'upgrade' 41 | ``` 42 | -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | var logger = require('./logger.js').getInstance() 3 | 4 | module.exports = { 5 | getTarget: getTarget 6 | } 7 | 8 | function getTarget (req, config) { 9 | var newTarget 10 | var router = config.router 11 | 12 | if (_.isPlainObject(router)) { 13 | newTarget = getTargetFromProxyTable(req, router) 14 | } else if (_.isFunction(router)) { 15 | newTarget = router(req) 16 | } 17 | 18 | return newTarget 19 | } 20 | 21 | function getTargetFromProxyTable (req, table) { 22 | var result 23 | var host = req.headers.host 24 | var path = req.url 25 | 26 | var hostAndPath = host + path 27 | 28 | _.forIn(table, function (value, key) { 29 | if (containsPath(key)) { 30 | if (hostAndPath.indexOf(key) > -1) { // match 'localhost:3000/api' 31 | result = table[key] 32 | logger.debug('[HPM] Router table match: "%s"', key) 33 | return false 34 | } 35 | } else { 36 | if (key === host) { // match 'localhost:3000' 37 | result = table[key] 38 | logger.debug('[HPM] Router table match: "%s"', host) 39 | return false 40 | } 41 | } 42 | }) 43 | 44 | return result 45 | } 46 | 47 | function containsPath (v) { 48 | return v.indexOf('/') > -1 49 | } 50 | -------------------------------------------------------------------------------- /recipes/delay.md: -------------------------------------------------------------------------------- 1 | # Delay proxied request/response 2 | 3 | Sometimes we need the ability to delay request to backend server or response from it back to client to test long execution time of particular url or all of them. With DevTool's [network throttling](https://developers.google.com/web/tools/chrome-devtools/profile/network-performance/network-conditions?hl=en) we can test slowdown of all request, not separately. 4 | But we can handle each request individually via our proxy, and add delay to its execution time. 5 | 6 | Let's assume that we want slow down the access to backend's `/api/get-me-something` resource. Delay request time by 2 seconds and increase response time by 5 seconds. 7 | 8 | For achieving it just put additional route handler to your app before proxy handler: 9 | 10 | ```javascript 11 | const myProxy = proxy({ 12 | target: 'http://www.example.com', 13 | changeOrigin: true 14 | }); 15 | const proxyDelay = function (req, res, next) { 16 | if (req.originalUrl === '/api/get-me-something') { 17 | // Delay request by 2 seconds 18 | setTimeout(next, 2000); 19 | 20 | // Delay response completion by 5 seconds 21 | const endOriginal = res.end; 22 | res.end = function (...args) { 23 | setTimeout(function () { 24 | endOriginal.apply(res, args); 25 | }, 5000); 26 | }; 27 | } else { 28 | next(); 29 | } 30 | }; 31 | 32 | app.use('/api', proxyDelay, myProxy); 33 | ``` 34 | 35 | And you will see result in devtools similar to this: 36 | 37 | ![http-proxy-delay](https://cloud.githubusercontent.com/assets/576077/15839924/49ebe256-2bfb-11e6-8591-ef0101670885.png) 38 | -------------------------------------------------------------------------------- /examples/websocket/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var express = require('express') 5 | var proxy = require('../../index') // require('http-proxy-middleware'); 6 | 7 | /** 8 | * Configure proxy middleware 9 | */ 10 | var wsProxy = proxy('/', { 11 | target: 'http://echo.websocket.org', 12 | // pathRewrite: { 13 | // '^/websocket' : '/socket', // rewrite path. 14 | // '^/removepath' : '' // remove path. 15 | // }, 16 | changeOrigin: true, // for vhosted sites, changes host header to match to target's host 17 | ws: true, // enable websocket proxy 18 | logLevel: 'debug' 19 | }) 20 | 21 | var app = express() 22 | app.use('/', express.static(__dirname)) // demo page 23 | app.use(wsProxy) // add the proxy to express 24 | 25 | var server = app.listen(3000) 26 | server.on('upgrade', wsProxy.upgrade) // optional: upgrade externally 27 | 28 | console.log('[DEMO] Server: listening on port 3000') 29 | console.log('[DEMO] Opening: http://localhost:3000') 30 | 31 | require('opn')('http://localhost:3000') 32 | 33 | /** 34 | * Example: 35 | * Open http://localhost:3000 in WebSocket compatible browser. 36 | * In browser console: 37 | * 1. `var socket = new WebSocket('ws://localhost:3000');` // create new WebSocket 38 | * 2. `socket.onmessage = function (msg) {console.log(msg)};` // listen to socket messages 39 | * 3. `socket.send('hello world');` // send message 40 | * > {data: "hello world"} // server should echo back your message. 41 | **/ 42 | -------------------------------------------------------------------------------- /test/e2e/express-router.spec.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var expect = require('chai').expect 3 | var http = require('http') 4 | var proxy = require('../../index') 5 | 6 | describe('Usage in Express', function () { 7 | var app 8 | var server 9 | 10 | beforeEach(function () { 11 | app = express() 12 | }) 13 | 14 | afterEach(function () { 15 | server && server.close() 16 | }) 17 | 18 | // https://github.com/chimurai/http-proxy-middleware/issues/94 19 | describe('Express Sub Route', function () { 20 | beforeEach(function () { 21 | // sub route config 22 | var sub = new express.Router() 23 | 24 | function filter (pathname, req) { 25 | var urlFilter = new RegExp('^/sub/api') 26 | var match = urlFilter.test(pathname) 27 | return match 28 | } 29 | 30 | /** 31 | * Mount proxy without 'path' in sub route 32 | */ 33 | var proxyConfig = {target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, logLevel: 'silent'} 34 | sub.use(proxy(filter, proxyConfig)) 35 | 36 | sub.get('/hello', jsonMiddleware({'content': 'foobar'})) 37 | 38 | // configure sub route on /sub junction 39 | app.use('/sub', sub) 40 | 41 | // start server 42 | server = app.listen(3000) 43 | }) 44 | 45 | it('should still return a response when route does not match proxyConfig', function (done) { 46 | var responseBody 47 | http.get('http://localhost:3000/sub/hello', function (res) { 48 | res.on('data', function (chunk) { 49 | responseBody = chunk.toString() 50 | expect(responseBody).to.equal('{"content":"foobar"}') 51 | done() 52 | }) 53 | }) 54 | }) 55 | }) 56 | 57 | function jsonMiddleware (data) { 58 | return function (req, res) { 59 | res.json(data) 60 | } 61 | } 62 | }) 63 | -------------------------------------------------------------------------------- /recipes/shorthand.md: -------------------------------------------------------------------------------- 1 | # Shorthand 2 | 3 | This example will create a proxy middleware using the shorthand notation. 4 | 5 | The http-proxy-middleware `context` and `config.target` will be set automatically. 6 | 7 | ```javascript 8 | var proxy = require("http-proxy-middleware"); 9 | 10 | var apiProxy = proxy('http://localhost:3000/api'); 11 | 12 | // equivalent: 13 | // var apiProxy = proxy('/api', {target:'http://localhost:3000'}); 14 | ``` 15 | 16 | ## Shorthand - Wildcard context 17 | 18 | This example will create a proxy middleware with shorthand wildcard context. 19 | 20 | ```javascript 21 | var proxy = require("http-proxy-middleware"); 22 | 23 | var apiProxy = proxy('http://localhost:3000/api/books/*/**.json'); 24 | // equals: 25 | // var apiProxy = proxy('/api/books/*/**.json', {target:'http://localhost:3000'}); 26 | ``` 27 | 28 | 29 | ## Shorthand with additional configuration 30 | 31 | This example will create a proxy middleware with shorthand and additional configuration. 32 | 33 | ```javascript 34 | var proxy = require("http-proxy-middleware"); 35 | 36 | var apiProxy = proxy('http://localhost:3000/api', {changeOrigin: true}); 37 | // equals: 38 | // var apiProxy = proxy('/api', {target:'http://localhost:3000', {changeOrigin:true}}); 39 | ``` 40 | 41 | ## Shorthand - WebSocket 42 | 43 | This example will create a proxy middleware with shorthand and additional configuration for WebSocket support. 44 | 45 | ```javascript 46 | var proxy = require("http-proxy-middleware"); 47 | 48 | var apiProxy = proxy('http://localhost:3000/api', {ws: true}); 49 | // equals: 50 | // var apiProxy = proxy('/api', {target:'http://localhost:3000', ws: true}); 51 | ``` 52 | 53 | ## Shorthand - WebSocket only 54 | 55 | This example will create a proxy middleware with websocket shorthand only configuration. 56 | 57 | ```javascript 58 | var proxy = require("http-proxy-middleware"); 59 | 60 | var apiProxy = proxy('ws://localhost:3000/api'); 61 | // equals: 62 | // var apiProxy = proxy('/api', {target:'ws://localhost:3000', ws: true}); 63 | ``` 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http-proxy-middleware", 3 | "version": "0.17.4", 4 | "description": "The one-liner node.js proxy middleware for connect, express and browser-sync", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js", 8 | "lib" 9 | ], 10 | "scripts": { 11 | "clean": "rm -rf coverage", 12 | "lint": "node test/lint.js", 13 | "test": "npm run lint && mocha --recursive --colors --reporter spec", 14 | "cover": "npm run clean && istanbul cover ./node_modules/mocha/bin/_mocha -- --recursive", 15 | "coveralls": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- --recursive --reporter spec && istanbul-coveralls && npm run clean" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/chimurai/http-proxy-middleware.git" 20 | }, 21 | "keywords": [ 22 | "reverse", 23 | "proxy", 24 | "middleware", 25 | "http", 26 | "https", 27 | "connect", 28 | "express", 29 | "browser-sync", 30 | "gulp", 31 | "grunt-contrib-connect", 32 | "websocket", 33 | "ws", 34 | "cors" 35 | ], 36 | "author": "Steven Chim", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/chimurai/http-proxy-middleware/issues" 40 | }, 41 | "homepage": "https://github.com/chimurai/http-proxy-middleware", 42 | "devDependencies": { 43 | "browser-sync": "^2.18.2", 44 | "chai": "^4.1.2", 45 | "connect": "^3.5.0", 46 | "coveralls": "^3.0.0", 47 | "express": "^4.14.0", 48 | "istanbul": "^0.4.5", 49 | "istanbul-coveralls": "^1.0.3", 50 | "mocha": "^4.0.1", 51 | "mocha-lcov-reporter": "1.3.0", 52 | "opn": "^5.1.0", 53 | "standard": "^10.0.2", 54 | "ws": "^3.2.0" 55 | }, 56 | "dependencies": { 57 | "http-proxy": "^1.16.2", 58 | "is-glob": "^4.0.0", 59 | "lodash": "^4.17.2", 60 | "micromatch": "^3.1.0" 61 | }, 62 | "engines": { 63 | "node" : ">=4.0.0" 64 | }, 65 | "standard": { 66 | "env": [ 67 | "mocha" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /recipes/logProvider.md: -------------------------------------------------------------------------------- 1 | # Log Provider 2 | 3 | Configure your own logger with the `logProvider` option. 4 | 5 | In this example [winston](https://www.npmjs.com/package/winston) is configured to do the actual logging. 6 | 7 | ```javascript 8 | var winston = require('winston'); 9 | var proxy = require("http-proxy-middleware"); 10 | 11 | var options = { 12 | target: 'http://localhost:3000', 13 | logProvider: function (provider) { 14 | return winston; 15 | } 16 | }; 17 | 18 | var apiProxy = proxy('/api', options); 19 | ``` 20 | 21 | ## Winston 22 | 23 | Configure your own logger with the `logProvider` option. 24 | 25 | In this example [winston](https://www.npmjs.com/package/winston) is configured to do the actual logging. Map the logging api if needed. 26 | 27 | ```javascript 28 | 29 | var winston = require('winston'); 30 | var proxy = require("http-proxy-middleware"); 31 | 32 | var logProvider = function (provider) { 33 | return { 34 | log : winston.log, 35 | debug : winston.debug, 36 | info : winston.info, 37 | warn : winston.warn, 38 | error : winston.error 39 | }; 40 | }; 41 | 42 | var options = { 43 | target: 'http://localhost:3000', 44 | logProvider: logProvider 45 | }; 46 | 47 | var apiProxy = proxy('/api', options); 48 | ``` 49 | 50 | # Winston Multi Transport 51 | 52 | Configure your own logger with the `logProvider` option. 53 | 54 | In this example [winston](https://www.npmjs.com/package/winston) is configured to do the actual logging. 55 | 56 | ```javascript 57 | var winston = require('winston'); 58 | var proxy = require("http-proxy-middleware"); 59 | 60 | var logProvider = function (provider) { 61 | var logger = new (winston.Logger)({ 62 | transports: [ 63 | new (winston.transports.Console)(), 64 | new (winston.transports.File)({ filename: 'somefile.log' }) 65 | ] 66 | }); 67 | 68 | return logger; 69 | }; 70 | 71 | var options = { 72 | target: 'http://localhost:3000', 73 | logProvider: logProvider 74 | }; 75 | 76 | var apiProxy = proxy('/api', options); 77 | ``` 78 | -------------------------------------------------------------------------------- /lib/path-rewriter.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | var logger = require('./logger').getInstance() 3 | var ERRORS = require('./errors') 4 | 5 | module.exports = { 6 | create: createPathRewriter 7 | } 8 | 9 | /** 10 | * Create rewrite function, to cache parsed rewrite rules. 11 | * 12 | * @param {Object} rewriteConfig 13 | * @return {Function} Function to rewrite paths; This function should accept `path` (request.url) as parameter 14 | */ 15 | function createPathRewriter (rewriteConfig) { 16 | var rulesCache 17 | 18 | if (!isValidRewriteConfig(rewriteConfig)) { 19 | return 20 | } 21 | 22 | if (_.isFunction(rewriteConfig)) { 23 | var customRewriteFn = rewriteConfig 24 | return customRewriteFn 25 | } else { 26 | rulesCache = parsePathRewriteRules(rewriteConfig) 27 | return rewritePath 28 | } 29 | 30 | function rewritePath (path) { 31 | var result = path 32 | 33 | _.forEach(rulesCache, function (rule) { 34 | if (rule.regex.test(path)) { 35 | result = result.replace(rule.regex, rule.value) 36 | logger.debug('[HPM] Rewriting path from "%s" to "%s"', path, result) 37 | return false 38 | } 39 | }) 40 | 41 | return result 42 | } 43 | } 44 | 45 | function isValidRewriteConfig (rewriteConfig) { 46 | if (_.isFunction(rewriteConfig)) { 47 | return true 48 | } else if (!_.isEmpty(rewriteConfig) && _.isPlainObject(rewriteConfig)) { 49 | return true 50 | } else if (_.isUndefined(rewriteConfig) || 51 | _.isNull(rewriteConfig) || 52 | _.isEqual(rewriteConfig, {})) { 53 | return false 54 | } else { 55 | throw new Error(ERRORS.ERR_PATH_REWRITER_CONFIG) 56 | } 57 | } 58 | 59 | function parsePathRewriteRules (rewriteConfig) { 60 | var rules = [] 61 | 62 | if (_.isPlainObject(rewriteConfig)) { 63 | _.forIn(rewriteConfig, function (value, key) { 64 | rules.push({ 65 | regex: new RegExp(key), 66 | value: rewriteConfig[key] 67 | }) 68 | logger.info('[HPM] Proxy rewrite rule created: "%s" ~> "%s"', key, rewriteConfig[key]) 69 | }) 70 | } 71 | 72 | return rules 73 | } 74 | -------------------------------------------------------------------------------- /recipes/pathRewrite.md: -------------------------------------------------------------------------------- 1 | # pathRewrite 2 | 3 | Modify request paths before requests are send to the target. 4 | 5 | 6 | 7 | - [rewrite paths](#rewrite-paths) 8 | - [remove paths](#remove-paths) 9 | - [add paths](#add-paths) 10 | - [custom rewrite function](#custom-rewrite-function) 11 | 12 | 13 | 14 | 15 | ## rewrite paths 16 | 17 | Rewrite paths 18 | 19 | ```javascript 20 | var proxy = require('http-proxy-middleware'); 21 | 22 | var options = { 23 | target: 'http://localhost:3000', 24 | pathRewrite: { 25 | '^/api/old-path' : '/api/new-path', // rewrite path 26 | }, 27 | }; 28 | 29 | var apiProxy = proxy('/api', options); 30 | 31 | // `/old/api/foo/bar` -> `http://localhost:3000/new/api/foo/bar` 32 | ``` 33 | 34 | ## remove paths 35 | 36 | Remove base path 37 | 38 | ```javascript 39 | var proxy = require('http-proxy-middleware'); 40 | 41 | var options = { 42 | target: 'http://localhost:3000', 43 | pathRewrite: { 44 | '^/api/' : '/' // remove base path 45 | } 46 | }; 47 | 48 | var apiProxy = proxy('/api', options); 49 | 50 | // `/api/lorum/ipsum` -> `http://localhost:3000/lorum/ipsum` 51 | ``` 52 | 53 | ## add paths 54 | 55 | Add base path 56 | 57 | ```javascript 58 | var proxy = require('http-proxy-middleware'); 59 | 60 | var options = { 61 | target: 'http://localhost:3000', 62 | pathRewrite: { 63 | '^/' : '/extra/' // add base path 64 | } 65 | }; 66 | 67 | var apiProxy = proxy('/api', options); 68 | 69 | // `/api/lorum/ipsum` -> `http://localhost:3000/extra/api/lorum/ipsum` 70 | ``` 71 | 72 | ## custom rewrite function 73 | 74 | Implement you own path rewrite logic. 75 | 76 | The unmodified path will be used, when rewrite function returns `undefined` 77 | 78 | ```javascript 79 | var proxy = require('http-proxy-middleware'); 80 | 81 | var rewriteFn = function (path, req) { 82 | return path.replace('/api/foo', '/api/bar'); 83 | } 84 | 85 | var options = { 86 | target: 'http://localhost:3000', 87 | pathRewrite: rewriteFn 88 | }; 89 | 90 | var apiProxy = proxy('/api', options); 91 | 92 | // `/api/foo/lorum/ipsum` -> `http://localhost:3000/api/bar/lorum/ipsum` 93 | ``` 94 | -------------------------------------------------------------------------------- /lib/handlers.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | var logger = require('./logger').getInstance() 3 | 4 | module.exports = { 5 | init: init, 6 | getHandlers: getProxyEventHandlers 7 | } 8 | 9 | function init (proxy, opts) { 10 | var handlers = getProxyEventHandlers(opts) 11 | 12 | _.forIn(handlers, function (handler, eventName) { 13 | proxy.on(eventName, handlers[eventName]) 14 | }) 15 | 16 | logger.debug('[HPM] Subscribed to http-proxy events: ', _.keys(handlers)) 17 | } 18 | 19 | function getProxyEventHandlers (opts) { 20 | // https://github.com/nodejitsu/node-http-proxy#listening-for-proxy-events 21 | var proxyEvents = ['error', 'proxyReq', 'proxyReqWs', 'proxyRes', 'open', 'close'] 22 | var handlers = {} 23 | 24 | _.forEach(proxyEvents, function (event) { 25 | // all handlers for the http-proxy events are prefixed with 'on'. 26 | // loop through options and try to find these handlers 27 | // and add them to the handlers object for subscription in init(). 28 | var eventName = _.camelCase('on ' + event) 29 | var fnHandler = _.get(opts, eventName) 30 | 31 | if (_.isFunction(fnHandler)) { 32 | handlers[event] = fnHandler 33 | } 34 | }) 35 | 36 | // add default error handler in absence of error handler 37 | if (!_.isFunction(handlers.error)) { 38 | handlers.error = defaultErrorHandler 39 | } 40 | 41 | // add default close handler in absence of close handler 42 | if (!_.isFunction(handlers.close)) { 43 | handlers.close = logClose 44 | } 45 | 46 | return handlers 47 | } 48 | 49 | function defaultErrorHandler (err, req, res) { 50 | var host = (req.headers && req.headers.host) 51 | var code = err.code 52 | 53 | if (res.writeHead && !res.headersSent) { 54 | if (/HPE_INVALID/.test(code)) { 55 | res.writeHead(502) 56 | } else { 57 | switch (code) { 58 | case 'ECONNRESET': 59 | case 'ENOTFOUND': 60 | case 'ECONNREFUSED': 61 | res.writeHead(504) 62 | break 63 | default: res.writeHead(500) 64 | } 65 | } 66 | } 67 | 68 | res.end('Error occured while trying to proxy to: ' + host + req.url) 69 | } 70 | 71 | function logClose (req, socket, head) { 72 | // view disconnected websocket connections 73 | logger.info('[HPM] Client disconnected') 74 | } 75 | -------------------------------------------------------------------------------- /recipes/proxy-events.md: -------------------------------------------------------------------------------- 1 | # Proxy Events 2 | 3 | Subscribe to [`http-proxy`](https://github.com/nodejitsu/node-http-proxy) [![GitHub stars](https://img.shields.io/github/stars/nodejitsu/node-http-proxy.svg?style=social&label=Star)](https://github.com/nodejitsu/node-http-proxy) events: `error`, `proxyReq`, `proxyReqWs`, `proxyRes`, `open`, `close`. 4 | 5 | ## onError 6 | 7 | Subscribe to http-proxy's [error event](https://www.npmjs.com/package/http-proxy#listening-for-proxy-events). 8 | 9 | ```javascript 10 | var proxy = require("http-proxy-middleware"); 11 | 12 | var onError = function (err, req, res) { 13 | console.log('Something went wrong.'); 14 | console.log('And we are reporting a custom error message.'); 15 | }; 16 | 17 | var options = {target:'http://localhost:3000', onError: onError}; 18 | 19 | var apiProxy = proxy('/api', options); 20 | ``` 21 | 22 | ## onProxyReq 23 | 24 | Subscribe to http-proxy's [proxyReq event](https://www.npmjs.com/package/http-proxy#listening-for-proxy-events). 25 | 26 | ```javascript 27 | var proxy = require("http-proxy-middleware"); 28 | 29 | var onProxyReq = function (proxyReq, req, res) { 30 | // add new header to request 31 | proxyReq.setHeader('x-added', 'foobar'); 32 | }; 33 | 34 | var options = {target:'http://localhost:3000', onProxyReq: onProxyReq}; 35 | 36 | var apiProxy = proxy('/api', options); 37 | ``` 38 | 39 | ## onProxyReqWs 40 | 41 | Subscribe to http-proxy's [proxyReqWs event](https://www.npmjs.com/package/http-proxy#listening-for-proxy-events). 42 | 43 | ```javascript 44 | var proxy = require("http-proxy-middleware"); 45 | 46 | var onProxyReqWs = function (proxyReq, req, socket, options, head) { 47 | // add custom header 48 | proxyReq.setHeader('X-Special-Proxy-Header', 'foobar'); 49 | }; 50 | 51 | var options = {target:'http://localhost:3000', onProxyReqWs: onProxyReqWs}; 52 | 53 | var apiProxy = proxy('/api', options); 54 | ``` 55 | 56 | ## onProxyRes 57 | 58 | Subscribe to http-proxy's [proxyRes event](https://www.npmjs.com/package/http-proxy#listening-for-proxy-events). 59 | 60 | ```javascript 61 | var proxy = require("http-proxy-middleware"); 62 | 63 | var onProxyRes = function (proxyRes, req, res) { 64 | // add new header to response 65 | proxyRes.headers['x-added'] = 'foobar'; 66 | 67 | // remove header from response 68 | delete proxyRes.headers['x-removed']; 69 | }; 70 | 71 | var options = {target:'http://localhost:3000', onProxyRes: onProxyRes}; 72 | 73 | var apiProxy = proxy('/api', options); 74 | ``` 75 | -------------------------------------------------------------------------------- /lib/context-matcher.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | var url = require('url') 3 | var isGlob = require('is-glob') 4 | var micromatch = require('micromatch') 5 | var ERRORS = require('./errors') 6 | 7 | module.exports = { 8 | match: matchContext 9 | } 10 | 11 | function matchContext (context, uri, req) { 12 | // single path 13 | if (isStringPath(context)) { 14 | return matchSingleStringPath(context, uri) 15 | } 16 | 17 | // single glob path 18 | if (isGlobPath(context)) { 19 | return matchSingleGlobPath(context, uri) 20 | } 21 | 22 | // multi path 23 | if (Array.isArray(context)) { 24 | if (context.every(isStringPath)) { 25 | return matchMultiPath(context, uri) 26 | } 27 | if (context.every(isGlobPath)) { 28 | return matchMultiGlobPath(context, uri) 29 | } 30 | 31 | throw new Error(ERRORS.ERR_CONTEXT_MATCHER_INVALID_ARRAY) 32 | } 33 | 34 | // custom matching 35 | if (_.isFunction(context)) { 36 | var pathname = getUrlPathName(uri) 37 | return context(pathname, req) 38 | } 39 | 40 | throw new Error(ERRORS.ERR_CONTEXT_MATCHER_GENERIC) 41 | } 42 | 43 | /** 44 | * @param {String} context '/api' 45 | * @param {String} uri 'http://example.org/api/b/c/d.html' 46 | * @return {Boolean} 47 | */ 48 | function matchSingleStringPath (context, uri) { 49 | var pathname = getUrlPathName(uri) 50 | return pathname.indexOf(context) === 0 51 | } 52 | 53 | function matchSingleGlobPath (pattern, uri) { 54 | var pathname = getUrlPathName(uri) 55 | var matches = micromatch(pathname, pattern) 56 | return matches && (matches.length > 0) 57 | } 58 | 59 | function matchMultiGlobPath (patternList, uri) { 60 | return matchSingleGlobPath(patternList, uri) 61 | } 62 | 63 | /** 64 | * @param {String} contextList ['/api', '/ajax'] 65 | * @param {String} uri 'http://example.org/api/b/c/d.html' 66 | * @return {Boolean} 67 | */ 68 | function matchMultiPath (contextList, uri) { 69 | for (var i = 0; i < contextList.length; i++) { 70 | var context = contextList[i] 71 | if (matchSingleStringPath(context, uri)) { 72 | return true 73 | } 74 | } 75 | return false 76 | } 77 | 78 | /** 79 | * Parses URI and returns RFC 3986 path 80 | * 81 | * @param {String} uri from req.url 82 | * @return {String} RFC 3986 path 83 | */ 84 | function getUrlPathName (uri) { 85 | return uri && url.parse(uri).pathname 86 | } 87 | 88 | function isStringPath (context) { 89 | return _.isString(context) && !isGlob(context) 90 | } 91 | 92 | function isGlobPath (context) { 93 | return isGlob(context) 94 | } 95 | -------------------------------------------------------------------------------- /recipes/router.md: -------------------------------------------------------------------------------- 1 | # router 2 | 3 | Allows you to route to a different `target` by using a table of a custom function. 4 | 5 | 6 | 7 | - [Custom router function](#custom-router-function) 8 | - [Proxy Table](#proxy-table) 9 | 10 | 11 | 12 | 13 | ## Custom router function 14 | 15 | Write your own router to dynamically route to a different `target`. 16 | The `req` object is provided to retrieve contextual data. 17 | 18 | ```javascript 19 | var express = require('express'); 20 | var proxy = require("http-proxy-middleware"); 21 | 22 | var customRouter = function(req) { 23 | return 'http://www.example.org' // protocol + host 24 | }; 25 | 26 | var options = { 27 | target: 'http://localhost:8000', 28 | router: customRouter 29 | }; 30 | 31 | var myProxy = proxy(options); 32 | 33 | var app = express(); 34 | app.use(myProxy); // add the proxy to express 35 | 36 | app.listen(3000); 37 | ``` 38 | 39 | 40 | ## Proxy Table 41 | 42 | Use a Proxy Table to proxy requests to a different `target` based on: 43 | * Host [HTTP header](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields). 44 | * Request path 45 | * Host HTTP header + path 46 | 47 | ```javascript 48 | var express = require('express'); 49 | var proxy = require("http-proxy-middleware"); 50 | 51 | var proxyTable = { 52 | "integration.localhost:3000" : "http://localhost:8001", // host only 53 | "staging.localhost:3000" : "http://localhost:8002", // host only 54 | "localhost:3000/api" : "http://localhost:8003", // host + path 55 | "/rest" : "http://localhost:8004" // path only 56 | }; 57 | 58 | var options = { 59 | target: 'http://localhost:8000', 60 | router: proxyTable 61 | }; 62 | 63 | var myProxy = proxy(options); 64 | 65 | var app = express(); 66 | app.use(myProxy); // add the proxy to express 67 | 68 | app.listen(3000); 69 | ``` 70 | 71 | ### Example 72 | 73 | In the example above; all requests will be proxied to `http://localhost:8000`. 74 | 75 | When request's `Host HTTP header` and/or `path` match a configuration in the proxyTable, they will be send to matching target. 76 | 77 | ``` 78 | http://localhost:3000/lorum/ipsum -> http://localhost:8000/lorum/ipsum 79 | http://integration.localhost:3000/lorum/ipsum -> http://localhost:8001/lorum/ipsum 80 | http://staging.localhost:3000/rest/foo/bar -> http://localhost:8002/rest/foo/bar 81 | http://localhost:3000/api/houses/123 -> http://localhost:8003/api/houses/123 82 | http://localhost:3000/rest/books/123 -> http://localhost:8004/rest/books/123 83 | ``` 84 | -------------------------------------------------------------------------------- /test/e2e/path-rewriter.spec.js: -------------------------------------------------------------------------------- 1 | var utils = require('./_utils') 2 | var expect = require('chai').expect 3 | var http = require('http') 4 | 5 | describe('E2E pathRewrite', function () { 6 | var createServer 7 | var proxyMiddleware 8 | 9 | beforeEach(function () { 10 | createServer = utils.createServer 11 | proxyMiddleware = utils.proxyMiddleware 12 | }) 13 | 14 | var targetMiddleware 15 | var targetData 16 | 17 | beforeEach(function () { 18 | targetData = {} 19 | targetMiddleware = function (req, res, next) { 20 | targetData.url = req.url // store target url. 21 | targetData.headers = req.headers // store target headers. 22 | res.write(req.url) // respond with target url. 23 | res.end() 24 | } 25 | }) 26 | 27 | var proxyServer 28 | var targetServer 29 | 30 | beforeEach(function () { 31 | targetServer = createServer(8000, targetMiddleware) 32 | }) 33 | 34 | afterEach(function () { 35 | proxyServer && proxyServer.close() 36 | targetServer.close() 37 | }) 38 | 39 | describe('Rewrite paths with rules table', function () { 40 | beforeEach(function () { 41 | var proxyConfig = { 42 | target: 'http://localhost:8000', 43 | pathRewrite: { 44 | '^/foobar/api/': '/api/' 45 | } 46 | } 47 | var proxy = proxyMiddleware(proxyConfig) 48 | proxyServer = createServer(3000, proxy) 49 | }) 50 | 51 | beforeEach(function (done) { 52 | http.get('http://localhost:3000/foobar/api/lorum/ipsum', function (res) { 53 | done() 54 | }) 55 | }) 56 | 57 | it('should remove "/foobar" from path', function () { 58 | expect(targetData.url).to.equal('/api/lorum/ipsum') 59 | }) 60 | }) 61 | 62 | describe('Rewrite paths with function', function () { 63 | var originalPath 64 | var pathRewriteReqObject 65 | 66 | beforeEach(function () { 67 | var proxyConfig = { 68 | target: 'http://localhost:8000', 69 | pathRewrite: function (path, req) { 70 | originalPath = path 71 | pathRewriteReqObject = req 72 | return path.replace('/foobar', '') 73 | } 74 | } 75 | var proxy = proxyMiddleware(proxyConfig) 76 | proxyServer = createServer(3000, proxy) 77 | }) 78 | 79 | beforeEach(function (done) { 80 | http.get('http://localhost:3000/foobar/api/lorum/ipsum', function (res) { 81 | done() 82 | }) 83 | }) 84 | 85 | it('should remove "/foobar" from path', function () { 86 | expect(targetData.url).to.equal('/api/lorum/ipsum') 87 | }) 88 | 89 | it('should provide the `path` parameter with the unmodified path value', function () { 90 | expect(originalPath).to.equal('/foobar/api/lorum/ipsum') 91 | }) 92 | 93 | it('should provide the `req` object as second parameter of the rewrite function', function () { 94 | expect(pathRewriteReqObject.method).to.equal('GET') 95 | expect(pathRewriteReqObject.url).to.equal('/api/lorum/ipsum') 96 | }) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /recipes/context-matching.md: -------------------------------------------------------------------------------- 1 | # Context matching 2 | 3 | Determine which requests should be proxied. 4 | 5 | Context matching is optional and is useful in cases where you are not able to use the regular [middleware mounting](http://expressjs.com/en/4x/api.html#app.use). 6 | 7 | The [RFC 3986 `path`](https://tools.ietf.org/html/rfc3986#section-3.3) is used for context matching. 8 | 9 | ``` 10 | foo://example.com:8042/over/there?name=ferret#nose 11 | \_/ \______________/\_________/ \_________/ \__/ 12 | | | | | | 13 | scheme authority path query fragment 14 | ``` 15 | 16 | 17 | `http-proxy-middleware` offers several ways to do this: 18 | 19 | 20 | 21 | - [Path](#path) 22 | - [Multi Path](#multi-path) 23 | - [Wildcard](#wildcard) 24 | - [Multi Wildcard](#multi-wildcard) 25 | - [Wildcard / Exclusion](#wildcard--exclusion) 26 | - [Custom filtering](#custom-filtering) 27 | 28 | 29 | 30 | 31 | ## Path 32 | 33 | This will match paths starting with `/api` 34 | 35 | ```javascript 36 | var proxy = require("http-proxy-middleware"); 37 | 38 | var apiProxy = proxy('/api', {target: 'http://localhost:3000'}); 39 | 40 | // `/api/foo/bar` -> `http://localhost:3000/api/foo/bar` 41 | ``` 42 | 43 | ## Multi Path 44 | 45 | This will match paths starting with `/api` or `/rest` 46 | 47 | ```javascript 48 | var proxy = require("http-proxy-middleware"); 49 | 50 | var apiProxy = proxy(['/api', '/rest'], {target: 'http://localhost:3000'}); 51 | 52 | // `/api/foo/bar` -> `http://localhost:3000/api/foo/bar` 53 | // `/rest/lorum/ipsum` -> `http://localhost:3000/rest/lorum/ipsum` 54 | ``` 55 | 56 | ## Wildcard 57 | 58 | This will match paths starting with `/api/` and should also end with `.json` 59 | 60 | ```javascript 61 | var proxy = require("http-proxy-middleware"); 62 | 63 | var apiProxy = proxy('/api/**/*.json', {target: 'http://localhost:3000'}); 64 | ``` 65 | 66 | ## Multi Wildcard 67 | 68 | Multiple wildcards can be used. 69 | 70 | ```javascript 71 | var proxy = require("http-proxy-middleware"); 72 | 73 | var apiProxy = proxy(['/api/**/*.json', '/rest/**'], {target: 'http://localhost:3000'}); 74 | ``` 75 | 76 | ## Wildcard / Exclusion 77 | 78 | This example will create a proxy with wildcard context matching. 79 | 80 | ```javascript 81 | var proxy = require("http-proxy-middleware"); 82 | 83 | var apiProxy = proxy(['foo/*.js', '!bar.js'], {target: 'http://localhost:3000'}); 84 | ``` 85 | 86 | ## Custom filtering 87 | 88 | Write your custom context matching function to have full control on the matching behavior. 89 | The request `pathname` and `req` object are provided to determine which requests should be proxied or not. 90 | 91 | ```javascript 92 | var proxy = require("http-proxy-middleware"); 93 | 94 | var filter = function (pathname, req) { 95 | return (pathname.match('^/api') && req.method === 'GET'); 96 | }; 97 | 98 | var apiProxy = proxy(filter, {target: 'http://localhost:3000'}); 99 | ``` 100 | -------------------------------------------------------------------------------- /examples/websocket/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | http-proxy-middleware - WebSocket example 6 | 19 | 20 | 21 | 22 | 23 |

WebSocket demo

24 | 25 |

26 | Proxy ws://localhost:3000 to ws://echo.websocket.org 27 |

28 | 29 |
30 |

31 | 32 | 33 | 34 | 35 |

36 |
37 |
38 |

39 | 40 | 41 | 42 |

43 |

44 | 45 | 46 |

47 |
48 | 49 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /recipes/modify-post.md: -------------------------------------------------------------------------------- 1 | ##Modify Post Parameters: 2 | The code example below illustrates how to modify POST body data prior to forwarding to the proxy target. 3 | Key to this example is the *"OnProxyReq"* event handler that creates a new POST body that can be manipulated to format the POST data as required. For example: inject new POST parameters that should only be visible server side. 4 | 5 | This example uses the *"body-parser"* module in the main app to create a req.body object with the decoded POST parameters. Side note - the code below will allow *"http-proxy-middleware"* to work with *"body-parser"*. 6 | 7 | Since this only modifys the request body stream the original POST body parameters remain in tact, so any POST data changes will not be sent back in the response to the client. 8 | 9 | ## Example: 10 | 11 | 'use strict'; 12 | 13 | var express = require('express'); 14 | var ProxyMiddleware = require('http-proxy-middleware'); 15 | var router = express.Router(); 16 | 17 | var proxy_filter = function (path, req) { 18 | return path.match('^/docs') && ( req.method === 'GET' || req.method === 'POST' ); 19 | }; 20 | 21 | var proxy_options = { 22 | target: 'http://localhost:8080', 23 | pathRewrite: { 24 | '^/docs' : '/java/rep/server1' // Host path & target path conversion 25 | }, 26 | onError(err, req, res) { 27 | res.writeHead(500, { 28 | 'Content-Type': 'text/plain' 29 | }); 30 | res.end('Something went wrong. And we are reporting a custom error message.' + err); 31 | }, 32 | onProxyReq(proxyReq, req, res) { 33 | if ( req.method == "POST" && req.body ) { 34 | // Add req.body logic here if needed.... 35 | 36 | // .... 37 | 38 | // Remove body-parser body object from the request 39 | if ( req.body ) delete req.body; 40 | 41 | // Make any needed POST parameter changes 42 | let body = new Object(); 43 | 44 | body.filename = 'reports/statistics/summary_2016.pdf'; 45 | body.routeid = 's003b012d002'; 46 | body.authid = 'bac02c1d-258a-4177-9da6-862580154960'; 47 | 48 | // URI encode JSON object 49 | body = Object.keys( body ).map(function( key ) { 50 | return encodeURIComponent( key ) + '=' + encodeURIComponent( body[ key ]) 51 | }).join('&'); 52 | 53 | // Update header 54 | proxyReq.setHeader( 'content-type', 'application/x-www-form-urlencoded' ); 55 | proxyReq.setHeader( 'content-length', body.length ); 56 | 57 | // Write out body changes to the proxyReq stream 58 | proxyReq.write( body ); 59 | proxyReq.end(); 60 | } 61 | } 62 | }; 63 | 64 | // Proxy configuration 65 | var proxy = ProxyMiddleware( proxy_filter, proxy_options ); 66 | 67 | /* GET home page. */ 68 | router.get('/', function(req, res, next) { 69 | res.render('index', { title: 'Node.js Express Proxy Test' }); 70 | }); 71 | 72 | router.all('/docs', proxy ); 73 | 74 | module.exports = router; 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love contributors! Your help is welcome to make this project better! 4 | 5 | Some simple guidelines we'd like you to follow. 6 | 7 | ## Got Questions or Problems? 8 | 9 | If you have questions about http-proxy-middle usage; Please check if your question hasn't been already answered on [Stack Overflow](http://stackoverflow.com/search?q=%22http-proxy-middleware%22) or in our [issue archive](https://github.com/chimurai/http-proxy-middleware/issues?utf8=%E2%9C%93&q=is%3Aissue+), [examples](https://github.com/chimurai/http-proxy-middleware/tree/master/examples) and [recipes](https://github.com/chimurai/http-proxy-middleware/tree/master/recipes). 10 | 11 | Since Nodejitsu's `http-proxy` is providing the actual proxy functionality; You might find your answer in their [documentation](https://github.com/nodejitsu/node-http-proxy), [issue archive](https://github.com/nodejitsu/node-http-proxy/issues?utf8=%E2%9C%93&q=is%3Aissue) or [examples](https://github.com/nodejitsu/node-http-proxy/tree/master/examples). 12 | 13 | Feel free to [ask a question](https://github.com/chimurai/http-proxy-middleware/issues) if the above resources didn't answer your question. 14 | 15 | Tips on how to ask: http://stackoverflow.com/help/how-to-ask 16 | 17 | ## Report Issues 18 | 19 | If you think you've found an issue, please submit it to the [Github issue tracker](https://github.com/chimurai/http-proxy-middleware/issues). 20 | 21 | "*[It doesn't work](https://goo.gl/GzkkTg)*" is not very useful for anyone. 22 | A good issue report should have a well described **problem description** and proxy **configuration**. A great issue report includes a **minimal example**. 23 | 24 | Properly format your code example for easier reading: [Code and Syntax Highlighting](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#code-and-syntax-highlighting). 25 | 26 | The quality of your issue report will determine how quickly and deeply we'll delve into it. Some simple pointers for reporting an issue: 27 | 28 | #### Problem Description 29 | 30 | - describe the problem 31 | - steps taken (request urls) 32 | - found 33 | - expected 34 | 35 | #### Configuration / Setup 36 | 37 | - http-proxy-middleware version 38 | - http-proxy-middleware configuration 39 | 40 | It might be useful to provide server information in which http-proxy-middleware is used and the target server information to which requests are being proxied. 41 | 42 | - server + version (express, connect, browser-sync, etc...) 43 | + server port number 44 | - target server 45 | + target server port number 46 | 47 | ### Minimal example 48 | 49 | Provide a minimal example to exclude external factors. This will greatly help identifying the issue. 50 | 51 | Tips on how to create a minimal example: http://stackoverflow.com/help/mcve 52 | 53 | ## New Feature? 54 | 55 | Request a new feature by submitting an issue into our [Github issue tracker](https://github.com/chimurai/http-proxy-middleware/issues). 56 | 57 | PRs are welcome. Please discuss it in our [Github issue tracker](https://github.com/chimurai/http-proxy-middleware/issues) before you start working on it, to avoid wasting your time and effort in case we are already (planning to) working on it. 58 | 59 | ## Documentation 60 | 61 | Feel free to send PRs to improve the documentation, [examples](https://github.com/chimurai/http-proxy-middleware/tree/master/examples) and [recipes](https://github.com/chimurai/http-proxy-middleware/tree/master/recipes). 62 | -------------------------------------------------------------------------------- /recipes/README.md: -------------------------------------------------------------------------------- 1 | # Recipes 2 | 3 | Common usages of `http-proxy-middleware`. 4 | 5 | # Configuration example 6 | 7 | Overview of `http-proxy-middleware` specific options. 8 | 9 | http-proxy-middleware uses Nodejitsu's [http-proxy](https://github.com/nodejitsu/node-http-proxy) to do the actual proxying. All of its [options](https://github.com/nodejitsu/node-http-proxy#options) are exposed via http-proxy-middleware's configuration object. 10 | 11 | 12 | ```javascript 13 | var proxy = require("http-proxy-middleware"); 14 | var winston = require('winston'); 15 | 16 | /** 17 | * Context matching: decide which path(s) should be proxied. (wildcards supported) 18 | **/ 19 | var context = '/api'; 20 | 21 | /** 22 | * Proxy options 23 | */ 24 | var options = { 25 | // hostname to the target server 26 | target: 'http://localhost:3000', 27 | 28 | // set correct host headers for name-based virtual hosted sites 29 | changeOrigin: true, 30 | 31 | // enable websocket proxying 32 | ws: true, 33 | 34 | // additional request headers 35 | headers: { 36 | 'x-powered-by': 'foobar' 37 | }, 38 | 39 | // rewrite paths 40 | pathRewrite: { 41 | '^/api/old-path' : '/api/new-path', // rewrite path 42 | '^/api/remove/path' : '/path' // remove base path 43 | }, 44 | 45 | // re-target based on the request's host header and/or path 46 | router: { 47 | // host[/path] : 48 | // /path : 49 | 'integration.localhost:8000' : 'http://localhost:8001', // host only 50 | 'staging.localhost:8000' : 'http://localhost:8002', // host only 51 | 'localhost:8000/api' : 'http://localhost:8003', // host + path 52 | '/rest' : 'http://localhost:8004' // path only 53 | }, 54 | 55 | // control logging 56 | logLevel: 'silent', 57 | 58 | // use a different lib for logging; 59 | // i.e., write logs to file or server 60 | logProvider: function (provider) { 61 | return winston; 62 | }, 63 | 64 | // subscribe to http-proxy's error event 65 | onError: function onError(err, req, res) { 66 | res.writeHead(500, {'Content-Type': 'text/plain'}); 67 | res.end('Something went wrong.'); 68 | }, 69 | 70 | // subscribe to http-proxy's proxyRes event 71 | onProxyRes: function (proxyRes, req, res) { 72 | proxyRes.headers['x-added'] = 'foobar'; 73 | delete proxyRes.headers['x-removed']; 74 | }, 75 | 76 | // subscribe to http-proxy's proxyReq event 77 | onProxyReq: function (proxyReq, req, res) { 78 | // add custom header to request 79 | proxyReq.setHeader('x-powered-by', 'foobar'); 80 | } 81 | 82 | /** 83 | * The following options are provided by Nodejitsu's http-proxy 84 | */ 85 | 86 | // target 87 | // forward 88 | // agent 89 | // ssl 90 | // ws 91 | // xfwd 92 | // secure 93 | // toProxy 94 | // prependPath 95 | // ignorePath 96 | // localAddress 97 | // changeOrigin 98 | // auth 99 | // hostRewrite 100 | // autoRewrite 101 | // protocolRewrite 102 | // headers 103 | 104 | }; 105 | 106 | /** 107 | * Create the proxy middleware, so it can be used in a server. 108 | */ 109 | var apiProxy = proxy(context, options); 110 | ``` 111 | -------------------------------------------------------------------------------- /test/e2e/router.spec.js: -------------------------------------------------------------------------------- 1 | var utils = require('./_utils') 2 | var expect = require('chai').expect 3 | var http = require('http') 4 | 5 | describe('E2E router', function () { 6 | var proxyServer, targetServerA, targetServerB, targetServerC 7 | var createServer 8 | var proxyMiddleware 9 | 10 | beforeEach(function () { 11 | createServer = utils.createServer 12 | proxyMiddleware = utils.proxyMiddleware 13 | }) 14 | 15 | beforeEach(function () { 16 | targetServerA = createServer(6001, function (req, res, next) { 17 | res.write('A') 18 | res.end() 19 | }) 20 | 21 | targetServerB = createServer(6002, function (req, res, next) { 22 | res.write('B') 23 | res.end() 24 | }) 25 | 26 | targetServerC = createServer(6003, function (req, res, next) { 27 | res.write('C') 28 | res.end() 29 | }) 30 | }) 31 | 32 | afterEach(function () { 33 | targetServerA.close() 34 | targetServerB.close() 35 | targetServerC.close() 36 | }) 37 | 38 | describe('router with proxyTable', function () { 39 | beforeEach(function () { 40 | proxyServer = createServer(6000, proxyMiddleware({ 41 | target: 'http://localhost:6001', 42 | router: function (req) { 43 | return 'http://localhost:6003' 44 | } 45 | })) 46 | }) 47 | 48 | afterEach(function () { 49 | proxyServer.close() 50 | }) 51 | 52 | it('should proxy to: "localhost:6003/api"', function (done) { 53 | var options = {hostname: 'localhost', port: 6000, path: '/api'} 54 | http.get(options, function (res) { 55 | res.on('data', function (chunk) { 56 | var responseBody = chunk.toString() 57 | expect(responseBody).to.equal('C') 58 | done() 59 | }) 60 | }) 61 | }) 62 | }) 63 | 64 | describe('router with proxyTable', function () { 65 | beforeEach(function setupServers () { 66 | proxyServer = createServer(6000, proxyMiddleware('/', { 67 | target: 'http://localhost:6001', 68 | router: { 69 | 'alpha.localhost:6000': 'http://localhost:6001', 70 | 'beta.localhost:6000': 'http://localhost:6002', 71 | 'localhost:6000/api': 'http://localhost:6003' 72 | } 73 | })) 74 | }) 75 | 76 | afterEach(function () { 77 | proxyServer.close() 78 | }) 79 | 80 | it('should proxy to option.target', function (done) { 81 | http.get('http://localhost:6000', function (res) { 82 | res.on('data', function (chunk) { 83 | var responseBody = chunk.toString() 84 | expect(responseBody).to.equal('A') 85 | done() 86 | }) 87 | }) 88 | }) 89 | 90 | it('should proxy when host is "alpha.localhost"', function (done) { 91 | var options = {hostname: 'localhost', port: 6000, path: '/'} 92 | options.headers = {host: 'alpha.localhost:6000'} 93 | http.get(options, function (res) { 94 | res.on('data', function (chunk) { 95 | var responseBody = chunk.toString() 96 | expect(responseBody).to.equal('A') 97 | done() 98 | }) 99 | }) 100 | }) 101 | 102 | it('should proxy when host is "beta.localhost"', function (done) { 103 | var options = {hostname: 'localhost', port: 6000, path: '/'} 104 | options.headers = {host: 'beta.localhost:6000'} 105 | http.get(options, function (res) { 106 | res.on('data', function (chunk) { 107 | var responseBody = chunk.toString() 108 | expect(responseBody).to.equal('B') 109 | done() 110 | }) 111 | }) 112 | }) 113 | 114 | it('should proxy with host & path config: "localhost:6000/api"', function (done) { 115 | var options = {hostname: 'localhost', port: 6000, path: '/api'} 116 | http.get(options, function (res) { 117 | res.on('data', function (chunk) { 118 | var responseBody = chunk.toString() 119 | expect(responseBody).to.equal('C') 120 | done() 121 | }) 122 | }) 123 | }) 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /lib/config-factory.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | var url = require('url') 3 | var ERRORS = require('./errors') 4 | var logger = require('./logger').getInstance() 5 | 6 | module.exports = { 7 | createConfig: createConfig 8 | } 9 | 10 | function createConfig (context, opts) { 11 | // structure of config object to be returned 12 | var config = { 13 | context: undefined, 14 | options: {} 15 | } 16 | 17 | // app.use('/api', proxy({target:'http://localhost:9000'})); 18 | if (isContextless(context, opts)) { 19 | config.context = '/' 20 | config.options = _.assign(config.options, context) 21 | 22 | // app.use('/api', proxy('http://localhost:9000')); 23 | // app.use(proxy('http://localhost:9000/api')); 24 | } else if (isStringShortHand(context)) { 25 | var oUrl = url.parse(context) 26 | var target = [oUrl.protocol, '//', oUrl.host].join('') 27 | 28 | config.context = oUrl.pathname || '/' 29 | config.options = _.assign(config.options, {target: target}, opts) 30 | 31 | if (oUrl.protocol === 'ws:' || oUrl.protocol === 'wss:') { 32 | config.options.ws = true 33 | } 34 | // app.use('/api', proxy({target:'http://localhost:9000'})); 35 | } else { 36 | config.context = context 37 | config.options = _.assign(config.options, opts) 38 | } 39 | 40 | configureLogger(config.options) 41 | 42 | if (!config.options.target) { 43 | throw new Error(ERRORS.ERR_CONFIG_FACTORY_TARGET_MISSING) 44 | } 45 | 46 | // Legacy option.proxyHost 47 | config.options = mapLegacyProxyHostOption(config.options) 48 | 49 | // Legacy option.proxyTable > option.router 50 | config.options = mapLegacyProxyTableOption(config.options) 51 | 52 | return config 53 | } 54 | 55 | /** 56 | * Checks if a String only target/config is provided. 57 | * This can be just the host or with the optional path. 58 | * 59 | * @example 60 | * app.use('/api', proxy('http://localhost:9000')); 61 | app.use(proxy('http://localhost:9000/api')); 62 | * 63 | * @param {String} context [description] 64 | * @return {Boolean} [description] 65 | */ 66 | function isStringShortHand (context) { 67 | if (_.isString(context)) { 68 | return !!(url.parse(context).host) 69 | } 70 | } 71 | 72 | /** 73 | * Checks if a Object only config is provided, without a context. 74 | * In this case the all paths will be proxied. 75 | * 76 | * @example 77 | * app.use('/api', proxy({target:'http://localhost:9000'})); 78 | * 79 | * @param {Object} context [description] 80 | * @param {*} opts [description] 81 | * @return {Boolean} [description] 82 | */ 83 | function isContextless (context, opts) { 84 | return (_.isPlainObject(context) && _.isEmpty(opts)) 85 | } 86 | 87 | function mapLegacyProxyHostOption (options) { 88 | // set options.headers.host when option.proxyHost is provided 89 | if (options.proxyHost) { 90 | logger.warn('*************************************') 91 | logger.warn('[HPM] Deprecated "option.proxyHost"') 92 | logger.warn(' Use "option.changeOrigin" or "option.headers.host" instead') 93 | logger.warn(' "option.proxyHost" will be removed in future release.') 94 | logger.warn('*************************************') 95 | 96 | options.headers = options.headers || {} 97 | options.headers.host = options.proxyHost 98 | } 99 | 100 | return options 101 | } 102 | 103 | // Warn deprecated proxyTable api usage 104 | function mapLegacyProxyTableOption (options) { 105 | if (options.proxyTable) { 106 | logger.warn('*************************************') 107 | logger.warn('[HPM] Deprecated "option.proxyTable"') 108 | logger.warn(' Use "option.router" instead') 109 | logger.warn(' "option.proxyTable" will be removed in future release.') 110 | logger.warn('*************************************') 111 | 112 | options.router = _.clone(options.proxyTable) 113 | _.omit(options, 'proxyTable') 114 | } 115 | 116 | return options 117 | } 118 | 119 | function configureLogger (options) { 120 | if (options.logLevel) { 121 | logger.setLevel(options.logLevel) 122 | } 123 | 124 | if (options.logProvider) { 125 | logger.setProvider(options.logProvider) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | var _ = require('lodash') 3 | 4 | var loggerInstance 5 | 6 | var defaultProvider = { 7 | log: console.log, 8 | debug: console.log, // use .log(); since console does not have .debug() 9 | info: console.info, 10 | warn: console.warn, 11 | error: console.error 12 | } 13 | 14 | // log level 'weight' 15 | var LEVELS = { 16 | debug: 10, 17 | info: 20, 18 | warn: 30, 19 | error: 50, 20 | silent: 80 21 | } 22 | 23 | module.exports = { 24 | // singleton 25 | getInstance: function () { 26 | if (!loggerInstance) { 27 | loggerInstance = new Logger() 28 | } 29 | 30 | return loggerInstance 31 | }, 32 | getArrow: getArrow 33 | } 34 | 35 | function Logger () { 36 | var logLevel 37 | var provider 38 | 39 | var api = { 40 | log: log, 41 | debug: debug, 42 | info: info, 43 | warn: warn, 44 | error: error, 45 | setLevel: function (v) { 46 | if (isValidLevel(v)) { 47 | logLevel = v 48 | } 49 | }, 50 | setProvider: function (fn) { 51 | if (fn && isValidProvider(fn)) { 52 | provider = fn(defaultProvider) 53 | } 54 | } 55 | } 56 | 57 | init() 58 | 59 | return api 60 | 61 | function init () { 62 | api.setLevel('info') 63 | api.setProvider(function () { 64 | return defaultProvider 65 | }) 66 | } 67 | 68 | // log will log messages, regardless of logLevels 69 | function log () { 70 | provider.log(_interpolate.apply(null, arguments)) 71 | } 72 | 73 | function debug () { 74 | if (_showLevel('debug')) { 75 | provider.debug(_interpolate.apply(null, arguments)) 76 | } 77 | } 78 | 79 | function info () { 80 | if (_showLevel('info')) { 81 | provider.info(_interpolate.apply(null, arguments)) 82 | } 83 | } 84 | 85 | function warn () { 86 | if (_showLevel('warn')) { 87 | provider.warn(_interpolate.apply(null, arguments)) 88 | } 89 | } 90 | 91 | function error () { 92 | if (_showLevel('error')) { 93 | provider.error(_interpolate.apply(null, arguments)) 94 | } 95 | } 96 | 97 | /** 98 | * Decide to log or not to log, based on the log levels 'weight' 99 | * @param {String} showLevel [debug, info, warn, error, silent] 100 | * @return {Boolean} 101 | */ 102 | function _showLevel (showLevel) { 103 | var result = false 104 | var currentLogLevel = LEVELS[logLevel] 105 | 106 | if (currentLogLevel && (currentLogLevel <= LEVELS[showLevel])) { 107 | result = true 108 | } 109 | 110 | return result 111 | } 112 | 113 | // make sure logged messages and its data are return interpolated 114 | // make it possible for additional log data, such date/time or custom prefix. 115 | function _interpolate () { 116 | var fn = _.spread(util.format) 117 | var result = fn(_.slice(arguments)) 118 | 119 | return result 120 | } 121 | 122 | function isValidProvider (fnProvider) { 123 | var result = true 124 | 125 | if (fnProvider && !_.isFunction(fnProvider)) { 126 | throw new Error('[HPM] Log provider config error. Expecting a function.') 127 | } 128 | 129 | return result 130 | } 131 | 132 | function isValidLevel (levelName) { 133 | var validLevels = _.keys(LEVELS) 134 | var isValid = _.includes(validLevels, levelName) 135 | 136 | if (!isValid) { 137 | throw new Error('[HPM] Log level error. Invalid logLevel.') 138 | } 139 | 140 | return isValid 141 | } 142 | } 143 | 144 | /** 145 | * -> normal proxy 146 | * => router 147 | * ~> pathRewrite 148 | * ≈> router + pathRewrite 149 | * 150 | * @param {String} originalPath 151 | * @param {String} newPath 152 | * @param {String} originalTarget 153 | * @param {String} newTarget 154 | * @return {String} 155 | */ 156 | function getArrow (originalPath, newPath, originalTarget, newTarget) { 157 | var arrow = ['>'] 158 | var isNewTarget = (originalTarget !== newTarget) // router 159 | var isNewPath = (originalPath !== newPath) // pathRewrite 160 | 161 | if (isNewPath && !isNewTarget) { 162 | arrow.unshift('~') 163 | } else if (!isNewPath && isNewTarget) { 164 | arrow.unshift('=') 165 | } else if (isNewPath && isNewTarget) { 166 | arrow.unshift('≈') 167 | } else { 168 | arrow.unshift('-') 169 | } 170 | 171 | return arrow.join('') 172 | } 173 | -------------------------------------------------------------------------------- /test/e2e/websocket.spec.js: -------------------------------------------------------------------------------- 1 | var utils = require('./_utils') 2 | var expect = require('chai').expect 3 | var http = require('http') 4 | var WebSocket = require('ws') 5 | var WebSocketServer = require('ws').Server 6 | 7 | describe('E2E WebSocket proxy', function () { 8 | var createServer 9 | var proxyMiddleware 10 | 11 | beforeEach(function () { 12 | createServer = utils.createServer 13 | proxyMiddleware = utils.proxyMiddleware 14 | }) 15 | 16 | var proxyServer, ws, wss 17 | var responseMessage 18 | var proxy 19 | 20 | beforeEach(function () { 21 | proxy = proxyMiddleware('/', { 22 | target: 'http://localhost:8000', 23 | ws: true, 24 | pathRewrite: {'^/socket': ''} 25 | }) 26 | 27 | proxyServer = createServer(3000, proxy) 28 | 29 | wss = new WebSocketServer({port: 8000}) 30 | 31 | wss.on('connection', function connection (ws) { 32 | ws.on('message', function incoming (message) { 33 | ws.send(message) // echo received message 34 | }) 35 | }) 36 | }) 37 | 38 | describe('option.ws', function () { 39 | beforeEach(function (done) { 40 | // need to make a normal http request, 41 | // so http-proxy-middleware can catch the upgrade request 42 | http.get('http://localhost:3000/', function () { 43 | // do a second http request to make 44 | // sure only 1 listener subscribes to upgrade request 45 | http.get('http://localhost:3000/', function () { 46 | ws = new WebSocket('ws://localhost:3000/socket') 47 | 48 | ws.on('message', function incoming (message) { 49 | responseMessage = message 50 | done() 51 | }) 52 | 53 | ws.on('open', function open () { 54 | ws.send('foobar') 55 | }) 56 | }) 57 | }) 58 | }) 59 | 60 | it('should proxy to path', function () { 61 | expect(responseMessage).to.equal('foobar') 62 | }) 63 | }) 64 | 65 | describe('option.ws with external server "upgrade"', function () { 66 | beforeEach(function (done) { 67 | proxyServer.on('upgrade', proxy.upgrade) 68 | 69 | ws = new WebSocket('ws://localhost:3000/socket') 70 | 71 | ws.on('message', function incoming (message) { 72 | responseMessage = message 73 | done() 74 | }) 75 | 76 | ws.on('open', function open () { 77 | ws.send('foobar') 78 | }) 79 | }) 80 | 81 | it('should proxy to path', function () { 82 | expect(responseMessage).to.equal('foobar') 83 | }) 84 | }) 85 | 86 | describe('option.ws with external server "upgrade" and shorthand usage', function () { 87 | beforeEach(function () { 88 | proxyServer.close() 89 | // override 90 | proxy = proxyMiddleware('ws://localhost:8000', {pathRewrite: {'^/socket': ''}}) 91 | proxyServer = createServer(3000, proxy) 92 | }) 93 | 94 | beforeEach(function (done) { 95 | proxyServer.on('upgrade', proxy.upgrade) 96 | 97 | ws = new WebSocket('ws://localhost:3000/socket') 98 | 99 | ws.on('message', function incoming (message) { 100 | responseMessage = message 101 | done() 102 | }) 103 | 104 | ws.on('open', function open () { 105 | ws.send('foobar') 106 | }) 107 | }) 108 | 109 | it('should proxy to path', function () { 110 | expect(responseMessage).to.equal('foobar') 111 | }) 112 | }) 113 | 114 | describe('with router and pathRewrite', function () { 115 | beforeEach(function () { 116 | proxyServer.close() 117 | // override 118 | proxy = proxyMiddleware('ws://notworkinghost:6789', {router: {'/socket': 'ws://localhost:8000'}, pathRewrite: {'^/socket': ''}}) 119 | proxyServer = createServer(3000, proxy) 120 | }) 121 | 122 | beforeEach(function (done) { 123 | proxyServer.on('upgrade', proxy.upgrade) 124 | 125 | ws = new WebSocket('ws://localhost:3000/socket') 126 | 127 | ws.on('message', function incoming (message) { 128 | responseMessage = message 129 | done() 130 | }) 131 | 132 | ws.on('open', function open () { 133 | ws.send('foobar') 134 | }) 135 | }) 136 | 137 | it('should proxy to path', function () { 138 | expect(responseMessage).to.equal('foobar') 139 | }) 140 | }) 141 | 142 | afterEach(function () { 143 | proxyServer.close() 144 | wss.close() 145 | ws = null 146 | }) 147 | }) 148 | -------------------------------------------------------------------------------- /test/unit/handlers.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | // https://github.com/feross/standard/issues/690#issuecomment-278533482 3 | 4 | var expect = require('chai').expect 5 | var handlers = require('./_libs').handlers 6 | 7 | describe('handlers factory', function () { 8 | var handlersMap 9 | 10 | it('should return default handlers when no handlers are provided', function () { 11 | handlersMap = handlers.getHandlers() 12 | expect(handlersMap.error).to.be.a('function') 13 | expect(handlersMap.close).to.be.a('function') 14 | }) 15 | 16 | describe('custom handlers', function () { 17 | beforeEach(function () { 18 | var fnCustom = function () { 19 | return 42 20 | } 21 | 22 | var proxyOptions = { 23 | target: 'http://www.example.org', 24 | onError: fnCustom, 25 | onOpen: fnCustom, 26 | onClose: fnCustom, 27 | onProxyReq: fnCustom, 28 | onProxyReqWs: fnCustom, 29 | onProxyRes: fnCustom, 30 | onDummy: fnCustom, 31 | foobar: fnCustom 32 | } 33 | 34 | handlersMap = handlers.getHandlers(proxyOptions) 35 | }) 36 | 37 | it('should only return http-proxy handlers', function () { 38 | expect(handlersMap.error).to.be.a('function') 39 | expect(handlersMap.open).to.be.a('function') 40 | expect(handlersMap.close).to.be.a('function') 41 | expect(handlersMap.proxyReq).to.be.a('function') 42 | expect(handlersMap.proxyReqWs).to.be.a('function') 43 | expect(handlersMap.proxyRes).to.be.a('function') 44 | expect(handlersMap.dummy).to.be.undefined 45 | expect(handlersMap.foobar).to.be.undefined 46 | expect(handlersMap.target).to.be.undefined 47 | }) 48 | 49 | it('should use the provided custom handlers', function () { 50 | expect(handlersMap.error()).to.equal(42) 51 | expect(handlersMap.open()).to.equal(42) 52 | expect(handlersMap.close()).to.equal(42) 53 | expect(handlersMap.proxyReq()).to.equal(42) 54 | expect(handlersMap.proxyReqWs()).to.equal(42) 55 | expect(handlersMap.proxyRes()).to.equal(42) 56 | }) 57 | }) 58 | }) 59 | 60 | describe('default proxy error handler', function () { 61 | var mockError = { 62 | code: 'ECONNREFUSED' 63 | } 64 | 65 | var mockReq = { 66 | headers: { 67 | host: 'localhost:3000' 68 | }, 69 | url: '/api' 70 | } 71 | 72 | var proxyOptions = { 73 | target: { 74 | host: 'localhost.dev' 75 | } 76 | } 77 | 78 | var httpErrorCode 79 | var errorMessage 80 | 81 | var mockRes = { 82 | writeHead: function (v) { 83 | httpErrorCode = v 84 | return v 85 | }, 86 | end: function (v) { 87 | errorMessage = v 88 | return v 89 | }, 90 | headersSent: false 91 | } 92 | 93 | var proxyError 94 | 95 | beforeEach(function () { 96 | var handlersMap = handlers.getHandlers() 97 | proxyError = handlersMap.error 98 | }) 99 | 100 | afterEach(function () { 101 | httpErrorCode = undefined 102 | errorMessage = undefined 103 | }) 104 | 105 | var codes = [ 106 | ['HPE_INVALID_FOO', 502], 107 | ['HPE_INVALID_BAR', 502], 108 | ['ECONNREFUSED', 504], 109 | ['ENOTFOUND', 504], 110 | ['ECONNREFUSED', 504], 111 | ['any', 500] 112 | ] 113 | codes.forEach(function (item) { 114 | var msg = item[0] 115 | var code = item[1] 116 | it('should set the http status code for ' + msg + ' to: ' + code, function () { 117 | proxyError({ code: msg }, mockReq, mockRes, proxyOptions) 118 | expect(httpErrorCode).to.equal(code) 119 | }) 120 | }) 121 | 122 | it('should end the response and return error message', function () { 123 | proxyError(mockError, mockReq, mockRes, proxyOptions) 124 | expect(errorMessage).to.equal('Error occured while trying to proxy to: localhost:3000/api') 125 | }) 126 | 127 | it('should not set the http status code to: 500 if headers have already been sent', function () { 128 | mockRes.headersSent = true 129 | proxyError(mockError, mockReq, mockRes, proxyOptions) 130 | expect(httpErrorCode).to.equal(undefined) 131 | }) 132 | 133 | it('should end the response and return error message', function () { 134 | mockRes.headersSent = true 135 | proxyError(mockError, mockReq, mockRes, proxyOptions) 136 | expect(errorMessage).to.equal('Error occured while trying to proxy to: localhost:3000/api') 137 | }) 138 | }) 139 | -------------------------------------------------------------------------------- /test/unit/path-rewriter.spec.js: -------------------------------------------------------------------------------- 1 | 2 | var expect = require('chai').expect 3 | var pathRewriter = require('./_libs').pathRewriter 4 | 5 | describe('Path rewriting', function () { 6 | var rewriter 7 | var result 8 | var config 9 | 10 | describe('Rewrite rules configuration and usage', function () { 11 | beforeEach(function () { 12 | config = { 13 | '^/api/old': '/api/new', 14 | '^/remove': '', 15 | 'invalid': 'path/new', 16 | '/valid': '/path/new', 17 | '/some/specific/path': '/awe/some/specific/path', 18 | '/some': '/awe/some' 19 | } 20 | }) 21 | 22 | beforeEach(function () { 23 | rewriter = pathRewriter.create(config) 24 | }) 25 | 26 | it('should rewrite path', function () { 27 | result = rewriter('/api/old/index.json') 28 | expect(result).to.equal('/api/new/index.json') 29 | }) 30 | 31 | it('should remove path', function () { 32 | result = rewriter('/remove/old/index.json') 33 | expect(result).to.equal('/old/index.json') 34 | }) 35 | 36 | it('should leave path intact', function () { 37 | result = rewriter('/foo/bar/index.json') 38 | expect(result).to.equal('/foo/bar/index.json') 39 | }) 40 | 41 | it('should not rewrite path when config-key does not match url with test(regex)', function () { 42 | result = rewriter('/invalid/bar/foo.json') 43 | expect(result).to.equal('/path/new/bar/foo.json') 44 | expect(result).to.not.equal('/invalid/new/bar/foo.json') 45 | }) 46 | 47 | it('should rewrite path when config-key does match url with test(regex)', function () { 48 | result = rewriter('/valid/foo/bar.json') 49 | expect(result).to.equal('/path/new/foo/bar.json') 50 | }) 51 | 52 | it('should return first match when similar paths are configured', function () { 53 | result = rewriter('/some/specific/path/bar.json') 54 | expect(result).to.equal('/awe/some/specific/path/bar.json') 55 | }) 56 | }) 57 | 58 | describe('Rewrite rule: add base path to requests', function () { 59 | beforeEach(function () { 60 | config = { 61 | '^/': '/extra/base/path/' 62 | } 63 | }) 64 | 65 | beforeEach(function () { 66 | rewriter = pathRewriter.create(config) 67 | }) 68 | 69 | it('should add base path to requests', function () { 70 | result = rewriter('/api/books/123') 71 | expect(result).to.equal('/extra/base/path/api/books/123') 72 | }) 73 | }) 74 | 75 | describe('Rewrite function', function () { 76 | var rewriter 77 | 78 | beforeEach(function () { 79 | rewriter = function (fn) { 80 | var rewriteFn = pathRewriter.create(fn) 81 | var requestPath = '/123/456' 82 | return rewriteFn(requestPath) 83 | } 84 | }) 85 | 86 | it('should return unmodified path', function () { 87 | var rewriteFn = function (path) { 88 | return path 89 | } 90 | 91 | expect(rewriter(rewriteFn)).to.equal('/123/456') 92 | }) 93 | 94 | it('should return alternative path', function () { 95 | var rewriteFn = function (path) { 96 | return '/foo/bar' 97 | } 98 | 99 | expect(rewriter(rewriteFn)).to.equal('/foo/bar') 100 | }) 101 | 102 | it('should return replaced path', function () { 103 | var rewriteFn = function (path) { 104 | return path.replace('/456', '/789') 105 | } 106 | 107 | expect(rewriter(rewriteFn)).to.equal('/123/789') 108 | }) 109 | }) 110 | 111 | describe('Invalid configuration', function () { 112 | var badFn 113 | 114 | beforeEach(function () { 115 | badFn = function (config) { 116 | return function () { 117 | pathRewriter.create(config) 118 | } 119 | } 120 | }) 121 | 122 | it('should return undefined when no config is provided', function () { 123 | expect((badFn())()).to.equal(undefined) 124 | expect((badFn(null)())).to.equal(undefined) 125 | expect((badFn(undefined)())).to.equal(undefined) 126 | }) 127 | 128 | it('should throw when bad config is provided', function () { 129 | expect(badFn(123)).to.throw(Error) 130 | expect(badFn('abc')).to.throw(Error) 131 | expect(badFn([])).to.throw(Error) 132 | expect(badFn([1, 2, 3])).to.throw(Error) 133 | }) 134 | 135 | it('should not throw when empty Object config is provided', function () { 136 | expect(badFn({})).to.not.throw(Error) 137 | }) 138 | 139 | it('should not throw when function config is provided', function () { 140 | expect(badFn(function () {})).to.not.throw(Error) 141 | }) 142 | }) 143 | }) 144 | -------------------------------------------------------------------------------- /test/unit/router.spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | var router = require('./_libs').router 3 | 4 | describe('router unit test', function () { 5 | var req, config, result, proxyOptionWithRouter 6 | 7 | beforeEach(function () { 8 | req = { 9 | headers: { 10 | host: 'localhost' 11 | }, 12 | url: '/' 13 | } 14 | 15 | config = { 16 | target: 'http://localhost:6000' 17 | } 18 | }) 19 | 20 | describe('router.getTarget from function', function () { 21 | var request 22 | 23 | beforeEach(function () { 24 | proxyOptionWithRouter = { 25 | target: 'http://localhost:6000', 26 | router: function (req) { 27 | request = req 28 | return 'http://foobar.com:666' 29 | } 30 | } 31 | 32 | result = router.getTarget(req, proxyOptionWithRouter) 33 | }) 34 | 35 | describe('custom dynamic router function', function () { 36 | it('should provide the request object for dynamic routing', function () { 37 | expect(request.headers.host).to.equal('localhost') 38 | expect(request.url).to.equal('/') 39 | }) 40 | it('should return new target', function () { 41 | expect(result).to.equal('http://foobar.com:666') 42 | }) 43 | }) 44 | }) 45 | 46 | describe('router.getTarget from table', function () { 47 | beforeEach(function () { 48 | proxyOptionWithRouter = { 49 | target: 'http://localhost:6000', 50 | router: { 51 | 'alpha.localhost': 'http://localhost:6001', 52 | 'beta.localhost': 'http://localhost:6002', 53 | 'gamma.localhost/api': 'http://localhost:6003', 54 | 'gamma.localhost': 'http://localhost:6004', 55 | '/rest': 'http://localhost:6005', 56 | '/some/specific/path': 'http://localhost:6006', 57 | '/some': 'http://localhost:6007' 58 | } 59 | } 60 | }) 61 | 62 | describe('without router config', function () { 63 | it('should return the normal target when router not present in config', function () { 64 | result = router.getTarget(req, config) 65 | expect(result).to.equal(undefined) 66 | }) 67 | }) 68 | 69 | describe('with just the host in router config', function () { 70 | it('should target http://localhost:6001 when for router:"alpha.localhost"', function () { 71 | req.headers.host = 'alpha.localhost' 72 | result = router.getTarget(req, proxyOptionWithRouter) 73 | expect(result).to.equal('http://localhost:6001') 74 | }) 75 | 76 | it('should target http://localhost:6002 when for router:"beta.localhost"', function () { 77 | req.headers.host = 'beta.localhost' 78 | result = router.getTarget(req, proxyOptionWithRouter) 79 | expect(result).to.equal('http://localhost:6002') 80 | }) 81 | }) 82 | 83 | describe('with host and host + path config', function () { 84 | it('should target http://localhost:6004 without path', function () { 85 | req.headers.host = 'gamma.localhost' 86 | result = router.getTarget(req, proxyOptionWithRouter) 87 | expect(result).to.equal('http://localhost:6004') 88 | }) 89 | 90 | it('should target http://localhost:6003 exact path match', function () { 91 | req.headers.host = 'gamma.localhost' 92 | req.url = '/api' 93 | result = router.getTarget(req, proxyOptionWithRouter) 94 | expect(result).to.equal('http://localhost:6003') 95 | }) 96 | 97 | it('should target http://localhost:6004 when contains path', function () { 98 | req.headers.host = 'gamma.localhost' 99 | req.url = '/api/books/123' 100 | result = router.getTarget(req, proxyOptionWithRouter) 101 | expect(result).to.equal('http://localhost:6003') 102 | }) 103 | }) 104 | 105 | describe('with just the path', function () { 106 | it('should target http://localhost:6005 with just a path as router config', function () { 107 | req.url = '/rest' 108 | result = router.getTarget(req, proxyOptionWithRouter) 109 | expect(result).to.equal('http://localhost:6005') 110 | }) 111 | 112 | it('should target http://localhost:6005 with just a path as router config', function () { 113 | req.url = '/rest/deep/path' 114 | result = router.getTarget(req, proxyOptionWithRouter) 115 | expect(result).to.equal('http://localhost:6005') 116 | }) 117 | 118 | it('should target http://localhost:6000 path in not present in router config', function () { 119 | req.url = '/unknow-path' 120 | result = router.getTarget(req, proxyOptionWithRouter) 121 | expect(result).to.equal(undefined) 122 | }) 123 | }) 124 | 125 | describe('matching order of router config', function () { 126 | it('should return first matching target when similar paths are configured', function () { 127 | req.url = '/some/specific/path' 128 | result = router.getTarget(req, proxyOptionWithRouter) 129 | expect(result).to.equal('http://localhost:6006') 130 | }) 131 | }) 132 | }) 133 | }) 134 | -------------------------------------------------------------------------------- /test/unit/config-factory.spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | var configFactory = require('./_libs').configFactory 3 | 4 | describe('configFactory', function () { 5 | var result 6 | var createConfig = configFactory.createConfig 7 | 8 | describe('createConfig()', function () { 9 | describe('classic config', function () { 10 | var context = '/api' 11 | var options = {target: 'http://www.example.org'} 12 | 13 | beforeEach(function () { 14 | result = createConfig(context, options) 15 | }) 16 | 17 | it('should return config object', function () { 18 | expect(result).to.have.all.keys('context', 'options') 19 | }) 20 | 21 | it('should return config object with context', function () { 22 | expect(result.context).to.equal(context) 23 | }) 24 | 25 | it('should return config object with options', function () { 26 | expect(result.options).to.deep.equal(options) 27 | }) 28 | }) 29 | 30 | describe('shorthand String', function () { 31 | describe('shorthand String config', function () { 32 | beforeEach(function () { 33 | result = createConfig('http://www.example.org:8000/api') 34 | }) 35 | 36 | it('should return config object', function () { 37 | expect(result).to.have.all.keys('context', 'options') 38 | }) 39 | 40 | it('should return config object with context', function () { 41 | expect(result.context).to.equal('/api') 42 | }) 43 | 44 | it('should return config object with options', function () { 45 | expect(result.options).to.deep.equal({target: 'http://www.example.org:8000'}) 46 | }) 47 | }) 48 | 49 | describe('shorthand String config for whole domain', function () { 50 | beforeEach(function () { 51 | result = createConfig('http://www.example.org:8000') 52 | }) 53 | 54 | it('should return config object with context', function () { 55 | expect(result.context).to.equal('/') 56 | }) 57 | }) 58 | 59 | describe('shorthand String config for websocket url', function () { 60 | beforeEach(function () { 61 | result = createConfig('ws://www.example.org:8000') 62 | }) 63 | 64 | it('should return config object with context', function () { 65 | expect(result.context).to.equal('/') 66 | }) 67 | 68 | it('should return options with ws = true', function () { 69 | expect(result.options.ws).to.equal(true) 70 | }) 71 | }) 72 | 73 | describe('shorthand String config for secure websocket url', function () { 74 | beforeEach(function () { 75 | result = createConfig('wss://www.example.org:8000') 76 | }) 77 | 78 | it('should return config object with context', function () { 79 | expect(result.context).to.equal('/') 80 | }) 81 | 82 | it('should return options with ws = true', function () { 83 | expect(result.options.ws).to.equal(true) 84 | }) 85 | }) 86 | 87 | describe('shorthand String config with globbing', function () { 88 | beforeEach(function () { 89 | result = createConfig('http://www.example.org:8000/api/*.json') 90 | }) 91 | 92 | it('should return config object with context', function () { 93 | expect(result.context).to.equal('/api/*.json') 94 | }) 95 | }) 96 | 97 | describe('shorthand String config with options', function () { 98 | beforeEach(function () { 99 | result = createConfig('http://www.example.org:8000/api', {changeOrigin: true}) 100 | }) 101 | 102 | it('should return config object with additional options', function () { 103 | expect(result.options).to.deep.equal({target: 'http://www.example.org:8000', changeOrigin: true}) 104 | }) 105 | }) 106 | }) 107 | 108 | describe('shorthand Object config', function () { 109 | beforeEach(function () { 110 | result = createConfig({target: 'http://www.example.org:8000'}) 111 | }) 112 | 113 | it('should set the proxy path to everything', function () { 114 | expect(result.context).to.equal('/') 115 | }) 116 | 117 | it('should return config object', function () { 118 | expect(result.options).to.deep.equal({target: 'http://www.example.org:8000'}) 119 | }) 120 | }) 121 | 122 | describe('missing option.target', function () { 123 | var fn 124 | 125 | beforeEach(function () { 126 | fn = function () { 127 | createConfig('/api') 128 | } 129 | }) 130 | 131 | it('should throw an error when target option is missing', function () { 132 | expect(fn).to.throw(Error) 133 | }) 134 | }) 135 | 136 | describe('faulty config. mixing classic with shorthand', function () { 137 | beforeEach(function () { 138 | result = createConfig('http://localhost:3000/api', {target: 'http://localhost:8000'}) 139 | }) 140 | 141 | it('should use the target in the configuration as target', function () { 142 | expect(result.options.target).to.equal('http://localhost:8000') 143 | }) 144 | 145 | it('should not use the host from the shorthand as target', function () { 146 | expect(result.options.target).not.to.equal('http://localhost:3000') 147 | }) 148 | }) 149 | }) 150 | }) 151 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.17.4](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.17.4) 4 | - fix(ntlm authentication): fixed bug preventing proxying with ntlm authentication. ([#132](https://github.com/chimurai/http-proxy-middleware/pull/149)) (Thanks: [EladBezalel](https://github.com/EladBezalel), [oshri551](https://github.com/oshri551)) 5 | 6 | 7 | ## [v0.17.3](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.17.3) 8 | - fix(onError): improve default proxy error handling. http status codes (504, 502 and 500). ([#132](https://github.com/chimurai/http-proxy-middleware/pull/132)) ([graingert](https://github.com/graingert)) 9 | 10 | ## [v0.17.2](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.17.2) 11 | - feat(logging): improve error message & add link to Node errors page. ([#106](https://github.com/chimurai/http-proxy-middleware/pull/106)) ([cloudmu](https://github.com/cloudmu)) 12 | - feat(pathRewrite): path can be empty string. ([#110](https://github.com/chimurai/http-proxy-middleware/pull/110)) ([sunnylqm](https://github.com/sunnylqm)) 13 | - bug(websocket): memory leak when option 'ws:true' is used. ([#114](https://github.com/chimurai/http-proxy-middleware/pull/114)) ([julbra](https://github.com/julbra)) 14 | - chore(package.json): reduce package size. ([#109](https://github.com/chimurai/http-proxy-middleware/pull/109)) 15 | 16 | ## [v0.17.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.17.1) 17 | - fix(Express sub Router): 404 on non-proxy routes ([#94](https://github.com/chimurai/http-proxy-middleware/issues/94)) 18 | 19 | ## [v0.17.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.17.0) 20 | - fix(context matching): Use [RFC 3986 path](https://tools.ietf.org/html/rfc3986#section-3.3) in context matching. (excludes query parameters) 21 | 22 | ## [v0.16.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.16.0) 23 | - deprecated(proxyTable): renamed `proxyTable` to `router`. 24 | - feat(router): support for custom `router` function. 25 | 26 | ## [v0.15.2](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.15.2) 27 | - fix(websocket): fixes websocket upgrade. 28 | 29 | ## [v0.15.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.15.1) 30 | - feat(pathRewrite): expose `req` object to pathRewrite function. 31 | - fix(websocket): fixes websocket upgrade when both config.ws and external .upgrade() are used. 32 | 33 | ## [v0.15.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.15.0) 34 | - feat(pathRewrite): support for custom pathRewrite function. 35 | 36 | ## [v0.14.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.14.0) 37 | - feat(proxy): support proxy creation without context. 38 | - fix(connect mounting): use connect's `path` configuration to mount proxy. 39 | 40 | ## [v0.13.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.13.0) 41 | - feat(context): custom context matcher; when simple `path` matching is not sufficient. 42 | 43 | ## [v0.12.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.12.0) 44 | - add option `onProxyReqWs` (subscribe to http-proxy `proxyReqWs` event) 45 | - add option `onOpen` (subscribe to http-proxy `open` event) 46 | - add option `onClose` (subscribe to http-proxy `close` event) 47 | 48 | ## [v0.11.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.11.0) 49 | - improved logging 50 | 51 | ## [v0.10.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.10.0) 52 | - feat(proxyTable) - added proxyTable support for WebSockets. 53 | - fixed(proxyTable) - ensure original path (not rewritten path) is being used when `proxyTable` is used in conjunction with `pathRewrite`. 54 | 55 | ## [v0.9.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.9.1) 56 | - fix server crash when socket error not handled correctly. 57 | 58 | ## [v0.9.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.9.0) 59 | - support subscribing to http-proxy `proxyReq` event ([trbngr](https://github.com/trbngr)) 60 | - add `logLevel` and `logProvider` support 61 | 62 | ## [v0.8.2](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.8.2) 63 | - fix proxyError handler ([mTazelaar](https://github.com/mTazelaar)) 64 | 65 | ## [v0.8.1](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.8.1) 66 | - fix pathRewrite when `agent` is configured 67 | 68 | ## [v0.8.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.8.0) 69 | - support external websocket upgrade 70 | - fix websocket shorthand 71 | 72 | ## [v0.7.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.7.0) 73 | - support shorthand syntax 74 | - fix express/connect mounting 75 | 76 | ## [v0.6.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.6.0) 77 | - support proxyTable 78 | 79 | ## [v0.5.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.5.0) 80 | - support subscribing to http-proxy `error` event 81 | - support subscribing to http-proxy `proxyRes` event 82 | 83 | ## [v0.4.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.4.0) 84 | - support websocket 85 | 86 | ## [v0.3.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.3.0) 87 | - support wildcard / glob 88 | 89 | ## [v0.2.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.2.0) 90 | - support multiple paths 91 | 92 | ## [v0.1.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.1.0) 93 | - support path rewrite 94 | - deprecate proxyHost option 95 | 96 | ## [v0.0.5](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.0.5) 97 | - initial release 98 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | var httpProxy = require('http-proxy') 3 | var configFactory = require('./config-factory') 4 | var handlers = require('./handlers') 5 | var contextMatcher = require('./context-matcher') 6 | var PathRewriter = require('./path-rewriter') 7 | var Router = require('./router') 8 | var logger = require('./logger').getInstance() 9 | var getArrow = require('./logger').getArrow 10 | 11 | module.exports = HttpProxyMiddleware 12 | 13 | function HttpProxyMiddleware (context, opts) { 14 | // https://github.com/chimurai/http-proxy-middleware/issues/57 15 | var wsUpgradeDebounced = _.debounce(handleUpgrade) 16 | var wsInitialized = false 17 | var config = configFactory.createConfig(context, opts) 18 | var proxyOptions = config.options 19 | 20 | // create proxy 21 | var proxy = httpProxy.createProxyServer({}) 22 | logger.info('[HPM] Proxy created:', config.context, ' -> ', proxyOptions.target) 23 | 24 | var pathRewriter = PathRewriter.create(proxyOptions.pathRewrite) // returns undefined when "pathRewrite" is not provided 25 | 26 | // attach handler to http-proxy events 27 | handlers.init(proxy, proxyOptions) 28 | 29 | // log errors for debug purpose 30 | proxy.on('error', logError) 31 | 32 | // https://github.com/chimurai/http-proxy-middleware/issues/19 33 | // expose function to upgrade externally 34 | middleware.upgrade = wsUpgradeDebounced 35 | 36 | return middleware 37 | 38 | function middleware (req, res, next) { 39 | if (shouldProxy(config.context, req)) { 40 | var activeProxyOptions = prepareProxyRequest(req) 41 | proxy.web(req, res, activeProxyOptions) 42 | } else { 43 | next() 44 | } 45 | 46 | if (proxyOptions.ws === true) { 47 | // use initial request to access the server object to subscribe to http upgrade event 48 | catchUpgradeRequest(req.connection.server) 49 | } 50 | } 51 | 52 | function catchUpgradeRequest (server) { 53 | // subscribe once; don't subscribe on every request... 54 | // https://github.com/chimurai/http-proxy-middleware/issues/113 55 | if (!wsInitialized) { 56 | server.on('upgrade', wsUpgradeDebounced) 57 | wsInitialized = true 58 | } 59 | } 60 | 61 | function handleUpgrade (req, socket, head) { 62 | // set to initialized when used externally 63 | wsInitialized = true 64 | 65 | if (shouldProxy(config.context, req)) { 66 | var activeProxyOptions = prepareProxyRequest(req) 67 | proxy.ws(req, socket, head, activeProxyOptions) 68 | logger.info('[HPM] Upgrading to WebSocket') 69 | } 70 | } 71 | 72 | /** 73 | * Determine whether request should be proxied. 74 | * 75 | * @private 76 | * @param {String} context [description] 77 | * @param {Object} req [description] 78 | * @return {Boolean} 79 | */ 80 | function shouldProxy (context, req) { 81 | var path = (req.originalUrl || req.url) 82 | return contextMatcher.match(context, path, req) 83 | } 84 | 85 | /** 86 | * Apply option.router and option.pathRewrite 87 | * Order matters: 88 | * Router uses original path for routing; 89 | * NOT the modified path, after it has been rewritten by pathRewrite 90 | * @param {Object} req 91 | * @return {Object} proxy options 92 | */ 93 | function prepareProxyRequest (req) { 94 | // https://github.com/chimurai/http-proxy-middleware/issues/17 95 | // https://github.com/chimurai/http-proxy-middleware/issues/94 96 | req.url = (req.originalUrl || req.url) 97 | 98 | // store uri before it gets rewritten for logging 99 | var originalPath = req.url 100 | var newProxyOptions = _.assign({}, proxyOptions) 101 | 102 | // Apply in order: 103 | // 1. option.router 104 | // 2. option.pathRewrite 105 | __applyRouter(req, newProxyOptions) 106 | __applyPathRewrite(req, pathRewriter) 107 | 108 | // debug logging for both http(s) and websockets 109 | if (proxyOptions.logLevel === 'debug') { 110 | var arrow = getArrow(originalPath, req.url, proxyOptions.target, newProxyOptions.target) 111 | logger.debug('[HPM] %s %s %s %s', req.method, originalPath, arrow, newProxyOptions.target) 112 | } 113 | 114 | return newProxyOptions 115 | } 116 | 117 | // Modify option.target when router present. 118 | function __applyRouter (req, options) { 119 | var newTarget 120 | 121 | if (options.router) { 122 | newTarget = Router.getTarget(req, options) 123 | 124 | if (newTarget) { 125 | logger.debug('[HPM] Router new target: %s -> "%s"', options.target, newTarget) 126 | options.target = newTarget 127 | } 128 | } 129 | } 130 | 131 | // rewrite path 132 | function __applyPathRewrite (req, pathRewriter) { 133 | if (pathRewriter) { 134 | var path = pathRewriter(req.url, req) 135 | 136 | if (typeof path === 'string') { 137 | req.url = path 138 | } else { 139 | logger.info('[HPM] pathRewrite: No rewritten path found. (%s)', req.url) 140 | } 141 | } 142 | } 143 | 144 | function logError (err, req, res) { 145 | var hostname = (req.headers && req.headers.host) || (req.hostname || req.host) // (websocket) || (node0.10 || node 4/5) 146 | var target = proxyOptions.target.host || proxyOptions.target 147 | var errorMessage = '[HPM] Error occurred while trying to proxy request %s from %s to %s (%s) (%s)' 148 | var errReference = 'https://nodejs.org/api/errors.html#errors_common_system_errors' // link to Node Common Systems Errors page 149 | 150 | logger.error(errorMessage, req.url, hostname, target, err.code, errReference) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /recipes/servers.md: -------------------------------------------------------------------------------- 1 | # Servers 2 | 3 | Overview of `http-proxy-middleware` implementation in different servers. 4 | 5 | Missing a server? Feel free to extend this list of examples. 6 | 7 | 8 | 9 | - [Browser-Sync](#browser-sync) 10 | - [Express](#express) 11 | - [Connect](#connect) 12 | - [lite-server](#lite-server) 13 | - [grunt-contrib-connect](#grunt-contrib-connect) 14 | - [grunt-browser-sync](#grunt-browser-sync) 15 | - [gulp-connect](#gulp-connect) 16 | - [gulp-webserver](#gulp-webserver) 17 | 18 | 19 | 20 | ## Browser-Sync 21 | 22 | https://github.com/BrowserSync/browser-sync 23 | [![GitHub stars](https://img.shields.io/github/stars/BrowserSync/browser-sync.svg?style=social&label=Star)](https://github.com/BrowserSync/browser-sync) 24 | 25 | ```javascript 26 | var browserSync = require('browser-sync').create(); 27 | var proxy = require('http-proxy-middleware'); 28 | 29 | var apiProxy = proxy('/api', { 30 | target: 'http://www.example.org', 31 | changeOrigin: true // for vhosted sites 32 | }); 33 | 34 | browserSync.init({ 35 | server: { 36 | baseDir: './', 37 | port: 3000, 38 | middleware: [apiProxy], 39 | }, 40 | startPath: '/api' 41 | }); 42 | 43 | ``` 44 | 45 | ## Express 46 | 47 | https://github.com/expressjs/express 48 | [![GitHub stars](https://img.shields.io/github/stars/expressjs/express.svg?style=social&label=Star)](https://github.com/expressjs/express) 49 | 50 | ```javascript 51 | var express = require('express'); 52 | var proxy = require('http-proxy-middleware'); 53 | 54 | var apiProxy = proxy('/api', { 55 | target: 'http://www.example.org', 56 | changeOrigin: true // for vhosted sites 57 | }); 58 | 59 | var app = express(); 60 | 61 | app.use(apiProxy); 62 | app.listen(3000); 63 | ``` 64 | 65 | ## Connect 66 | 67 | https://github.com/senchalabs/connect 68 | [![GitHub stars](https://img.shields.io/github/stars/senchalabs/connect.svg?style=social&label=Star)](https://github.com/senchalabs/connect) 69 | 70 | ```javascript 71 | var http = require('http'); 72 | var connect = require('connect'); 73 | var proxy = require('http-proxy-middleware'); 74 | 75 | var apiProxy = proxy('/api', { 76 | target: 'http://www.example.org', 77 | changeOrigin: true // for vhosted sites 78 | }); 79 | 80 | var app = connect(); 81 | app.use(apiProxy); 82 | 83 | http.createServer(app).listen(3000); 84 | ``` 85 | 86 | ## lite-server 87 | 88 | https://github.com/johnpapa/lite-server 89 | [![GitHub stars](https://img.shields.io/github/stars/johnpapa/lite-server.svg?style=social&label=Star)](https://github.com/johnpapa/lite-server) 90 | 91 | File: `bs-config.js` 92 | 93 | ```javascript 94 | var proxy = require('http-proxy-middleware'); 95 | 96 | var apiProxy = proxy('/api', { 97 | target: 'http://www.example.org', 98 | changeOrigin: true // for vhosted sites 99 | }); 100 | 101 | module.exports = { 102 | server: { 103 | // Start from key `10` in order to NOT overwrite the default 2 middleware provided 104 | // by `lite-server` or any future ones that might be added. 105 | // Reference: https://github.com/johnpapa/lite-server/blob/master/lib/config-defaults.js#L16 106 | middleware: { 107 | 10: apiProxy 108 | } 109 | } 110 | }; 111 | ``` 112 | 113 | ## grunt-contrib-connect 114 | 115 | https://github.com/gruntjs/grunt-contrib-connect 116 | [![GitHub stars](https://img.shields.io/github/stars/gruntjs/grunt-contrib-connect.svg?style=social&label=Star)](https://github.com/gruntjs/grunt-contrib-connect) 117 | 118 | As an `Array`: 119 | ```javascript 120 | var proxy = require('http-proxy-middleware'); 121 | 122 | var apiProxy = proxy('/api', { 123 | target: 'http://www.example.org', 124 | changeOrigin: true // for vhosted sites 125 | }); 126 | 127 | grunt.initConfig({ 128 | connect: { 129 | server: { 130 | options: { 131 | middleware: [apiProxy], 132 | }, 133 | }, 134 | }, 135 | }); 136 | ``` 137 | 138 | As a `function`: 139 | ```javascript 140 | var proxy = require('http-proxy-middleware'); 141 | 142 | var apiProxy = proxy('/api', { 143 | target: 'http://www.example.org', 144 | changeOrigin: true // for vhosted sites 145 | }); 146 | 147 | grunt.initConfig({ 148 | connect: { 149 | server: { 150 | options: { 151 | middleware: function(connect, options, middlewares) { 152 | // inject a custom middleware into the array of default middlewares 153 | middlewares.unshift(apiProxy); 154 | 155 | return middlewares; 156 | }, 157 | }, 158 | }, 159 | }, 160 | }); 161 | ``` 162 | 163 | 164 | ## grunt-browser-sync 165 | 166 | https://github.com/BrowserSync/grunt-browser-sync 167 | [![GitHub stars](https://img.shields.io/github/stars/BrowserSync/grunt-browser-sync.svg?style=social&label=Star)](https://github.com/BrowserSync/grunt-browser-sync) 168 | 169 | 170 | ```javascript 171 | var proxy = require('http-proxy-middleware'); 172 | 173 | var apiProxy = proxy('/api', { 174 | target: 'http://www.example.org', 175 | changeOrigin: true // for vhosted sites 176 | }); 177 | 178 | grunt.initConfig({ 179 | 180 | // BrowserSync Task 181 | browserSync: { 182 | default_options: { 183 | options: { 184 | files: [ 185 | "css/*.css", 186 | "*.html" 187 | ], 188 | port: 9000, 189 | server: { 190 | baseDir: ['app'], 191 | middleware: apiProxy 192 | } 193 | } 194 | } 195 | } 196 | 197 | }); 198 | ``` 199 | 200 | ## gulp-connect 201 | 202 | https://github.com/avevlad/gulp-connect 203 | [![GitHub stars](https://img.shields.io/github/stars/avevlad/gulp-connect.svg?style=social&label=Star)](https://github.com/avevlad/gulp-connect) 204 | 205 | ```javascript 206 | var gulp = require('gulp'); 207 | var connect = require('gulp-connect'); 208 | var proxy = require('http-proxy-middleware'); 209 | 210 | gulp.task('connect', function() { 211 | connect.server({ 212 | root: ['./app'], 213 | middleware: function(connect, opt) { 214 | 215 | var apiProxy = proxy('/api', { 216 | target: 'http://www.example.org', 217 | changeOrigin: true // for vhosted sites 218 | }); 219 | 220 | return [apiProxy]; 221 | } 222 | 223 | }); 224 | }); 225 | 226 | gulp.task('default', ['connect']); 227 | ``` 228 | 229 | ## gulp-webserver 230 | 231 | https://github.com/schickling/gulp-webserver 232 | [![GitHub stars](https://img.shields.io/github/stars/schickling/gulp-webserver.svg?style=social&label=Star)](https://github.com/schickling/gulp-webserver) 233 | 234 | ```javascript 235 | var gulp = require('gulp'); 236 | var webserver = require('gulp-webserver'); 237 | var proxy = require('http-proxy-middleware'); 238 | 239 | gulp.task('webserver', function() { 240 | var apiProxy = proxy('/api', { 241 | target: 'http://www.example.org', 242 | changeOrigin: true // for vhosted sites 243 | }); 244 | 245 | gulp.src('app') 246 | .pipe(webserver({ 247 | livereload: true, 248 | directoryListing: true, 249 | open: true, 250 | middleware: [apiProxy] 251 | })); 252 | }); 253 | 254 | gulp.task('default', ['webserver']); 255 | ``` 256 | -------------------------------------------------------------------------------- /test/unit/logger.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | // https://github.com/feross/standard/issues/690#issuecomment-278533482 3 | 4 | var expect = require('chai').expect 5 | var Logger = require('./_libs').Logger 6 | var getArrow = Logger.getArrow 7 | 8 | describe('Logger', function () { 9 | var logger 10 | var logMessage, debugMessage, infoMessage, warnMessage, errorMessage 11 | 12 | beforeEach(function () { 13 | logMessage = undefined 14 | debugMessage = undefined 15 | infoMessage = undefined 16 | warnMessage = undefined 17 | errorMessage = undefined 18 | }) 19 | 20 | beforeEach(function () { 21 | logger = Logger.getInstance() 22 | }) 23 | 24 | beforeEach(function () { 25 | logger.setProvider(function (provider) { 26 | provider.log = function (message) { logMessage = message } 27 | provider.debug = function (message) { debugMessage = message } 28 | provider.info = function (message) { infoMessage = message } 29 | provider.warn = function (message) { warnMessage = message } 30 | provider.error = function (message) { errorMessage = message } 31 | 32 | return provider 33 | }) 34 | }) 35 | 36 | describe('logging with different levels', function () { 37 | beforeEach(function () { 38 | logger.log('log') 39 | logger.debug('debug') 40 | logger.info('info') 41 | logger.warn('warn') 42 | logger.error('error') 43 | }) 44 | 45 | describe('level: debug', function () { 46 | beforeEach(function () { 47 | logger.setLevel('debug') 48 | }) 49 | 50 | it('should log .log() messages', function () { 51 | expect(logMessage).to.equal('log') 52 | }) 53 | it('should log .debug() messages', function () { 54 | expect(debugMessage).to.equal('debug') 55 | }) 56 | it('should log .info() messages', function () { 57 | expect(infoMessage).to.equal('info') 58 | }) 59 | it('should log .warn() messages', function () { 60 | expect(warnMessage).to.equal('warn') 61 | }) 62 | it('should log .error() messages', function () { 63 | expect(errorMessage).to.equal('error') 64 | }) 65 | }) 66 | 67 | describe('level: info', function () { 68 | beforeEach(function () { 69 | logger.setLevel('info') 70 | }) 71 | 72 | it('should log .log() messages', function () { 73 | expect(logMessage).to.equal('log') 74 | }) 75 | it('should not log .debug() messages', function () { 76 | expect(debugMessage).to.equal(undefined) 77 | }) 78 | it('should log .info() messages', function () { 79 | expect(infoMessage).to.equal('info') 80 | }) 81 | it('should log .warn() messages', function () { 82 | expect(warnMessage).to.equal('warn') 83 | }) 84 | it('should log .error() messages', function () { 85 | expect(errorMessage).to.equal('error') 86 | }) 87 | }) 88 | 89 | describe('level: warn', function () { 90 | beforeEach(function () { 91 | logger.setLevel('warn') 92 | }) 93 | 94 | it('should log .log() messages', function () { 95 | expect(logMessage).to.equal('log') 96 | }) 97 | it('should not log .debug() messages', function () { 98 | expect(debugMessage).to.equal(undefined) 99 | }) 100 | it('should not log .info() messages', function () { 101 | expect(infoMessage).to.equal(undefined) 102 | }) 103 | it('should log .warn() messages', function () { 104 | expect(warnMessage).to.equal('warn') 105 | }) 106 | it('should log .error() messages', function () { 107 | expect(errorMessage).to.equal('error') 108 | }) 109 | }) 110 | 111 | describe('level: error', function () { 112 | beforeEach(function () { 113 | logger.setLevel('error') 114 | }) 115 | 116 | it('should log .log() messages', function () { 117 | expect(logMessage).to.equal('log') 118 | }) 119 | it('should not log .debug() messages', function () { 120 | expect(debugMessage).to.equal(undefined) 121 | }) 122 | it('should not log .info() messages', function () { 123 | expect(infoMessage).to.equal(undefined) 124 | }) 125 | it('should log .warn() messages', function () { 126 | expect(warnMessage).to.equal(undefined) 127 | }) 128 | it('should log .error() messages', function () { 129 | expect(errorMessage).to.equal('error') 130 | }) 131 | }) 132 | 133 | describe('level: silent', function () { 134 | beforeEach(function () { 135 | logger.setLevel('silent') 136 | }) 137 | 138 | it('should log .log() messages', function () { 139 | expect(logMessage).to.equal('log') 140 | }) 141 | it('should not log .debug() messages', function () { 142 | expect(debugMessage).to.equal(undefined) 143 | }) 144 | it('should not log .info() messages', function () { 145 | expect(infoMessage).to.equal(undefined) 146 | }) 147 | it('should not log .warn() messages', function () { 148 | expect(warnMessage).to.equal(undefined) 149 | }) 150 | it('should not log .error() messages', function () { 151 | expect(errorMessage).to.equal(undefined) 152 | }) 153 | }) 154 | 155 | describe('Interpolation', function () { 156 | // make sure all messages are logged 157 | beforeEach(function () { 158 | logger.setLevel('debug') 159 | }) 160 | 161 | beforeEach(function () { 162 | logger.log('log %s %s', 123, 456) 163 | logger.debug('debug %s %s', 123, 456) 164 | logger.info('info %s %s', 123, 456) 165 | logger.warn('warn %s %s', 123, 456) 166 | logger.error('error %s %s', 123, 456) 167 | }) 168 | 169 | it('should interpolate .log() messages', function () { 170 | expect(logMessage).to.equal('log 123 456') 171 | }) 172 | it('should interpolate .debug() messages', function () { 173 | expect(debugMessage).to.equal('debug 123 456') 174 | }) 175 | it('should interpolate .info() messages', function () { 176 | expect(infoMessage).to.equal('info 123 456') 177 | }) 178 | it('should interpolate .warn() messages', function () { 179 | expect(warnMessage).to.equal('warn 123 456') 180 | }) 181 | it('should interpolate .error() messages', function () { 182 | expect(errorMessage).to.equal('error 123 456') 183 | }) 184 | }) 185 | }) 186 | 187 | describe('Erroneous usage.', function () { 188 | var fn 189 | 190 | describe('Log provider is not a function', function () { 191 | beforeEach(function () { 192 | fn = function () { 193 | logger.setProvider({}) 194 | } 195 | }) 196 | 197 | it('should throw an error', function () { 198 | expect(fn).to.throw(Error) 199 | }) 200 | }) 201 | 202 | describe('Invalid logLevel', function () { 203 | beforeEach(function () { 204 | fn = function () { 205 | logger.setLevel('foo') 206 | } 207 | }) 208 | 209 | it('should throw an error', function () { 210 | expect(fn).to.throw(Error) 211 | }) 212 | }) 213 | }) 214 | }) 215 | 216 | describe('getArrow', function () { 217 | var arrow 218 | // scenario = [originalPath, newPath, originalTarget, newTarget] 219 | 220 | describe('default arrow', function () { 221 | beforeEach(function () { 222 | arrow = getArrow('/api', '/api', 'localhost:1337', 'localhost:1337') 223 | }) 224 | 225 | it('should return arrow: "->"', function () { 226 | expect(arrow).to.equal('->') 227 | }) 228 | }) 229 | 230 | describe('"pathRewrite" arrow', function () { 231 | beforeEach(function () { 232 | arrow = getArrow('/api', '/rest', 'localhost:1337', 'localhost:1337') 233 | }) 234 | 235 | it('should return arrow: "~>"', function () { 236 | expect(arrow).to.equal('~>') 237 | }) 238 | }) 239 | 240 | describe('"router" arrow', function () { 241 | beforeEach(function () { 242 | arrow = getArrow('/api', '/api', 'localhost:1337', 'localhost:8888') 243 | }) 244 | 245 | it('should return arrow: "=>"', function () { 246 | expect(arrow).to.equal('=>') 247 | }) 248 | }) 249 | 250 | describe('"pathRewrite" + "router" arrow', function () { 251 | beforeEach(function () { 252 | arrow = getArrow('/api', '/rest', 'localhost:1337', 'localhost:8888') 253 | }) 254 | 255 | it('should return arrow: "≈>"', function () { 256 | expect(arrow).to.equal('≈>') 257 | }) 258 | }) 259 | }) 260 | -------------------------------------------------------------------------------- /test/unit/context-matcher.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | // https://github.com/feross/standard/issues/690#issuecomment-278533482 3 | 4 | var expect = require('chai').expect 5 | var contextMatcher = require('./_libs').contextMatcher 6 | 7 | describe('Context Matching', function () { 8 | describe('String path matching', function () { 9 | var result 10 | 11 | describe('Single path matching', function () { 12 | it('should match all paths', function () { 13 | result = contextMatcher.match('', 'http://localhost/api/foo/bar') 14 | expect(result).to.be.true 15 | }) 16 | 17 | it('should match all paths starting with forward-slash', function () { 18 | result = contextMatcher.match('/', 'http://localhost/api/foo/bar') 19 | expect(result).to.be.true 20 | }) 21 | 22 | it('should return true when the context is present in url', function () { 23 | result = contextMatcher.match('/api', 'http://localhost/api/foo/bar') 24 | expect(result).to.be.true 25 | }) 26 | 27 | it('should return false when the context is not present in url', function () { 28 | result = contextMatcher.match('/abc', 'http://localhost/api/foo/bar') 29 | expect(result).to.be.false 30 | }) 31 | 32 | it('should return false when the context is present half way in url', function () { 33 | result = contextMatcher.match('/foo', 'http://localhost/api/foo/bar') 34 | expect(result).to.be.false 35 | }) 36 | 37 | it('should return false when the context does not start with /', function () { 38 | result = contextMatcher.match('api', 'http://localhost/api/foo/bar') 39 | expect(result).to.be.false 40 | }) 41 | }) 42 | 43 | describe('Multi path matching', function () { 44 | it('should return true when the context is present in url', function () { 45 | result = contextMatcher.match(['/api'], 'http://localhost/api/foo/bar') 46 | expect(result).to.be.true 47 | }) 48 | 49 | it('should return true when the context is present in url', function () { 50 | result = contextMatcher.match(['/api', '/ajax'], 'http://localhost/ajax/foo/bar') 51 | expect(result).to.be.true 52 | }) 53 | 54 | it('should return false when the context does not match url', function () { 55 | result = contextMatcher.match(['/api', '/ajax'], 'http://localhost/foo/bar') 56 | expect(result).to.be.false 57 | }) 58 | 59 | it('should return false when empty array provided', function () { 60 | result = contextMatcher.match([], 'http://localhost/api/foo/bar') 61 | expect(result).to.be.false 62 | }) 63 | }) 64 | }) 65 | 66 | describe('Wildcard path matching', function () { 67 | describe('Single glob', function () { 68 | var url 69 | 70 | beforeEach(function () { 71 | url = 'http://localhost/api/foo/bar.html' 72 | }) 73 | 74 | describe('url-path matching', function () { 75 | it('should match any path', function () { 76 | expect(contextMatcher.match('**', url)).to.be.true 77 | expect(contextMatcher.match('/**', url)).to.be.true 78 | }) 79 | 80 | it('should only match paths starting with "/api" ', function () { 81 | expect(contextMatcher.match('/api/**', url)).to.be.true 82 | expect(contextMatcher.match('/ajax/**', url)).to.be.false 83 | }) 84 | 85 | it('should only match paths starting with "foo" folder in it ', function () { 86 | expect(contextMatcher.match('**/foo/**', url)).to.be.true 87 | expect(contextMatcher.match('**/invalid/**', url)).to.be.false 88 | }) 89 | }) 90 | 91 | describe('file matching', function () { 92 | it('should match any path, file and extension', function () { 93 | expect(contextMatcher.match('**', url)).to.be.true 94 | expect(contextMatcher.match('**/*', url)).to.be.true 95 | expect(contextMatcher.match('**/*.*', url)).to.be.true 96 | expect(contextMatcher.match('/**', url)).to.be.true 97 | expect(contextMatcher.match('/**/*', url)).to.be.true 98 | expect(contextMatcher.match('/**/*.*', url)).to.be.true 99 | }) 100 | 101 | it('should only match .html files', function () { 102 | expect(contextMatcher.match('**/*.html', url)).to.be.true 103 | expect(contextMatcher.match('/**/*.html', url)).to.be.true 104 | expect(contextMatcher.match('/*.htm', url)).to.be.false 105 | expect(contextMatcher.match('/*.jpg', url)).to.be.false 106 | }) 107 | 108 | it('should only match .html under root path', function () { 109 | var pattern = '/*.html' 110 | expect(contextMatcher.match(pattern, 'http://localhost/index.html')).to.be.true 111 | expect(contextMatcher.match(pattern, 'http://localhost/some/path/index.html')).to.be.false 112 | }) 113 | 114 | it('should ignore query params', function () { 115 | expect(contextMatcher.match('/**/*.php', 'http://localhost/a/b/c.php?d=e&e=f')).to.be.true 116 | expect(contextMatcher.match('/**/*.php?*', 'http://localhost/a/b/c.php?d=e&e=f')).to.be.false 117 | }) 118 | 119 | it('should only match any file in root path', function () { 120 | expect(contextMatcher.match('/*', 'http://localhost/bar.html')).to.be.true 121 | expect(contextMatcher.match('/*.*', 'http://localhost/bar.html')).to.be.true 122 | expect(contextMatcher.match('/*', 'http://localhost/foo/bar.html')).to.be.false 123 | }) 124 | 125 | it('should only match .html file is in root path', function () { 126 | expect(contextMatcher.match('/*.html', 'http://localhost/bar.html')).to.be.true 127 | expect(contextMatcher.match('/*.html', 'http://localhost/api/foo/bar.html')).to.be.false 128 | }) 129 | 130 | it('should only match .html files in "foo" folder', function () { 131 | expect(contextMatcher.match('**/foo/*.html', url)).to.be.true 132 | expect(contextMatcher.match('**/bar/*.html', url)).to.be.false 133 | }) 134 | 135 | it('should not match .html files', function () { 136 | expect(contextMatcher.match('!**/*.html', url)).to.be.false 137 | }) 138 | }) 139 | }) 140 | 141 | describe('Multi glob matching', function () { 142 | describe('Multiple patterns', function () { 143 | it('should return true when both path patterns match', function () { 144 | var pattern = ['/api/**', '/ajax/**'] 145 | expect(contextMatcher.match(pattern, 'http://localhost/api/foo/bar.json')).to.be.true 146 | expect(contextMatcher.match(pattern, 'http://localhost/ajax/foo/bar.json')).to.be.true 147 | expect(contextMatcher.match(pattern, 'http://localhost/rest/foo/bar.json')).to.be.false 148 | }) 149 | it('should return true when both file extensions pattern match', function () { 150 | var pattern = ['/**/*.html', '/**/*.jpeg'] 151 | expect(contextMatcher.match(pattern, 'http://localhost/api/foo/bar.html')).to.be.true 152 | expect(contextMatcher.match(pattern, 'http://localhost/api/foo/bar.jpeg')).to.be.true 153 | expect(contextMatcher.match(pattern, 'http://localhost/api/foo/bar.gif')).to.be.false 154 | }) 155 | }) 156 | 157 | describe('Negation patterns', function () { 158 | it('should not match file extension', function () { 159 | var url = 'http://localhost/api/foo/bar.html' 160 | expect(contextMatcher.match(['**', '!**/*.html'], url)).to.be.false 161 | expect(contextMatcher.match(['**', '!**/*.json'], url)).to.be.true 162 | }) 163 | }) 164 | }) 165 | }) 166 | 167 | describe('Use function for matching', function () { 168 | var testFunctionAsContext = function (val) { 169 | return contextMatcher.match(fn, 'http://localhost/api/foo/bar') 170 | 171 | function fn (path, req) { 172 | return val 173 | } 174 | } 175 | 176 | describe('truthy', function () { 177 | it('should match when function returns true', function () { 178 | expect(testFunctionAsContext(true)).to.be.ok 179 | expect(testFunctionAsContext('true')).to.be.ok 180 | }) 181 | }) 182 | 183 | describe('falsy', function () { 184 | it('should not match when function returns falsy value', function () { 185 | expect(testFunctionAsContext()).to.not.be.ok 186 | expect(testFunctionAsContext(undefined)).to.not.be.ok 187 | expect(testFunctionAsContext(false)).to.not.be.ok 188 | expect(testFunctionAsContext('')).to.not.be.ok 189 | }) 190 | }) 191 | }) 192 | 193 | describe('Test invalid contexts', function () { 194 | var testContext 195 | 196 | beforeEach(function () { 197 | testContext = function (context) { 198 | return function () { 199 | contextMatcher.match(context, 'http://localhost/api/foo/bar') 200 | } 201 | } 202 | }) 203 | 204 | describe('Throw error', function () { 205 | it('should throw error with undefined', function () { 206 | expect(testContext(undefined)).to.throw(Error) 207 | }) 208 | 209 | it('should throw error with null', function () { 210 | expect(testContext(null)).to.throw(Error) 211 | }) 212 | 213 | it('should throw error with object literal', function () { 214 | expect(testContext({})).to.throw(Error) 215 | }) 216 | 217 | it('should throw error with integers', function () { 218 | expect(testContext(123)).to.throw(Error) 219 | }) 220 | 221 | it('should throw error with mixed string and glob pattern', function () { 222 | expect(testContext(['/api', '!*.html'])).to.throw(Error) 223 | }) 224 | }) 225 | 226 | describe('Do not throw error', function () { 227 | it('should not throw error with string', function () { 228 | expect(testContext('/123')).not.to.throw(Error) 229 | }) 230 | 231 | it('should not throw error with Array', function () { 232 | expect(testContext(['/123'])).not.to.throw(Error) 233 | }) 234 | it('should not throw error with glob', function () { 235 | expect(testContext('/**')).not.to.throw(Error) 236 | }) 237 | 238 | it('should not throw error with Array of globs', function () { 239 | expect(testContext(['/**', '!*.html'])).not.to.throw(Error) 240 | }) 241 | 242 | it('should not throw error with Function', function () { 243 | expect(testContext(function () {})).not.to.throw(Error) 244 | }) 245 | }) 246 | }) 247 | }) 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # http-proxy-middleware 2 | 3 | [![Build Status](https://img.shields.io/travis/chimurai/http-proxy-middleware/master.svg?style=flat-square)](https://travis-ci.org/chimurai/http-proxy-middleware) 4 | [![Coveralls](https://img.shields.io/coveralls/chimurai/http-proxy-middleware.svg?style=flat-square)](https://coveralls.io/r/chimurai/http-proxy-middleware) 5 | [![dependency Status](https://img.shields.io/david/chimurai/http-proxy-middleware.svg?style=flat-square)](https://david-dm.org/chimurai/http-proxy-middleware#info=dependencies) 6 | [![dependency Status](https://snyk.io/test/npm/http-proxy-middleware/badge.svg)](https://snyk.io/test/npm/http-proxy-middleware) 7 | [![JavaScript Style Guide](https://img.shields.io/badge/codestyle-standard-brightgreen.svg)](https://standardjs.com) 8 | 9 | Node.js proxying made simple. Configure proxy middleware with ease for [connect](https://github.com/senchalabs/connect), [express](https://github.com/strongloop/express), [browser-sync](https://github.com/BrowserSync/browser-sync) and [many more](#compatible-servers). 10 | 11 | Powered by the popular Nodejitsu [`http-proxy`](https://github.com/nodejitsu/node-http-proxy). [![GitHub stars](https://img.shields.io/github/stars/nodejitsu/node-http-proxy.svg?style=social&label=Star)](https://github.com/nodejitsu/node-http-proxy) 12 | 13 | ## TL;DR 14 | 15 | Proxy `/api` requests to `http://www.example.org` 16 | 17 | ```javascript 18 | var express = require('express'); 19 | var proxy = require('http-proxy-middleware'); 20 | 21 | var app = express(); 22 | 23 | app.use('/api', proxy({target: 'http://www.example.org', changeOrigin: true})); 24 | app.listen(3000); 25 | 26 | // http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar 27 | ``` 28 | 29 | _All_ `http-proxy` [options](https://github.com/nodejitsu/node-http-proxy#options) can be used, along with some extra `http-proxy-middleware` [options](#options). 30 | 31 | :bulb: **Tip:** Set the option `changeOrigin` to `true` for [name-based virtual hosted sites](http://en.wikipedia.org/wiki/Virtual_hosting#Name-based). 32 | 33 | ## Table of Contents 34 | 35 | 36 | 37 | - [Install](#install) 38 | - [Core concept](#core-concept) 39 | - [Example](#example) 40 | - [Context matching](#context-matching) 41 | - [Options](#options) 42 | - [http-proxy-middleware options](#http-proxy-middleware-options) 43 | - [http-proxy events](#http-proxy-events) 44 | - [http-proxy options](#http-proxy-options) 45 | - [Shorthand](#shorthand) 46 | - [app.use\(path, proxy\)](#appusepath-proxy) 47 | - [WebSocket](#websocket) 48 | - [External WebSocket upgrade](#external-websocket-upgrade) 49 | - [Working examples](#working-examples) 50 | - [Recipes](#recipes) 51 | - [Compatible servers](#compatible-servers) 52 | - [Tests](#tests) 53 | - [Changelog](#changelog) 54 | - [License](#license) 55 | 56 | 57 | 58 | 59 | ## Install 60 | 61 | ```javascript 62 | $ npm install --save-dev http-proxy-middleware 63 | ``` 64 | 65 | ## Core concept 66 | 67 | Proxy middleware configuration. 68 | 69 | #### proxy([context,] config) 70 | 71 | ```javascript 72 | var proxy = require('http-proxy-middleware'); 73 | 74 | var apiProxy = proxy('/api', {target: 'http://www.example.org'}); 75 | // \____/ \_____________________________/ 76 | // | | 77 | // context options 78 | 79 | // 'apiProxy' is now ready to be used as middleware in a server. 80 | ``` 81 | * **context**: Determine which requests should be proxied to the target host. 82 | (more on [context matching](#context-matching)) 83 | * **options.target**: target host to proxy to. _(protocol + host)_ 84 | 85 | (full list of [`http-proxy-middleware` configuration options](#options)) 86 | 87 | #### proxy(uri [, config]) 88 | 89 | ``` javascript 90 | // shorthand syntax for the example above: 91 | var apiProxy = proxy('http://www.example.org/api'); 92 | 93 | ``` 94 | More about the [shorthand configuration](#shorthand). 95 | 96 | ## Example 97 | 98 | An example with `express` server. 99 | 100 | ```javascript 101 | // include dependencies 102 | var express = require('express'); 103 | var proxy = require('http-proxy-middleware'); 104 | 105 | // proxy middleware options 106 | var options = { 107 | target: 'http://www.example.org', // target host 108 | changeOrigin: true, // needed for virtual hosted sites 109 | ws: true, // proxy websockets 110 | pathRewrite: { 111 | '^/api/old-path' : '/api/new-path', // rewrite path 112 | '^/api/remove/path' : '/path' // remove base path 113 | }, 114 | router: { 115 | // when request.headers.host == 'dev.localhost:3000', 116 | // override target 'http://www.example.org' to 'http://localhost:8000' 117 | 'dev.localhost:3000' : 'http://localhost:8000' 118 | } 119 | }; 120 | 121 | // create the proxy (without context) 122 | var exampleProxy = proxy(options); 123 | 124 | // mount `exampleProxy` in web server 125 | var app = express(); 126 | app.use('/api', exampleProxy); 127 | app.listen(3000); 128 | ``` 129 | 130 | ## Context matching 131 | 132 | Providing an alternative way to decide which requests should be proxied; In case you are not able to use the server's [`path` parameter](http://expressjs.com/en/4x/api.html#app.use) to mount the proxy or when you need more flexibility. 133 | 134 | [RFC 3986 `path`](https://tools.ietf.org/html/rfc3986#section-3.3) is used for context matching. 135 | 136 | ``` 137 | foo://example.com:8042/over/there?name=ferret#nose 138 | \_/ \______________/\_________/ \_________/ \__/ 139 | | | | | | 140 | scheme authority path query fragment 141 | ``` 142 | 143 | * **path matching** 144 | - `proxy({...})` - matches any path, all requests will be proxied. 145 | - `proxy('/', {...})` - matches any path, all requests will be proxied. 146 | - `proxy('/api', {...})` - matches paths starting with `/api` 147 | 148 | * **multiple path matching** 149 | - `proxy(['/api', '/ajax', '/someotherpath'], {...})` 150 | 151 | * **wildcard path matching** 152 | 153 | For fine-grained control you can use wildcard matching. Glob pattern matching is done by _micromatch_. Visit [micromatch](https://www.npmjs.com/package/micromatch) or [glob](https://www.npmjs.com/package/glob) for more globbing examples. 154 | - `proxy('**', {...})` matches any path, all requests will be proxied. 155 | - `proxy('**/*.html', {...})` matches any path which ends with `.html` 156 | - `proxy('/*.html', {...})` matches paths directly under path-absolute 157 | - `proxy('/api/**/*.html', {...})` matches requests ending with `.html` in the path of `/api` 158 | - `proxy(['/api/**', '/ajax/**'], {...})` combine multiple patterns 159 | - `proxy(['/api/**', '!**/bad.json'], {...})` exclusion 160 | 161 | * **custom matching** 162 | 163 | For full control you can provide a custom function to determine which requests should be proxied or not. 164 | ```javascript 165 | /** 166 | * @return {Boolean} 167 | */ 168 | var filter = function (pathname, req) { 169 | return (pathname.match('^/api') && req.method === 'GET'); 170 | }; 171 | 172 | var apiProxy = proxy(filter, {target: 'http://www.example.org'}) 173 | ``` 174 | 175 | ## Options 176 | 177 | ### http-proxy-middleware options 178 | 179 | * **option.pathRewrite**: object/function, rewrite target's url path. Object-keys will be used as _RegExp_ to match paths. 180 | ```javascript 181 | // rewrite path 182 | pathRewrite: {'^/old/api' : '/new/api'} 183 | 184 | // remove path 185 | pathRewrite: {'^/remove/api' : ''} 186 | 187 | // add base path 188 | pathRewrite: {'^/' : '/basepath/'} 189 | 190 | // custom rewriting 191 | pathRewrite: function (path, req) { return path.replace('/api', '/base/api') } 192 | ``` 193 | 194 | * **option.router**: object/function, re-target `option.target` for specific requests. 195 | ```javascript 196 | // Use `host` and/or `path` to match requests. First match will be used. 197 | // The order of the configuration matters. 198 | router: { 199 | 'integration.localhost:3000' : 'http://localhost:8001', // host only 200 | 'staging.localhost:3000' : 'http://localhost:8002', // host only 201 | 'localhost:3000/api' : 'http://localhost:8003', // host + path 202 | '/rest' : 'http://localhost:8004' // path only 203 | } 204 | 205 | // Custom router function 206 | router: function(req) { 207 | return 'http://localhost:8004'; 208 | } 209 | ``` 210 | 211 | * **option.logLevel**: string, ['debug', 'info', 'warn', 'error', 'silent']. Default: `'info'` 212 | 213 | * **option.logProvider**: function, modify or replace log provider. Default: `console`. 214 | ```javascript 215 | // simple replace 216 | function logProvider(provider) { 217 | // replace the default console log provider. 218 | return require('winston'); 219 | } 220 | ``` 221 | 222 | ```javascript 223 | // verbose replacement 224 | function logProvider(provider) { 225 | var logger = new (require('winston').Logger)(); 226 | 227 | var myCustomProvider = { 228 | log: logger.log, 229 | debug: logger.debug, 230 | info: logger.info, 231 | warn: logger.warn, 232 | error: logger.error 233 | } 234 | return myCustomProvider; 235 | } 236 | ``` 237 | * (DEPRECATED) **option.proxyHost**: Use `option.changeOrigin = true` instead. 238 | * (DEPRECATED) **option.proxyTable**: Use `option.router` instead. 239 | 240 | 241 | ### http-proxy events 242 | 243 | Subscribe to [http-proxy events](https://github.com/nodejitsu/node-http-proxy#listening-for-proxy-events): 244 | 245 | * **option.onError**: function, subscribe to http-proxy's `error` event for custom error handling. 246 | ```javascript 247 | function onError(err, req, res) { 248 | res.writeHead(500, { 249 | 'Content-Type': 'text/plain' 250 | }); 251 | res.end('Something went wrong. And we are reporting a custom error message.'); 252 | } 253 | ``` 254 | 255 | * **option.onProxyRes**: function, subscribe to http-proxy's `proxyRes` event. 256 | ```javascript 257 | function onProxyRes(proxyRes, req, res) { 258 | proxyRes.headers['x-added'] = 'foobar'; // add new header to response 259 | delete proxyRes.headers['x-removed']; // remove header from response 260 | } 261 | ``` 262 | 263 | * **option.onProxyReq**: function, subscribe to http-proxy's `proxyReq` event. 264 | ```javascript 265 | function onProxyReq(proxyReq, req, res) { 266 | // add custom header to request 267 | proxyReq.setHeader('x-added', 'foobar'); 268 | // or log the req 269 | } 270 | ``` 271 | 272 | * **option.onProxyReqWs**: function, subscribe to http-proxy's `proxyReqWs` event. 273 | ```javascript 274 | function onProxyReqWs(proxyReq, req, socket, options, head) { 275 | // add custom header 276 | proxyReq.setHeader('X-Special-Proxy-Header', 'foobar'); 277 | } 278 | ``` 279 | 280 | * **option.onOpen**: function, subscribe to http-proxy's `open` event. 281 | ```javascript 282 | function onOpen(proxySocket) { 283 | // listen for messages coming FROM the target here 284 | proxySocket.on('data', hybiParseAndLogMessage); 285 | } 286 | ``` 287 | 288 | * **option.onClose**: function, subscribe to http-proxy's `close` event. 289 | ```javascript 290 | function onClose(res, socket, head) { 291 | // view disconnected websocket connections 292 | console.log('Client disconnected'); 293 | } 294 | ``` 295 | 296 | ### http-proxy options 297 | 298 | The following options are provided by the underlying [http-proxy](https://github.com/nodejitsu/node-http-proxy#options) library. 299 | 300 | * **option.target**: url string to be parsed with the url module 301 | * **option.forward**: url string to be parsed with the url module 302 | * **option.agent**: object to be passed to http(s).request (see Node's [https agent](http://nodejs.org/api/https.html#https_class_https_agent) and [http agent](http://nodejs.org/api/http.html#http_class_http_agent) objects) 303 | * **option.ssl**: object to be passed to https.createServer() 304 | * **option.ws**: true/false: if you want to proxy websockets 305 | * **option.xfwd**: true/false, adds x-forward headers 306 | * **option.secure**: true/false, if you want to verify the SSL Certs 307 | * **option.toProxy**: true/false, passes the absolute URL as the `path` (useful for proxying to proxies) 308 | * **option.prependPath**: true/false, Default: true - specify whether you want to prepend the target's path to the proxy path 309 | * **option.ignorePath**: true/false, Default: false - specify whether you want to ignore the proxy path of the incoming request (note: you will have to append / manually if required). 310 | * **option.localAddress** : Local interface string to bind for outgoing connections 311 | * **option.changeOrigin**: true/false, Default: false - changes the origin of the host header to the target URL 312 | * **option.auth** : Basic authentication i.e. 'user:password' to compute an Authorization header. 313 | * **option.hostRewrite**: rewrites the location hostname on (301/302/307/308) redirects. 314 | * **option.autoRewrite**: rewrites the location host/port on (301/302/307/308) redirects based on requested host/port. Default: false. 315 | * **option.protocolRewrite**: rewrites the location protocol on (301/302/307/308) redirects to 'http' or 'https'. Default: null. 316 | * **option.cookieDomainRewrite**: rewrites domain of `set-cookie` headers. Possible values: 317 | * `false` (default): disable cookie rewriting 318 | * String: new domain, for example `cookieDomainRewrite: "new.domain"`. To remove the domain, use `cookieDomainRewrite: ""`. 319 | * Object: mapping of domains to new domains, use `"*"` to match all domains. 320 | For example keep one domain unchanged, rewrite one domain and remove other domains: 321 | ``` 322 | cookieDomainRewrite: { 323 | "unchanged.domain": "unchanged.domain", 324 | "old.domain": "new.domain", 325 | "*": "" 326 | } 327 | ``` 328 | * **option.headers**: object, adds [request headers](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields). (Example: `{host:'www.example.org'}`) 329 | * **option.proxyTimeout**: timeout (in millis) when proxy receives no response from target 330 | 331 | 332 | 333 | ## Shorthand 334 | 335 | Use the shorthand syntax when verbose configuration is not needed. The `context` and `option.target` will be automatically configured when shorthand is used. Options can still be used if needed. 336 | 337 | ```javascript 338 | proxy('http://www.example.org:8000/api'); 339 | // proxy('/api', {target: 'http://www.example.org:8000'}); 340 | 341 | 342 | proxy('http://www.example.org:8000/api/books/*/**.json'); 343 | // proxy('/api/books/*/**.json', {target: 'http://www.example.org:8000'}); 344 | 345 | 346 | proxy('http://www.example.org:8000/api', {changeOrigin:true}); 347 | // proxy('/api', {target: 'http://www.example.org:8000', changeOrigin: true}); 348 | ``` 349 | 350 | ### app.use(path, proxy) 351 | 352 | If you want to use the server's `app.use` `path` parameter to match requests; 353 | Create and mount the proxy without the http-proxy-middleware `context` parameter: 354 | ```javascript 355 | app.use('/api', proxy({target:'http://www.example.org', changeOrigin:true})); 356 | ``` 357 | 358 | `app.use` documentation: 359 | * express: http://expressjs.com/en/4x/api.html#app.use 360 | * connect: https://github.com/senchalabs/connect#mount-middleware 361 | 362 | ## WebSocket 363 | 364 | ```javascript 365 | // verbose api 366 | proxy('/', {target:'http://echo.websocket.org', ws:true}); 367 | 368 | // shorthand 369 | proxy('http://echo.websocket.org', {ws:true}); 370 | 371 | // shorter shorthand 372 | proxy('ws://echo.websocket.org'); 373 | ``` 374 | 375 | ### External WebSocket upgrade 376 | 377 | In the previous WebSocket examples, http-proxy-middleware relies on a initial http request in order to listen to the http `upgrade` event. If you need to proxy WebSockets without the initial http request, you can subscribe to the server's http `upgrade` event manually. 378 | ```javascript 379 | var wsProxy = proxy('ws://echo.websocket.org', {changeOrigin:true}); 380 | 381 | var app = express(); 382 | app.use(wsProxy); 383 | 384 | var server = app.listen(3000); 385 | server.on('upgrade', wsProxy.upgrade); // <-- subscribe to http 'upgrade' 386 | ``` 387 | 388 | 389 | ## Working examples 390 | 391 | View and play around with [working examples](https://github.com/chimurai/http-proxy-middleware/tree/master/examples). 392 | 393 | * Browser-Sync ([example source](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/browser-sync/index.js)) 394 | * express ([example source](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/express/index.js)) 395 | * connect ([example source](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/connect/index.js)) 396 | * WebSocket ([example source](https://github.com/chimurai/http-proxy-middleware/tree/master/examples/websocket/index.js)) 397 | 398 | ## Recipes 399 | 400 | View the [recipes](https://github.com/chimurai/http-proxy-middleware/tree/master/recipes) for common use cases. 401 | 402 | ## Compatible servers 403 | 404 | `http-proxy-middleware` is compatible with the following servers: 405 | * [connect](https://www.npmjs.com/package/connect) 406 | * [express](https://www.npmjs.com/package/express) 407 | * [browser-sync](https://www.npmjs.com/package/browser-sync) 408 | * [lite-server](https://www.npmjs.com/package/lite-server) 409 | * [grunt-contrib-connect](https://www.npmjs.com/package/grunt-contrib-connect) 410 | * [grunt-browser-sync](https://www.npmjs.com/package/grunt-browser-sync) 411 | * [gulp-connect](https://www.npmjs.com/package/gulp-connect) 412 | * [gulp-webserver](https://www.npmjs.com/package/gulp-webserver) 413 | 414 | Sample implementations can be found in the [server recipes](https://github.com/chimurai/http-proxy-middleware/tree/master/recipes/servers.md). 415 | 416 | ## Tests 417 | 418 | Run the test suite: 419 | 420 | ```bash 421 | # install dependencies 422 | $ npm install 423 | 424 | # linting 425 | $ npm run lint 426 | 427 | # unit tests 428 | $ npm test 429 | 430 | # code coverage 431 | $ npm run cover 432 | ``` 433 | 434 | ## Changelog 435 | 436 | - [View changelog](https://github.com/chimurai/http-proxy-middleware/blob/master/CHANGELOG.md) 437 | 438 | 439 | ## License 440 | 441 | The MIT License (MIT) 442 | 443 | Copyright (c) 2015-2017 Steven Chim 444 | -------------------------------------------------------------------------------- /test/e2e/http-proxy-middleware.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | // https://github.com/feross/standard/issues/690#issuecomment-278533482 3 | 4 | var utils = require('./_utils') 5 | var expect = require('chai').expect 6 | var http = require('http') 7 | 8 | describe('E2E http-proxy-middleware', function () { 9 | var createServer 10 | var proxyMiddleware 11 | 12 | beforeEach(function () { 13 | createServer = utils.createServer 14 | proxyMiddleware = utils.proxyMiddleware 15 | }) 16 | 17 | describe('http-proxy-middleware creation', function () { 18 | it('should create a middleware', function () { 19 | var middleware 20 | middleware = proxyMiddleware('/api', {target: 'http://localhost:8000'}) 21 | expect(middleware).to.be.a('function') 22 | }) 23 | }) 24 | 25 | describe('context matching', function () { 26 | describe('do not proxy', function () { 27 | var isSkipped 28 | 29 | beforeEach(function () { 30 | isSkipped = false 31 | 32 | var middleware 33 | 34 | var mockReq = {url: '/foo/bar', originalUrl: '/foo/bar'} 35 | var mockRes = {} 36 | var mockNext = function () { 37 | // mockNext will be called when request is not proxied 38 | isSkipped = true 39 | } 40 | 41 | middleware = proxyMiddleware('/api', {target: 'http://localhost:8000'}) 42 | middleware(mockReq, mockRes, mockNext) 43 | }) 44 | 45 | it('should not proxy requests when request url does not match context', function () { 46 | expect(isSkipped).to.be.true 47 | }) 48 | }) 49 | }) 50 | 51 | describe('http-proxy-middleware in actual server', function () { 52 | describe('basic setup, requests to target', function () { 53 | var proxyServer, targetServer 54 | var targetHeaders 55 | var targetUrl 56 | var responseBody 57 | 58 | beforeEach(function (done) { 59 | var mwProxy = proxyMiddleware('/api', {target: 'http://localhost:8000'}) 60 | 61 | var mwTarget = function (req, res, next) { 62 | targetUrl = req.url // store target url. 63 | targetHeaders = req.headers // store target headers. 64 | res.write('HELLO WEB') // respond with 'HELLO WEB' 65 | res.end() 66 | } 67 | 68 | proxyServer = createServer(3000, mwProxy) 69 | targetServer = createServer(8000, mwTarget) 70 | 71 | http.get('http://localhost:3000/api/b/c/dp?q=1&r=[2,3]#s"', function (res) { 72 | res.on('data', function (chunk) { 73 | responseBody = chunk.toString() 74 | done() 75 | }) 76 | }) 77 | }) 78 | 79 | afterEach(function () { 80 | proxyServer.close() 81 | targetServer.close() 82 | }) 83 | 84 | it('should have the same headers.host value', function () { 85 | expect(targetHeaders.host).to.equal('localhost:3000') 86 | }) 87 | 88 | it('should have proxied the uri-path and uri-query, but not the uri-hash', function () { 89 | expect(targetUrl).to.equal('/api/b/c/dp?q=1&r=[2,3]') 90 | }) 91 | 92 | it('should have response body: "HELLO WEB"', function () { 93 | expect(responseBody).to.equal('HELLO WEB') 94 | }) 95 | }) 96 | 97 | describe('custom context matcher/filter', function () { 98 | var proxyServer, targetServer 99 | var responseBody 100 | 101 | var filterPath, filterReq 102 | 103 | beforeEach(function (done) { 104 | var filter = function (path, req) { 105 | filterPath = path 106 | filterReq = req 107 | return true 108 | } 109 | 110 | var mwProxy = proxyMiddleware(filter, {target: 'http://localhost:8000'}) 111 | 112 | var mwTarget = function (req, res, next) { 113 | res.write('HELLO WEB') // respond with 'HELLO WEB' 114 | res.end() 115 | } 116 | 117 | proxyServer = createServer(3000, mwProxy) 118 | targetServer = createServer(8000, mwTarget) 119 | 120 | http.get('http://localhost:3000/api/b/c/d', function (res) { 121 | res.on('data', function (chunk) { 122 | responseBody = chunk.toString() 123 | done() 124 | }) 125 | }) 126 | }) 127 | 128 | afterEach(function () { 129 | proxyServer.close() 130 | targetServer.close() 131 | }) 132 | 133 | it('should have response body: "HELLO WEB"', function () { 134 | expect(responseBody).to.equal('HELLO WEB') 135 | }) 136 | 137 | it('should provide the url path in the first argument', function () { 138 | expect(filterPath).to.equal('/api/b/c/d') 139 | }) 140 | 141 | it('should provide the req object in the second argument', function () { 142 | expect(filterReq.method).to.equal('GET') 143 | }) 144 | }) 145 | 146 | describe('multi path', function () { 147 | var proxyServer, targetServer 148 | var response, responseBody 149 | 150 | beforeEach(function () { 151 | var mwProxy = proxyMiddleware(['/api', '/ajax'], {target: 'http://localhost:8000'}) 152 | 153 | var mwTarget = function (req, res, next) { 154 | res.write(req.url) // respond with req.url 155 | res.end() 156 | } 157 | 158 | proxyServer = createServer(3000, mwProxy) 159 | targetServer = createServer(8000, mwTarget) 160 | }) 161 | 162 | afterEach(function () { 163 | proxyServer.close() 164 | targetServer.close() 165 | }) 166 | 167 | describe('request to path A, configured', function () { 168 | beforeEach(function (done) { 169 | http.get('http://localhost:3000/api/some/endpoint', function (res) { 170 | response = res 171 | res.on('data', function (chunk) { 172 | responseBody = chunk.toString() 173 | done() 174 | }) 175 | }) 176 | }) 177 | 178 | it('should proxy to path A', function () { 179 | expect(response.statusCode).to.equal(200) 180 | expect(responseBody).to.equal('/api/some/endpoint') 181 | }) 182 | }) 183 | 184 | describe('request to path B, configured', function () { 185 | beforeEach(function (done) { 186 | http.get('http://localhost:3000/ajax/some/library', function (res) { 187 | response = res 188 | res.on('data', function (chunk) { 189 | responseBody = chunk.toString() 190 | done() 191 | }) 192 | }) 193 | }) 194 | 195 | it('should proxy to path B', function () { 196 | expect(response.statusCode).to.equal(200) 197 | expect(responseBody).to.equal('/ajax/some/library') 198 | }) 199 | }) 200 | 201 | describe('request to path C, not configured', function () { 202 | beforeEach(function (done) { 203 | http.get('http://localhost:3000/lorum/ipsum', function (res) { 204 | response = res 205 | res.on('data', function (chunk) { 206 | responseBody = chunk.toString() 207 | done() 208 | }) 209 | }) 210 | }) 211 | 212 | it('should not proxy to this path', function () { 213 | expect(response.statusCode).to.equal(404) 214 | }) 215 | }) 216 | }) 217 | 218 | describe('wildcard path matching', function () { 219 | var proxyServer, targetServer 220 | var response, responseBody 221 | 222 | beforeEach(function () { 223 | var mwProxy = proxyMiddleware('/api/**', {target: 'http://localhost:8000'}) 224 | 225 | var mwTarget = function (req, res, next) { 226 | res.write(req.url) // respond with req.url 227 | res.end() 228 | } 229 | 230 | proxyServer = createServer(3000, mwProxy) 231 | targetServer = createServer(8000, mwTarget) 232 | }) 233 | 234 | beforeEach(function (done) { 235 | http.get('http://localhost:3000/api/some/endpoint', function (res) { 236 | response = res 237 | res.on('data', function (chunk) { 238 | responseBody = chunk.toString() 239 | done() 240 | }) 241 | }) 242 | }) 243 | 244 | afterEach(function () { 245 | proxyServer.close() 246 | targetServer.close() 247 | }) 248 | 249 | it('should proxy to path', function () { 250 | expect(response.statusCode).to.equal(200) 251 | expect(responseBody).to.equal('/api/some/endpoint') 252 | }) 253 | }) 254 | 255 | describe('multi glob wildcard path matching', function () { 256 | var proxyServer, targetServer 257 | var responseA, responseBodyA 258 | var responseB 259 | 260 | beforeEach(function () { 261 | var mwProxy = proxyMiddleware(['**/*.html', '!**.json'], {target: 'http://localhost:8000'}) 262 | 263 | var mwTarget = function (req, res, next) { 264 | res.write(req.url) // respond with req.url 265 | res.end() 266 | } 267 | 268 | proxyServer = createServer(3000, mwProxy) 269 | targetServer = createServer(8000, mwTarget) 270 | }) 271 | 272 | beforeEach(function (done) { 273 | http.get('http://localhost:3000/api/some/endpoint/index.html', function (res) { 274 | responseA = res 275 | res.on('data', function (chunk) { 276 | responseBodyA = chunk.toString() 277 | done() 278 | }) 279 | }) 280 | }) 281 | 282 | beforeEach(function (done) { 283 | http.get('http://localhost:3000/api/some/endpoint/data.json', function (res) { 284 | responseB = res 285 | res.on('data', function (chunk) { 286 | done() 287 | }) 288 | }) 289 | }) 290 | 291 | afterEach(function () { 292 | proxyServer.close() 293 | targetServer.close() 294 | }) 295 | 296 | it('should proxy to paths ending with *.html', function () { 297 | expect(responseA.statusCode).to.equal(200) 298 | expect(responseBodyA).to.equal('/api/some/endpoint/index.html') 299 | }) 300 | 301 | it('should not proxy to paths ending with *.json', function () { 302 | expect(responseB.statusCode).to.equal(404) 303 | }) 304 | }) 305 | 306 | describe('option.headers - additional request headers', function () { 307 | var proxyServer, targetServer 308 | var targetHeaders 309 | 310 | beforeEach(function (done) { 311 | var mwProxy = proxyMiddleware('/api', {target: 'http://localhost:8000', headers: {host: 'foobar.dev'}}) 312 | 313 | var mwTarget = function (req, res, next) { 314 | targetHeaders = req.headers 315 | res.end() 316 | } 317 | 318 | proxyServer = createServer(3000, mwProxy) 319 | targetServer = createServer(8000, mwTarget) 320 | 321 | http.get('http://localhost:3000/api/', function (res) { 322 | done() 323 | }) 324 | }) 325 | 326 | afterEach(function () { 327 | proxyServer.close() 328 | targetServer.close() 329 | }) 330 | 331 | it('should send request header "host" to target server', function () { 332 | expect(targetHeaders.host).to.equal('foobar.dev') 333 | }) 334 | }) 335 | 336 | describe('legacy option.proxyHost', function () { 337 | var proxyServer, targetServer 338 | var targetHeaders 339 | 340 | beforeEach(function (done) { 341 | var mwProxy = proxyMiddleware('/api', {target: 'http://localhost:8000', proxyHost: 'foobar.dev'}) 342 | 343 | var mwTarget = function (req, res, next) { 344 | targetHeaders = req.headers 345 | res.end() 346 | } 347 | 348 | proxyServer = createServer(3000, mwProxy) 349 | targetServer = createServer(8000, mwTarget) 350 | 351 | http.get('http://localhost:3000/api/', function (res) { 352 | done() 353 | }) 354 | }) 355 | 356 | afterEach(function () { 357 | proxyServer.close() 358 | targetServer.close() 359 | }) 360 | 361 | it('should proxy host header to target server', function () { 362 | expect(targetHeaders.host).to.equal('foobar.dev') 363 | }) 364 | }) 365 | 366 | describe('option.onError - Error handling', function () { 367 | var proxyServer, targetServer 368 | var response, responseBody 369 | 370 | describe('default', function () { 371 | beforeEach(function (done) { 372 | var mwProxy = proxyMiddleware('/api', {target: 'http://localhost:666'}) // unreachable host on port:666 373 | var mwTarget = function (req, res, next) { next() } 374 | 375 | proxyServer = createServer(3000, mwProxy) 376 | targetServer = createServer(8000, mwTarget) 377 | 378 | http.get('http://localhost:3000/api/', function (res) { 379 | response = res 380 | done() 381 | }) 382 | }) 383 | 384 | afterEach(function () { 385 | proxyServer.close() 386 | targetServer.close() 387 | }) 388 | 389 | it('should handle errors when host is not reachable', function () { 390 | expect(response.statusCode).to.equal(504) 391 | }) 392 | }) 393 | 394 | describe('custom', function () { 395 | beforeEach(function (done) { 396 | var customOnError = function (err, req, res) { 397 | if (err) { 398 | res.writeHead(418) // different error code 399 | res.end('I\'m a teapot') // no response body 400 | } 401 | } 402 | 403 | var mwProxy = proxyMiddleware('/api', {target: 'http://localhost:666', onError: customOnError}) // unreachable host on port:666 404 | var mwTarget = function (req, res, next) { next() } 405 | 406 | proxyServer = createServer(3000, mwProxy) 407 | targetServer = createServer(8000, mwTarget) 408 | 409 | http.get('http://localhost:3000/api/', function (res) { 410 | response = res 411 | res.on('data', function (chunk) { 412 | responseBody = chunk.toString() 413 | done() 414 | }) 415 | }) 416 | }) 417 | 418 | afterEach(function () { 419 | proxyServer.close() 420 | targetServer.close() 421 | }) 422 | 423 | it('should respond with custom http status code', function () { 424 | expect(response.statusCode).to.equal(418) 425 | }) 426 | 427 | it('should respond with custom status message', function () { 428 | expect(responseBody).to.equal('I\'m a teapot') 429 | }) 430 | }) 431 | }) 432 | 433 | describe('option.onProxyRes', function () { 434 | var proxyServer, targetServer 435 | var response 436 | 437 | beforeEach(function (done) { 438 | var fnOnProxyRes = function (proxyRes, req, res) { 439 | proxyRes.headers['x-added'] = 'foobar' // add custom header to response 440 | delete proxyRes.headers['x-removed'] 441 | } 442 | 443 | var mwProxy = proxyMiddleware('/api', { 444 | target: 'http://localhost:8000', 445 | onProxyRes: fnOnProxyRes 446 | }) 447 | var mwTarget = function (req, res, next) { 448 | res.setHeader('x-removed', 'remove-header') 449 | res.write(req.url) // respond with req.url 450 | res.end() 451 | } 452 | 453 | proxyServer = createServer(3000, mwProxy) 454 | targetServer = createServer(8000, mwTarget) 455 | 456 | http.get('http://localhost:3000/api/foo/bar', function (res) { 457 | response = res 458 | res.on('data', function (chunk) { 459 | done() 460 | }) 461 | }) 462 | }) 463 | 464 | afterEach(function () { 465 | proxyServer.close() 466 | targetServer.close() 467 | }) 468 | 469 | it('should add `x-added` as custom header to response"', function () { 470 | expect(response.headers['x-added']).to.equal('foobar') 471 | }) 472 | 473 | it('should remove `x-removed` field from response header"', function () { 474 | expect(response.headers['x-removed']).to.equal(undefined) 475 | }) 476 | }) 477 | 478 | describe('option.onProxyReq', function () { 479 | var proxyServer, targetServer 480 | var receivedRequest 481 | 482 | beforeEach(function (done) { 483 | var fnOnProxyReq = function (proxyReq, req, res) { 484 | proxyReq.setHeader('x-added', 'foobar') // add custom header to request 485 | } 486 | 487 | var mwProxy = proxyMiddleware('/api', { 488 | target: 'http://localhost:8000', 489 | onProxyReq: fnOnProxyReq 490 | }) 491 | 492 | var mwTarget = function (req, res, next) { 493 | receivedRequest = req 494 | res.write(req.url) // respond with req.url 495 | res.end() 496 | } 497 | 498 | proxyServer = createServer(3000, mwProxy) 499 | targetServer = createServer(8000, mwTarget) 500 | 501 | http.get('http://localhost:3000/api/foo/bar', function () { 502 | done() 503 | }) 504 | }) 505 | 506 | afterEach(function () { 507 | proxyServer.close() 508 | targetServer.close() 509 | }) 510 | 511 | it('should add `x-added` as custom header to request"', function () { 512 | expect(receivedRequest.headers['x-added']).to.equal('foobar') 513 | }) 514 | }) 515 | 516 | describe('option.pathRewrite', function () { 517 | var proxyServer, targetServer 518 | var responseBody 519 | 520 | beforeEach(function (done) { 521 | var mwProxy = proxyMiddleware('/api', { 522 | target: 'http://localhost:8000', 523 | pathRewrite: { 524 | '^/api': '/rest', 525 | '^/remove': '' 526 | } 527 | }) 528 | var mwTarget = function (req, res, next) { 529 | res.write(req.url) // respond with req.url 530 | res.end() 531 | } 532 | 533 | proxyServer = createServer(3000, mwProxy) 534 | targetServer = createServer(8000, mwTarget) 535 | 536 | http.get('http://localhost:3000/api/foo/bar', function (res) { 537 | res.on('data', function (chunk) { 538 | responseBody = chunk.toString() 539 | done() 540 | }) 541 | }) 542 | }) 543 | 544 | afterEach(function () { 545 | proxyServer.close() 546 | targetServer.close() 547 | }) 548 | 549 | it('should have rewritten path from "/api/foo/bar" to "/rest/foo/bar"', function () { 550 | expect(responseBody).to.equal('/rest/foo/bar') 551 | }) 552 | }) 553 | 554 | describe('shorthand usage', function () { 555 | var proxyServer, targetServer 556 | var responseBody 557 | 558 | beforeEach(function (done) { 559 | var mwProxy = proxyMiddleware('http://localhost:8000/api') 560 | var mwTarget = function (req, res, next) { 561 | res.write(req.url) // respond with req.url 562 | res.end() 563 | } 564 | 565 | proxyServer = createServer(3000, mwProxy) 566 | targetServer = createServer(8000, mwTarget) 567 | 568 | http.get('http://localhost:3000/api/foo/bar', function (res) { 569 | res.on('data', function (chunk) { 570 | responseBody = chunk.toString() 571 | done() 572 | }) 573 | }) 574 | }) 575 | 576 | afterEach(function () { 577 | proxyServer.close() 578 | targetServer.close() 579 | }) 580 | 581 | it('should have proxy with shorthand configuration', function () { 582 | expect(responseBody).to.equal('/api/foo/bar') 583 | }) 584 | }) 585 | 586 | describe('express with path + proxy', function () { 587 | var proxyServer, targetServer 588 | var responseBody 589 | 590 | beforeEach(function (done) { 591 | var mwProxy = proxyMiddleware('http://localhost:8000') 592 | var mwTarget = function (req, res, next) { 593 | res.write(req.url) // respond with req.url 594 | res.end() 595 | } 596 | 597 | proxyServer = createServer(3000, mwProxy, '/api') 598 | targetServer = createServer(8000, mwTarget) 599 | 600 | http.get('http://localhost:3000/api/foo/bar', function (res) { 601 | res.on('data', function (chunk) { 602 | responseBody = chunk.toString() 603 | done() 604 | }) 605 | }) 606 | }) 607 | 608 | afterEach(function () { 609 | proxyServer.close() 610 | targetServer.close() 611 | }) 612 | 613 | it('should proxy to target with the baseUrl', function () { 614 | expect(responseBody).to.equal('/api/foo/bar') 615 | }) 616 | }) 617 | 618 | describe('option.logLevel & option.logProvider', function () { 619 | var proxyServer, targetServer 620 | var logMessage 621 | 622 | beforeEach(function (done) { 623 | var customLogger = function (message) { 624 | logMessage = message 625 | } 626 | 627 | var mwProxy = proxyMiddleware('http://localhost:8000/api', { 628 | logLevel: 'info', 629 | logProvider: function (provider) { 630 | provider.debug = customLogger 631 | provider.info = customLogger 632 | return provider 633 | } 634 | }) 635 | var mwTarget = function (req, res, next) { 636 | res.write(req.url) // respond with req.url 637 | res.end() 638 | } 639 | 640 | proxyServer = createServer(3000, mwProxy) 641 | targetServer = createServer(8000, mwTarget) 642 | 643 | http.get('http://localhost:3000/api/foo/bar', function (res) { 644 | res.on('data', function (chunk) { 645 | done() 646 | }) 647 | }) 648 | }) 649 | 650 | afterEach(function () { 651 | proxyServer.close() 652 | targetServer.close() 653 | }) 654 | 655 | it('should have logged messages', function () { 656 | expect(logMessage).not.to.equal(undefined) 657 | }) 658 | }) 659 | }) 660 | }) 661 | --------------------------------------------------------------------------------