Skip to content

feat(proxy): allow contextless proxy configuration #70

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 18, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [v0.x.x](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.x.x)
- feat(proxy): support proxy creation without context.
- fix(connect mounting): use connect's `path` configuration to mount proxy.

## [v0.13.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v0.13.0)
- feat(context): custom context matcher; when simple `path` matching is not sufficient.

Expand Down
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ $ npm install --save-dev http-proxy-middleware
## Core concept

Configure the proxy middleware.

```javascript
var proxy = require('http-proxy-middleware');

Expand Down Expand Up @@ -150,6 +151,19 @@ proxy('http://www.example.org:8000/api', {changeOrigin:true});
// proxy('/api', {target: 'http://www.example.org:8000', changeOrigin: true});
```

### app.use(path, proxy)

If you want to use the server's `app.use` `path` parameter to match requests;

Create and mount the proxy without the http-proxy-middleware `context` parameter:
```javascript
app.use('/api', proxy({target:'http://www.example.org', changeOrigin:true}));
```

`app.use` documentation:
* express: http://expressjs.com/en/4x/api.html#app.use
* connect: https://github.com/senchalabs/connect#mount-middleware

## WebSocket

```javascript
Expand Down Expand Up @@ -227,6 +241,9 @@ var server = app.listen(3000);
}
```

