Skip to content

Commit

Permalink
Added support for HTTP/2 request (#1427)
Browse files Browse the repository at this point in the history
  • Loading branch information
parthverma1 authored Jul 30, 2024
1 parent 54dc943 commit 1668b04
Show file tree
Hide file tree
Showing 8 changed files with 373 additions and 6 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ runner.run(collection, {
// Maximum allowed response size in bytes (only supported on Node, ignored in the browser)
maxResponseSize: 1000000,

// HTTP Protocol version to use. Valid options are http1, http2, and auto (only supported on Node, ignored in the browser)
protocolVersion: 'http1',

// Enable to use WHATWG URL parser and encoder
useWhatWGUrlParser: true,

Expand Down
3 changes: 3 additions & 0 deletions docs/protocol-profile-behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ Redirect with the original HTTP method, by default redirects with HTTP method GE
- `followAuthorizationHeader: Boolean`<br/>
Retain `authorization` header when a redirect happens to a different hostname.

- `protocolVersion: String`<br/>
HTTP Protocol version to be used, supported values are 'http1', 'http2', and 'auto'

- `removeRefererHeaderOnRedirect: Boolean`<br/>
Removes the `referer` header when a redirect happens.

Expand Down
6 changes: 5 additions & 1 deletion lib/requester/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ var dns = require('dns'),

// removes the `referer` header when a redirect happens (default: false)
// @note `referer` header set in the initial request will be preserved during redirect chain
removeRefererHeader: 'removeRefererHeaderOnRedirect'
removeRefererHeader: 'removeRefererHeaderOnRedirect',

// Select the HTTP protocol version to be used. Valid options are http1/http2/auto
protocolVersion: 'protocolVersion'
},

/**
Expand Down Expand Up @@ -399,6 +402,7 @@ module.exports = {
* @param defaultOpts.followOriginalHttpMethod
* @param defaultOpts.maxRedirects
* @param defaultOpts.maxResponseSize
* @param defaultOpts.protocolVersion
* @param defaultOpts.implicitCacheControl
* @param defaultOpts.implicitTraceHeader
* @param defaultOpts.removeRefererHeaderOnRedirect
Expand Down
1 change: 1 addition & 0 deletions lib/requester/requester-pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ RequesterPool = function (options, callback) {
cookieJar: _.get(options, 'requester.cookieJar'), // default set later in this constructor
strictSSL: _.get(options, 'requester.strictSSL'),
maxResponseSize: _.get(options, 'requester.maxResponseSize'),
protocolVersion: _.get(options, 'requester.protocolVersion'),
// @todo drop support in v8
useWhatWGUrlParser: _.get(options, 'requester.useWhatWGUrlParser', false),
insecureHTTPParser: _.get(options, 'requester.insecureHTTPParser'),
Expand Down
38 changes: 37 additions & 1 deletion test/fixtures/servers/_servers.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const fs = require('fs'),
path = require('path'),
http = require('http'),
https = require('https'),
http2 = require('http2'),
crypto = require('crypto'),
GraphQL = require('graphql'),
express = require('express'),
Expand Down Expand Up @@ -684,6 +685,40 @@ function createDigestServer (options) {
return app;
}

function createHTTP2Server (opts) {
var server,
certDataPath = path.join(__dirname, '../certificates'),
options = {
key: path.join(certDataPath, 'server-key.pem'),
cert: path.join(certDataPath, 'server-crt.pem'),
ca: path.join(certDataPath, 'ca.pem')
},
optionsWithFilePath = ['key', 'cert', 'ca', 'pfx'];

if (opts) {
options = Object.assign(options, opts);
}

optionsWithFilePath.forEach(function (option) {
if (!options[option]) { return; }

options[option] = fs.readFileSync(options[option]);
});

server = http2.createSecureServer(options, function (req, res) {
server.emit(req.url, req, res);
});

server.on('listening', function () {
server.port = this.address().port;
server.url = 'https://localhost:' + server.port;
});

enableServerDestroy(server);

return server;
}

module.exports = {
createSSLServer,
createHTTPServer,
Expand All @@ -694,5 +729,6 @@ module.exports = {
createEdgeGridAuthServer,
createNTLMServer,
createBytesServer,
createDigestServer
createDigestServer,
createHTTP2Server
};
152 changes: 152 additions & 0 deletions test/integration/protocol-profile-behavior/protocolVersion.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
var sinon = require('sinon'),
expect = require('chai').expect,
IS_NODE = typeof window === 'undefined',
IS_NODE_20 = IS_NODE && parseInt(process.versions.node.split('.')[0], 10) === 20,
server = IS_NODE && require('../../fixtures/servers/_servers');

(IS_NODE ? describe : describe.skip)('Requester Spec: protocolVersion', function () {
var protocolVersions = [undefined, 'http1', 'http2', 'auto'],
servers = {
http1: null,
http2: null
},
requestHandler = function (req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('okay');
},
forInAsync = function (obj, fn, cb) {
if (!(obj && fn)) { return; }
!cb && (cb = function () { /* (ಠ_ಠ) */ });

