Skip to content

Commit

Permalink
HTTP/2 socket reuse
Browse files Browse the repository at this point in the history
  • Loading branch information
szmarczak committed Sep 25, 2020
1 parent 28a45ca commit 27d437a
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 40 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@
"xo": "^0.32.1"
},
"ava": {
"timeout": "2m"
"timeout": "2s"
}
}
94 changes: 55 additions & 39 deletions source/auto.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const http = require('http');
const https = require('https');
const resolveALPN = require('resolve-alpn');
const QuickLRU = require('quick-lru');
const {Agent, globalAgent} = require('./agent');
const Http2ClientRequest = require('./client-request');
const calculateServerName = require('./utils/calculate-server-name');
const urlToOptions = require('./utils/url-to-options');
Expand Down Expand Up @@ -46,53 +47,37 @@ const resolveProtocol = async options => {
if (!cache.has(name)) {
if (queue.has(name)) {
const result = await queue.get(name);
return result.alpnProtocol;
return {alpnProtocol: result.alpnProtocol};
}

const {path, agent} = options;
const {path} = options;
options.path = options.socketPath;
options.resolveSocket = true;

const resultPromise = resolveALPN(options);
queue.set(name, resultPromise);

try {
const {socket, alpnProtocol} = await resultPromise;
cache.set(name, alpnProtocol);

options.path = path;
const result = await resultPromise;

if (alpnProtocol === 'h2') {
// https://github.com/nodejs/node/issues/33343
socket.destroy();
} else {
const {globalAgent} = https;
const defaultCreateConnection = https.Agent.prototype.createConnection;

if (agent) {
if (agent.createConnection === defaultCreateConnection) {
installSocket(agent, socket, options);
} else {
socket.destroy();
}
} else if (globalAgent.createConnection === defaultCreateConnection) {
installSocket(globalAgent, socket, options);
} else {
socket.destroy();
}
}
// https://github.com/nodejs/node/pull/34859
result.socket.secureConnecting = false;

cache.set(name, result.alpnProtocol);
queue.delete(name);

return alpnProtocol;
options.path = path;

return result;
} catch (error) {
queue.delete(name);

options.path = path;

throw error;
}
}

return cache.get(name);
return {alpnProtocol: cache.get(name)};
};

module.exports = async (input, options, callback) => {
Expand Down Expand Up @@ -124,26 +109,57 @@ module.exports = async (input, options, callback) => {
options.port = options.port || (isHttps ? 443 : 80);
options._defaultAgent = isHttps ? https.globalAgent : http.globalAgent;

const agents = options.agent;

if (agents) {
if (agents.addRequest) {
throw new Error('The `options.agent` object can contain only `http`, `https` or `http2` properties');
}
let {agent} = options;

options.agent = agents[isHttps ? 'https' : 'http'];
// We don't support `false` agents
if (agent !== false && agent !== undefined && typeof agent !== 'object') {
throw new Error('The `options.agent` can be only an object `http`, `https` or `http2` properties');
}

if (isHttps) {
const protocol = await resolveProtocol(options);
options.resolveSocket = true;

let {socket, alpnProtocol} = await resolveProtocol(options);

if (socket && options.createConnection) {
socket.destroy();
socket = undefined;
}

delete options.resolveSocket;

const isH2 = alpnProtocol === 'h2';

if (agent) {
agent = isH2 ? agent.http2 : agent.https;
options.agent = agent;
} else if (agent === undefined) {
agent = isH2 ? globalAgent : https.globalAgent;
}

if (socket) {
if (agent === false) {
socket.destroy();
} else {
const defaultCreateConnection = (isH2 ? Agent : https.Agent).prototype.createConnection;

if (protocol === 'h2') {
if (agents) {
options.agent = agents.http2;
if (agent.createConnection === defaultCreateConnection) {
if (isH2) {
options.createConnection = () => socket;
} else {
installSocket(agent, socket, options);
}
} else {
socket.destroy();
}
}
}

if (isH2) {
return new Http2ClientRequest(options, callback);
}
} else if (agent) {
options.agent = agent.http;
}

return http.request(options, callback);
Expand Down
37 changes: 37 additions & 0 deletions test/auto.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,43 @@ test('invalid `agent` option', async t => {
}));
});

test.serial('reuses HTTP/2 TLS sockets', async t => {
http2.auto.protocolCache.clear();

const agent = new http2.Agent();

let counter = 0;

tls._connect = tls.connect;
tls.connect = (...args) => {
counter++;
return tls._connect(...args);
};

const options = {
agent: {
http2: agent
},
ALPNProtocols: ['h2']
};

const request = await http2.auto(h2s.url, options);
request.end();

const response = await pEvent(request, 'response');
const body = await getStream(response);

t.is(body, 'h2');

tls.connect = tls._connect;
delete tls._connect;

agent.destroy();

t.is(counter, 1);
t.pass();
});

test.serial('reuses HTTP/1.1 TLS sockets', async t => {
http2.auto.protocolCache.clear();

Expand Down

0 comments on commit 27d437a

Please sign in to comment.