diff --git a/lib/requester/request-wrapper.js b/lib/requester/request-wrapper.js index 29c39e141..31b57fd13 100644 --- a/lib/requester/request-wrapper.js +++ b/lib/requester/request-wrapper.js @@ -63,7 +63,7 @@ var _ = require('lodash'), cb(null, options); }; -module.exports = function (request, options, onStart, onData, callback) { +function requestWrapper (request, options, onStart, onData, callback) { var req = {}; async.waterfall([ @@ -89,4 +89,7 @@ module.exports = function (request, options, onStart, onData, callback) { }); return req; -}; +} + +module.exports = requestWrapper; +module.exports.request = requestWrapper; diff --git a/lib/requester/requester.js b/lib/requester/requester.js index da1710ecb..bb44a233c 100644 --- a/lib/requester/requester.js +++ b/lib/requester/requester.js @@ -4,7 +4,7 @@ var _ = require('lodash'), EventEmitter = require('events'), now = require('performance-now'), sdk = require('postman-collection'), - requests = require('./request-wrapper'), + requestWrapperModule = require('./request-wrapper'), dryRun = require('./dry-run'), SSEProcessor = require('./sse-processor'), @@ -435,13 +435,15 @@ class Requester extends EventEmitter { return onEnd(new Error(ERROR_RESTRICTED_ADDRESS + hostname)); } + const requests = requestWrapperModule.request || requestWrapperModule; + return requests(request, requestOptions, onStart, onData, function (err, res, resBody, debug) { // prepare history from request debug data var history = getExecutionHistory(debug), responseTime, response; - if (err) { + if (err && !err.res) { // bubble up http errors // @todo - Should we send an empty sdk Response here? // @@ -449,6 +451,15 @@ class Requester extends EventEmitter { return onEnd(err, undefined, history); } + // If the error has a response attached, use it to populate the response instead of treating + // it as a crash. + if (err && err.res) { + res = err.res; + + if (Buffer.isBuffer(err.res.body)) { + resBody = err.res.body; + } + } // Calculate the time taken for us to get the response. responseTime = Date.now() - startTime; diff --git a/test/integration/sanity/error-with-response.test.js b/test/integration/sanity/error-with-response.test.js new file mode 100644 index 000000000..ef3dbb441 --- /dev/null +++ b/test/integration/sanity/error-with-response.test.js @@ -0,0 +1,82 @@ +const sdk = require('postman-collection'), + sinon = require('sinon').createSandbox(), + requestWrapper = require('../../../lib/requester/request-wrapper'); + +(typeof window === 'undefined' ? describe : describe.skip)('handling of error with response', function () { + before(function (done) { + const fakeRequester = sinon.fake((_request, _requestOptions, onStart, _onData, callback) => { + const error = new Error('test error'), + mockResponse = { + status: 407, + statusMessage: 'Proxy Authentication Required', + body: Buffer.from('Proxy Authentication Required - Response Body from Proxy'), + headers: [], + request: { + _debug: [{ + request: { + method: 'GET', + href: 'https://postman-echo.com/get', + headers: [], + httpVersion: '1.1' + }, + response: { + downloadedBytes: 100 + } + }] + } + }; + + mockResponse.getAllResponseHeaders = () => { + return mockResponse.headers; + }; + + error.res = mockResponse; + + onStart(error.res); // Calling of onStart via the 'response' event is handled by the postman-request library + callback(error, null, null, mockResponse.request._debug); + }); + + sinon.stub(requestWrapper, 'request').callsFake(fakeRequester); + + done(); + }); + + after(function () { + sinon.restore(); + }); + + it('should handle error with an attached response', function (done) { + this.run({ collection: { item: { request: 'https://postman-echo.com/get' } } }, function (err, results) { + if (err) { + return done(err); + } + + const testrun = results; + + sinon.assert.calledOnce(testrun.responseStart); + sinon.assert.calledOnce(testrun.response); + sinon.assert.calledOnce(testrun.done); + + // Even though the callback to runtime returned an error, we handle the response attached to it + // and don't consider it as a crash. + + let responseStartCalledWith = testrun.responseStart.getCall(0).args, + responseCalledWith = testrun.response.getCall(0).args, + doneCalledWith = testrun.done.getCall(0).args; + + expect(responseStartCalledWith[0]).to.be.equal(null); // Not an error + expect(responseStartCalledWith[2]).to.be.instanceOf(sdk.Response); + expect(responseStartCalledWith[2].code).to.be.equal(407); + + expect(responseCalledWith[0]).to.be.equal(null); // Not an error + expect(responseCalledWith[2]).to.be.instanceOf(sdk.Response); + expect(responseCalledWith[2].text()).to.be.equal('Proxy Authentication Required' + + ' - Response Body from Proxy'); + expect(responseCalledWith[2].code).to.be.equal(407); + + expect(doneCalledWith[0]).to.be.equal(null); // Not an error + + done(); + }); + }); +}); diff --git a/test/integration/sanity/proxy-tunnel-with-error.test.js b/test/integration/sanity/proxy-tunnel-with-error.test.js new file mode 100644 index 000000000..cd696a216 --- /dev/null +++ b/test/integration/sanity/proxy-tunnel-with-error.test.js @@ -0,0 +1,60 @@ +var ProxyConfigList = require('postman-collection').ProxyConfigList, + expect = require('chai').expect; + +(typeof window === 'undefined' ? describe : describe.skip)('proxy with tunnel', function () { + var testrun; + + describe('proxy tunnel with invalid auth', function () { + var proxyHost, + proxyPort, + proxyList, + auth = { + username: 'random-user', + password: 'wrong-password' + }; + + before(function (done) { + proxyHost = 'localhost'; + proxyPort = global.servers.proxyAuth.split(':')[2]; + proxyList = new ProxyConfigList({}, [{ + match: '*://postman-echo.com/*', + host: proxyHost, + port: proxyPort, + authenticate: true, + username: auth.username, + password: auth.password + }]); + + this.run({ + collection: { + item: { + request: 'https://postman-echo.com/get' + } + }, + proxies: proxyList + }, function (err, results) { + testrun = results; + done(err); + }); + }); + + it('should have started and completed the test run', function () { + expect(testrun).to.be.ok; + expect(testrun).to.nested.include({ + 'done.calledOnce': true, + 'start.calledOnce': true + }); + }); + + it('should receive response from the proxy', function () { + var response = testrun.request.getCall(0).args[2], + request = testrun.request.getCall(0).args[3]; + + expect(testrun.request.calledOnce).to.be.ok; + expect(request.proxy.getProxyUrl()) + .to.eql(`http://${auth.username}:${auth.password}@${proxyHost}:${proxyPort}`); + expect(response.reason()).to.eql('Proxy Authentication Required'); + expect(response.text()).to.equal('Proxy Authentication Required'); + }); + }); +});