### Events
Subscribe to [http-proxy events](https://github.com/nodejitsu/node-http-proxy#listening-for-proxy-events):

* **option.onError**: function, subscribe to http-proxy's `error` event for custom error handling.
```javascript
function onError(err, req, res) {
Expand Down Expand Up @@ -280,7 +297,9 @@ var server = app.listen(3000);

* (DEPRECATED) **option.proxyHost**: Use `option.changeOrigin = true` instead.

The following options are provided by the underlying [http-proxy](https://www.npmjs.com/package/http-proxy).
### http-proxy options

The following options are provided by the underlying [http-proxy](https://github.com/nodejitsu/node-http-proxy#options).

* **option.target**: url string to be parsed with the url module
* **option.forward**: url string to be parsed with the url module
Expand Down
4 changes: 1 addition & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ var httpProxyMiddleware = function(context, opts) {

function middleware(req, res, next) {
// https://github.com/chimurai/http-proxy-middleware/issues/17
if (req.baseUrl) {
req.url = req.originalUrl;
}
req.url = req.originalUrl;

if (contextMatcher.match(config.context, req.url, req)) {
var activeProxyOptions = prepareProxyRequest(req);
Expand Down
43 changes: 37 additions & 6 deletions lib/config-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ function createConfig(context, opts) {
options: {}
};

var useShortHand = isShortHand(context);

if (useShortHand) {
// app.use('/api', proxy({target:'http://localhost:9000'}));
if (isContexless(context, opts)) {
config.context = '/';
config.options = _.assign(config.options, context);
}
// app.use('/api', proxy('http://localhost:9000'));
// app.use(proxy('http://localhost:9000/api'));
else if (isStringShortHand(context)) {
var oUrl = url.parse(context);
var target = [oUrl.protocol, '//', oUrl.host].join('');

Expand All @@ -25,7 +30,7 @@ function createConfig(context, opts) {
if (oUrl.protocol === 'ws:' || oUrl.protocol === 'wss:') {
config.options.ws = true;
}

// app.use('/api', proxy({target:'http://localhost:9000'}));
} else {
config.context = context;
config.options = _.assign(config.options, opts);
Expand All @@ -41,14 +46,40 @@ function createConfig(context, opts) {
config.options = mapLegacyProxyHostOption(config.options);

return config;
};
}

function isShortHand(context) {
/**
* Checks if a String only target/config is provided.
* This can be just the host or with the optional path.
*
* @example
* app.use('/api', proxy('http://localhost:9000'));
app.use(proxy('http://localhost:9000/api'));
*
* @param {String} context [description]
* @return {Boolean} [description]
*/
function isStringShortHand(context) {
if (_.isString(context)) {
return (url.parse(context).host) ? true : false;
}
}

/**
* Checks if a Object only config is provided, without a context.
* In this case the all paths will be proxied.
*
* @example
* app.use('/api', proxy({target:'http://localhost:9000'}));
*
* @param {Object} context [description]
* @param {*} opts [description]
* @return {Boolean} [description]
*/
function isContexless(context, opts) {
return (_.isPlainObject(context) && _.isEmpty(opts));
}

function mapLegacyProxyHostOption(options) {
// set options.headers.host when option.proxyHost is provided
if (options.proxyHost) {
Expand Down
20 changes: 20 additions & 0 deletions recipes/basic.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,23 @@ var apiProxy = proxy('/api', {target: 'http://localhost:3000'});
// | |
// context options
```

## Alternative configuration

The proxy behavior of the following examples are **exactly** the same; Just different ways to configure it.

```javascript
app.use(proxy('/api', {target: 'http://localhost:3000', changeOrigin:true}));
```

```javascript
app.use(proxy('http://localhost:3000/api', {changeOrigin:true}));
```

```javascript
app.use('/api', proxy('http://localhost:3000', {changeOrigin:true}));
```

```javascript
app.use('/api', proxy({target: 'http://localhost:3000', changeOrigin:true}));
```
129 changes: 73 additions & 56 deletions test/config-factory.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,111 +3,128 @@ var configFactory = require('../lib/config-factory');

describe('configFactory', function() {
var result;
var createConfig = configFactory.createConfig;

describe('createConfig()', function() {

describe('classic api', function() {
describe('classic config', function() {
var context = '/api';
var options = {target: 'http://www.example.org'};

beforeEach(function() {
result = configFactory.createConfig(context, options);
result = createConfig(context, options);
});

it('should return on config object', function() {
it('should return config object', function() {
expect(result).to.have.all.keys('context', 'options');
});

it('should return on config object with context', function() {
it('should return config object with context', function() {
expect(result.context).to.equal(context);
});

it('should return on config object with options', function() {
it('should return config object with options', function() {
expect(result.options).to.deep.equal(options);
});
});

describe('shorthand api', function() {
beforeEach(function() {
result = configFactory.createConfig('http://www.example.org:8000/api');
});
describe('shorthand String', function() {
describe('shorthand String config', function() {
beforeEach(function() {
result = createConfig('http://www.example.org:8000/api');
});

it('should return on config object', function() {
expect(result).to.have.all.keys('context', 'options');
});
it('should return config object', function() {
expect(result).to.have.all.keys('context', 'options');
});

it('should return on config object with context', function() {
expect(result.context).to.equal('/api');
});
it('should return config object with context', function() {
expect(result.context).to.equal('/api');
});

it('should return on config object with options', function() {
expect(result.options).to.deep.equal({target: 'http://www.example.org:8000'});
it('should return config object with options', function() {
expect(result.options).to.deep.equal({target: 'http://www.example.org:8000'});
});
});
});

describe('shorthand api for whole domain', function() {
beforeEach(function() {
result = configFactory.createConfig('http://www.example.org:8000');
});
describe('shorthand String config for whole domain', function() {
beforeEach(function() {
result = createConfig('http://www.example.org:8000');
});

it('should return on config object with context', function() {
expect(result.context).to.equal('/');
it('should return config object with context', function() {
expect(result.context).to.equal('/');
});
});
});

describe('shorthand api for websocket url', function() {
beforeEach(function() {
result = configFactory.createConfig('ws://www.example.org:8000');
});
describe('shorthand String config for websocket url', function() {
beforeEach(function() {
result = createConfig('ws://www.example.org:8000');
});

it('should return on config object with context', function() {
expect(result.context).to.equal('/');
});
it('should return config object with context', function() {
expect(result.context).to.equal('/');
});

it('should return on options with ws = true', function() {
expect(result.options.ws).to.equal(true);
it('should return options with ws = true', function() {
expect(result.options.ws).to.equal(true);
});
});
});

describe('shorthand api for secure websocket url', function() {
beforeEach(function() {
result = configFactory.createConfig('wss://www.example.org:8000');
});
describe('shorthand String config for secure websocket url', function() {
beforeEach(function() {
result = createConfig('wss://www.example.org:8000');
});

it('should return on config object with context', function() {
expect(result.context).to.equal('/');
});
it('should return config object with context', function() {
expect(result.context).to.equal('/');
});

it('should return on options with ws = true', function() {
expect(result.options.ws).to.equal(true);
it('should return options with ws = true', function() {
expect(result.options.ws).to.equal(true);
});
});
});

describe('shorthand api with globbing', function() {
beforeEach(function() {
result = configFactory.createConfig('http://www.example.org:8000/api/*.json');
describe('shorthand String config with globbing', function() {
beforeEach(function() {
result = createConfig('http://www.example.org:8000/api/*.json');
});

it('should return config object with context', function() {
expect(result.context).to.equal('/api/*.json');
});
});

it('should return on config object with context', function() {
expect(result.context).to.equal('/api/*.json');
describe('shorthand String config with options', function() {
beforeEach(function() {
result = createConfig('http://www.example.org:8000/api', {changeOrigin: true});
});

it('should return config object with additional options', function() {
expect(result.options).to.deep.equal({target: 'http://www.example.org:8000', changeOrigin: true});
});
});
});

describe('shorthand api with options', function() {
describe('shorthand Object config', function() {
beforeEach(function() {
result = configFactory.createConfig('http://www.example.org:8000/api', {changeOrigin: true});
result = createConfig({target: 'http://www.example.org:8000'});
});

it('should return on config object with additional options', function() {
expect(result.options).to.deep.equal({target: 'http://www.example.org:8000', changeOrigin: true});
it('should set the proxy path to everything', function() {
expect(result.context).to.equal('/');
});

it('should return config object', function() {
expect(result.options).to.deep.equal({target: 'http://www.example.org:8000'});
});
});

describe('missing option.target', function() {
var fn;
beforeEach(function() {
fn = function() {
configFactory.createConfig('/api');
createConfig('/api');
};
});

Expand All @@ -119,7 +136,7 @@ describe('configFactory', function() {
describe('faulty config. mixing classic with shorthand', function() {
var fn;
beforeEach(function() {
result = configFactory.createConfig('http://localhost:3000/api', {target: 'http://localhost:8000'});
result = createConfig('http://localhost:3000/api', {target: 'http://localhost:8000'});
});

it('should use the target in the configuration as target', function() {
Expand Down
2 changes: 1 addition & 1 deletion test/http-proxy-middleware.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('context matching', function() {

var middleware;

var mockReq = {url: '/foo/bar'};
var mockReq = {url: '/foo/bar', originalUrl: '/foo/bar'};
var mockRes = {};
var mockNext = function() {
// mockNext will be called when request is not proxied
Expand Down