Skip to content

Commit

Permalink
Initial request replacement
Browse files Browse the repository at this point in the history
  • Loading branch information
Eran Hammer committed May 12, 2013
1 parent fafa52d commit f2ce1af
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 59 deletions.
2 changes: 0 additions & 2 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,6 @@ The following options are available when adding a route:
- `settings` - the proxy handler configuration.
- `res` - the node response object received from the upstream service.
- `payload` - the response payload.
- `httpClient` - an alternative HTTP client function, compatible with the [**request**](https://npmjs.org/package/request) module `request()`
interface.
<p></p>
- <a name="route.config.view"></a>`view` - generates a template-based response. The `view` options is set to the desired template file name.
The view context available to the template includes:
Expand Down
110 changes: 110 additions & 0 deletions lib/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Load modules

var Url = require('url');
var Http = require('http');
var Https = require('https');
var Stream = require('stream');
var Utils = require('./utils');
var Boom = require('boom');


// Declare internals

var internals = {};


// Create and configure server instance

exports.request = function (method, url, options, callback) {

var uri = Url.parse(url);
uri.method = method.toUpperCase();
uri.headers = options.headers;

var agent = (uri.protocol === 'https:' ? Https : Http);
var req = agent.request(uri);

var isFinished = false;
var finish = function (err, res) {

if (!isFinished) {
isFinished = true;

req.removeAllListeners();

return callback(err, res);
}
};

req.once('error', finish);

req.once('response', function (res) {

// res.statusCode
// res.headers
// res Readable stream

return finish(null, res);
});

if (uri.method !== 'GET' &&
uri.method !== 'HEAD' &&
options.payload !== null &&
options.payload !== undefined) { // Value can be falsey

if (options.payload instanceof Stream) {
options.payload.pipe(req);
return;
}

req.write(options.payload);
}

req.end();
};


exports.parse = function (res, callback) {

var Writer = function () {

Stream.Writable.call(this);
this.buffers = [];
this.length = 0;

return this;
};

Utils.inherits(Writer, Stream.Writable);

Writer.prototype._write = function (chunk, encoding, next) {

this.legnth += chunk.length;
this.buffers.push(chunk);
next();
};

var isFinished = false;
var finish = function (err, buffer) {

if (!isFinished) {
isFinished = true;

writer.removeAllListeners();
res.removeAllListeners();

return callback(err || (buffer ? null : Boom.internal('Client request closed')), buffer);

This comment has been minimized.

Copy link
@iamdoron

iamdoron May 12, 2013

Contributor

Are you planning on writing unit tests for client.js ?
It's hard to know if this line is fully covered via integration tests of proxy.js.

If so, https://github.com/thlorenz/proxyquire is an interesting way to stub or mock the http require

This comment has been minimized.

Copy link
@hueniverse

hueniverse May 12, 2013

Contributor

The goal is to break a lot of these into smaller statements for proper coverage. Not sure on timing. But in general, this code is pretty simple.

}
};

var writer = new Writer();
writer.once('finish', function () {

return finish(null, Buffer.concat(writer.buffers, writer.length));
});

res.once('error', finish);
res.once('close', finish);

res.pipe(writer);
};
70 changes: 32 additions & 38 deletions lib/proxy.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Load modules

var Request = require('request');
var Utils = require('./utils');
var Boom = require('boom');
var Client = require('./client');
var Utils = require('./utils');


// Declare internals
Expand Down Expand Up @@ -32,9 +32,6 @@ exports = module.exports = internals.Proxy = function (options, route) {
};


internals.Proxy.prototype.httpClient = Request;


internals.Proxy.prototype.handler = function () {

var self = this;
Expand All @@ -50,10 +47,8 @@ internals.Proxy.prototype.handler = function () {
var req = request.raw.req;

var options = {
uri: uri,
method: request.method,
headers: {},
jar: false
payload: null
};

if (self.settings.passThrough) { // Never set with cache
Expand All @@ -71,46 +66,45 @@ internals.Proxy.prototype.handler = function () {
options.headers['x-forwarded-proto'] = (options.headers['x-forwarded-proto'] ? options.headers['x-forwarded-proto'] + ',' : '') + self.settings.protocol;
}

var isGet = (request.method === 'get' || request.method === 'head');

// Parsed payload interface

if (self.settings.isCustomPostResponse || // Custom response method
(isGet && request.route.cache.mode.server)) { // GET/HEAD with Cache

delete options.headers['accept-encoding']; // Remove until Request supports unzip/deflate
self.httpClient(options, function (err, res, payload) {

// Request handles all redirect responses (3xx) and will return an err if redirection fails

if (err) {
return request.reply(Boom.internal('Proxy error', err));
}

return self.settings.postResponse(request, self.settings, res, payload);
});

return;
var isParsed = (self.settings.isCustomPostResponse || request.route.cache.mode.server);
if (isParsed) {
delete options.headers['accept-encoding'];
}

// Streamed payload interface
// Set payload

if (!isGet &&
request.rawPayload &&
if (request.rawPayload &&
request.rawPayload.length) {

options.headers['Content-Type'] = req.headers['content-type'];
options.body = request.rawPayload;
options.payload = request.rawPayload;
}
else {
options.payload = request.raw.req;
}

var reqStream = self.httpClient(options);
reqStream.once('response', request.reply); // Request._respond will pass-through headers and status code
// Send request

if (!isGet &&
!request.rawPayload) {
Client.request(request.method, uri, options, function (err, res) {

request.raw.req.pipe(reqStream);
}
if (err) {
console.log(err);
return request.reply(Boom.internal('Proxy error', err));
}

if (!isParsed) {
return request.reply(res); // Request._respond will pass-through headers and status code
}

Client.parse(res, function (err, buffer) {

if (err) {
return request.reply(err);
}

return self.settings.postResponse(request, self.settings, res, buffer);
});
});
});
};
};
Expand Down
21 changes: 2 additions & 19 deletions test/integration/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,8 @@ describe('Proxy', function () {
{ method: 'GET', path: '/noHeaders', handler: { proxy: { host: 'localhost', port: backendPort } } },
{ method: 'GET', path: '/gzip', handler: { proxy: { host: 'localhost', port: backendPort, passThrough: true } } },
{ method: 'GET', path: '/gzipstream', handler: { proxy: { host: 'localhost', port: backendPort, passThrough: true } } },
{ method: 'GET', path: '/google', handler: { proxy: { mapUri: function (request, callback) { callback(null, 'http://google.com'); } } } }
]);
{ method: 'GET', path: '/google', handler: { proxy: { mapUri: function (request, callback) { callback(null, 'http://www.google.com'); } } } }
]);

server.state('auto', { autoValue: 'xyz' });
server.start(function () {
Expand Down Expand Up @@ -324,23 +324,6 @@ describe('Proxy', function () {
});
});

it('handles an error from request safely', function (done) {

var requestStub = function (options, callback) {

callback(new Error());
};

var route = server._router.route({ method: 'get', path: '/proxyerror', info: {}, raw: { req: { headers: {} } } });
route.proxy.httpClient = requestStub;

makeRequest({ path: '/proxyerror', method: 'get' }, function (rawRes) {

expect(rawRes.statusCode).to.equal(500);
done();
});
});

it('forwards on a POST body', function (done) {

makeRequest({ path: '/echo', method: 'post', form: { echo: true } }, function (rawRes) {
Expand Down

0 comments on commit f2ce1af

Please sign in to comment.