var index = 0,
keys = Object.keys(obj),
next = function (err) {
if (err || index >= keys.length) {
return cb(err);
}

fn.call(obj, keys[index++], next);
};

if (!keys.length) {
return cb();
}

next();
};

before(function (done) {
servers = {
http1: server.createSSLServer(),
http2: server.createHTTP2Server()
};

forInAsync(servers, function (protocol, next) {
servers[protocol].on('/test', requestHandler);
servers[protocol].listen(0, next);
}, done);
});

after(function (done) {
forInAsync(servers, function (protocol, next) {
servers[protocol].destroy(next);
}, done);
});

protocolVersions.forEach((protocolVersion) => {
describe('HTTP/1.1 Server with item protocolVersion ' + protocolVersion, function () {
var testrun;

before(function (done) {
this.run({
requester: {
strictSSL: false
},
collection: {
item: [{
request: {
url: 'https://localhost:' + servers.http1.port + '/test'
},
protocolProfileBehavior: {
protocolVersion
}
}]
}
}, function (err, results) {
testrun = results;
done(err);
});
});

it('should complete the run', function () {
expect(testrun).to.be.ok;
sinon.assert.calledOnce(testrun.start);
sinon.assert.calledOnce(testrun.done);
sinon.assert.calledWith(testrun.done.getCall(0), null);
});

it('should use the requested protocol', function () {
var history = testrun.response.getCall(0).lastArg,
executionData = history.execution.data[0];

if (protocolVersion === 'http2') {
const error = testrun.response.getCall(0).firstArg;

expect(error.code).to.eql(IS_NODE_20 ? 'ERR_HTTP2_STREAM_CANCEL' : 'ERR_HTTP2_ERROR');
!IS_NODE_20 && expect(error.errno).to.eql(-505);

return;
}
expect(executionData.response.httpVersion).to.eql('1.1');
});
});
});

protocolVersions.forEach((protocolVersion) => {
describe('HTTP/2 Server with item protocolVersion ' + protocolVersion, function () {
var testrun;

before(function (done) {
this.run({
requester: {
strictSSL: false
},
collection: {
item: [{
request: {
url: 'https://localhost:' + servers.http2.port + '/test'
},
protocolProfileBehavior: {
protocolVersion
}
}]
}
}, function (err, results) {
testrun = results;
done(err);
});
});

it('should complete the run', function () {
expect(testrun).to.be.ok;
sinon.assert.calledOnce(testrun.start);
sinon.assert.calledOnce(testrun.done);
sinon.assert.calledWith(testrun.done.getCall(0), null);
});

it('should use the requested protocol', function () {
var history = testrun.response.getCall(0).lastArg,
executionData = history.execution.data[0],
response = testrun.response.getCall(0).args[2];

if (protocolVersion === 'http1' || !protocolVersion) {
expect(executionData.response.httpVersion).to.eql('1.0');
expect(response).to.have.property('code', 403);

return;
}
expect(executionData.response.httpVersion).to.eql('2.0');
});
});
});
});
12 changes: 8 additions & 4 deletions test/integration/request-flow/pm-send-request-cookies.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,10 @@ var _ = require('lodash'),
headers = historyOne.execution.data[1].request.headers;

// cookies are set after the first response in redirect
expect(headers[headers.length - 1]).to.have.property('key', 'Cookie');
expect(headers[headers.length - 1].value).to.not.include('foo=bar');
const cookieHeaderIndex = headers.findIndex((header) => { return header.key === 'Cookie'; });

expect(cookieHeaderIndex).to.be.greaterThan(-1);
expect(headers[cookieHeaderIndex].value).to.not.include('foo=bar');

expect(resOne.json()).to.eql({ cookies: {} });
expect(testrun.request.secondCall.args[2].json()).to.eql({ cookies: { foo: 'bar' } });
Expand Down Expand Up @@ -491,8 +493,10 @@ var _ = require('lodash'),
historyOne = testrun.response.firstCall.lastArg,
headers = historyOne.execution.data[1].request.headers;

expect(headers[headers.length - 1]).to.have.property('key', 'Cookie');
expect(headers[headers.length - 1].value).to.include('foo=bar;');
const cookieHeaderIndex = headers.findIndex((header) => { return header.key === 'Cookie'; });

expect(cookieHeaderIndex).to.be.greaterThan(-1);
expect(headers[cookieHeaderIndex].value).to.include('foo=bar;');

expect(!_.includes(_.get(resOne, 'headers.reference.set-cookie.value', ''), 'foo=bar;')).to
.be.true;
Expand Down
Loading

0 comments on commit 1668b04

Please sign in to comment.