http: use Keep-Alive by default in global agents

PR-URL: https://github.com/nodejs/node/pull/43522
Fixes: https://github.com/nodejs/node/issues/37184
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
Paolo Insogna 2022-06-21 14:50:55 +02:00
parent 8e19dab677
commit 4267b92604
27 changed files with 173 additions and 35 deletions

View file

@ -1449,11 +1449,20 @@ type other than {net.Socket}.
<!-- YAML <!-- YAML
added: v0.1.90 added: v0.1.90
changes:
- version:
- REPLACEME
pr-url: https://github.com/nodejs/node/pull/43522
description: The method closes idle connections before returning.
--> -->
* `callback` {Function} * `callback` {Function}
Stops the server from accepting new connections. See [`net.Server.close()`][]. Stops the server from accepting new connections and closes all connections
connected to this server which are not sending a request or waiting for
a response.
See [`net.Server.close()`][].
### `server.closeAllConnections()` ### `server.closeAllConnections()`
@ -3214,6 +3223,11 @@ server.listen(8000);
<!-- YAML <!-- YAML
added: v0.5.9 added: v0.5.9
changes:
- version:
- REPLACEME
pr-url: https://github.com/nodejs/node/pull/43522
description: The agent now uses HTTP Keep-Alive by default.
--> -->
* {http.Agent} * {http.Agent}

View file

@ -309,6 +309,11 @@ https.get('https://encrypted.google.com/', (res) => {
<!-- YAML <!-- YAML
added: v0.5.9 added: v0.5.9
changes:
- version:
- REPLACEME
pr-url: https://github.com/nodejs/node/pull/43522
description: The agent now uses HTTP Keep-Alive by default.
--> -->
Global instance of [`https.Agent`][] for all HTTPS client requests. Global instance of [`https.Agent`][] for all HTTPS client requests.

View file

@ -31,10 +31,12 @@ const {
ArrayPrototypeSplice, ArrayPrototypeSplice,
FunctionPrototypeCall, FunctionPrototypeCall,
NumberIsNaN, NumberIsNaN,
NumberParseInt,
ObjectCreate, ObjectCreate,
ObjectKeys, ObjectKeys,
ObjectSetPrototypeOf, ObjectSetPrototypeOf,
ObjectValues, ObjectValues,
RegExpPrototypeExec,
StringPrototypeIndexOf, StringPrototypeIndexOf,
StringPrototypeSplit, StringPrototypeSplit,
StringPrototypeStartsWith, StringPrototypeStartsWith,
@ -492,7 +494,24 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
socket.setKeepAlive(true, this.keepAliveMsecs); socket.setKeepAlive(true, this.keepAliveMsecs);
socket.unref(); socket.unref();
const agentTimeout = this.options.timeout || 0; let agentTimeout = this.options.timeout || 0;
if (socket._httpMessage?.res) {
const keepAliveHint = socket._httpMessage.res.headers['keep-alive'];
if (keepAliveHint) {
const hint = RegExpPrototypeExec(/^timeout=(\d+)/, keepAliveHint)?.[1];
if (hint) {
const serverHintTimeout = NumberParseInt(hint) * 1000;
if (serverHintTimeout < agentTimeout) {
agentTimeout = serverHintTimeout;
}
}
}
}
if (socket.timeout !== agentTimeout) { if (socket.timeout !== agentTimeout) {
socket.setTimeout(agentTimeout); socket.setTimeout(agentTimeout);
} }
@ -542,5 +561,5 @@ function asyncResetHandle(socket) {
module.exports = { module.exports = {
Agent, Agent,
globalAgent: new Agent() globalAgent: new Agent({ keepAlive: true, scheduling: 'lifo', timeout: 5000 })
}; };

View file

@ -478,6 +478,7 @@ ObjectSetPrototypeOf(Server.prototype, net.Server.prototype);
ObjectSetPrototypeOf(Server, net.Server); ObjectSetPrototypeOf(Server, net.Server);
Server.prototype.close = function() { Server.prototype.close = function() {
this.closeIdleConnections();
clearInterval(this[kConnectionsCheckingInterval]); clearInterval(this[kConnectionsCheckingInterval]);
ReflectApply(net.Server.prototype.close, this, arguments); ReflectApply(net.Server.prototype.close, this, arguments);
}; };

View file

@ -331,7 +331,7 @@ Agent.prototype._evictSession = function _evictSession(key) {
delete this._sessionCache.map[key]; delete this._sessionCache.map[key];
}; };
const globalAgent = new Agent(); const globalAgent = new Agent({ keepAlive: true, scheduling: 'lifo', timeout: 5000 });
/** /**
* Makes a request to a secure web server. * Makes a request to a secure web server.

View file

@ -12,6 +12,7 @@ const hooks = initHooks();
hooks.enable(); hooks.enable();
const server = http.createServer(common.mustCall((req, res) => { const server = http.createServer(common.mustCall((req, res) => {
res.writeHead(200, { 'Connection': 'close' });
res.end(); res.end();
server.close(common.mustCall()); server.close(common.mustCall());
})); }));

View file

@ -0,0 +1,24 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const server = http.createServer(function(req, res) {
res.writeHead(200);
res.end();
});
server.listen(0, common.mustCall(() => {
const req = http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.statusCode, 200);
res.resume();
server.close();
});
req.end();
}));
// This timer should never go off as the server will close the socket
setTimeout(common.mustNotCall(), 1000).unref();

View file

@ -27,6 +27,7 @@ const Countdown = require('../common/countdown');
let name; let name;
const max = 3; const max = 3;
const agent = new http.Agent();
const server = http.Server(common.mustCall((req, res) => { const server = http.Server(common.mustCall((req, res) => {
if (req.url === '/0') { if (req.url === '/0') {
@ -40,27 +41,28 @@ const server = http.Server(common.mustCall((req, res) => {
} }
}, max)); }, max));
server.listen(0, common.mustCall(() => { server.listen(0, common.mustCall(() => {
name = http.globalAgent.getName({ port: server.address().port }); name = agent.getName({ port: server.address().port });
for (let i = 0; i < max; ++i) for (let i = 0; i < max; ++i)
request(i); request(i);
})); }));
const countdown = new Countdown(max, () => { const countdown = new Countdown(max, () => {
assert(!(name in http.globalAgent.sockets)); assert(!(name in agent.sockets));
assert(!(name in http.globalAgent.requests)); assert(!(name in agent.requests));
server.close(); server.close();
}); });
function request(i) { function request(i) {
const req = http.get({ const req = http.get({
port: server.address().port, port: server.address().port,
path: `/${i}` path: `/${i}`,
agent
}, function(res) { }, function(res) {
const socket = req.socket; const socket = req.socket;
socket.on('close', common.mustCall(() => { socket.on('close', common.mustCall(() => {
countdown.dec(); countdown.dec();
if (countdown.remaining > 0) { if (countdown.remaining > 0) {
assert.strictEqual(http.globalAgent.sockets[name].includes(socket), assert.strictEqual(agent.sockets[name].includes(socket),
false); false);
} }
})); }));

View file

@ -0,0 +1,23 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const server = http.createServer(function(req, res) {
res.writeHead(200);
res.end();
});
server.listen(0, common.mustCall(() => {
const req = http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.statusCode, 200);
res.resume();
server.close();
});
req.end();
}));
// This timer should never go off as the server will close the socket
setTimeout(common.mustNotCall(), common.platformTimeout(10000)).unref();

View file

@ -10,7 +10,7 @@ function execute(options) {
const expectHeaders = { const expectHeaders = {
'x-foo': 'boom', 'x-foo': 'boom',
'cookie': 'a=1; b=2; c=3', 'cookie': 'a=1; b=2; c=3',
'connection': 'close' 'connection': 'keep-alive'
}; };
// no Host header when you set headers an array // no Host header when you set headers an array
@ -28,6 +28,7 @@ function execute(options) {
assert.deepStrictEqual(req.headers, expectHeaders); assert.deepStrictEqual(req.headers, expectHeaders);
res.writeHead(200, { 'Connection': 'close' });
res.end(); res.end();
}).listen(0, function() { }).listen(0, function() {
options = Object.assign(options, { options = Object.assign(options, {

View file

@ -0,0 +1,27 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const server = http.createServer(
{ keepAliveTimeout: common.platformTimeout(60000) },
function(req, res) {
req.resume();
res.writeHead(200, { 'Connection': 'keep-alive', 'Keep-Alive': 'timeout=1' });
res.end('FOO');
}
);
server.listen(0, common.mustCall(() => {
http.get({ port: server.address().port }, (res) => {
assert.strictEqual(res.statusCode, 200);
res.resume();
server.close();
});
}));
// This timer should never go off as the agent will parse the hint and terminate earlier
setTimeout(common.mustNotCall(), common.platformTimeout(3000)).unref();

View file

@ -10,7 +10,7 @@ const N = 2;
let abortRequest = true; let abortRequest = true;
const server = http.Server(common.mustCall((req, res) => { const server = http.Server(common.mustCall((req, res) => {
const headers = { 'Content-Type': 'text/plain' }; const headers = { 'Content-Type': 'text/plain', 'Connection': 'close' };
headers['Content-Length'] = 50; headers['Content-Length'] = 50;
const socket = res.socket; const socket = res.socket;
res.writeHead(200, headers); res.writeHead(200, headers);

View file

@ -13,7 +13,10 @@ const server = http.createServer((req, res) => {
server.listen(0, common.localhostIPv4, common.mustCall(() => { server.listen(0, common.localhostIPv4, common.mustCall(() => {
const port = server.address().port; const port = server.address().port;
const req = http.get(`http://${common.localhostIPv4}:${port}`); const req = http.get(
`http://${common.localhostIPv4}:${port}`,
{ agent: new http.Agent() }
);
req.setTimeout(1); req.setTimeout(1);
req.on('socket', common.mustCall((socket) => { req.on('socket', common.mustCall((socket) => {

View file

@ -5,17 +5,17 @@ const http = require('http');
const Countdown = require('../common/countdown'); const Countdown = require('../common/countdown');
const expectedHeadersMultipleWrites = { const expectedHeadersMultipleWrites = {
'connection': 'close', 'connection': 'keep-alive',
'transfer-encoding': 'chunked', 'transfer-encoding': 'chunked',
}; };
const expectedHeadersEndWithData = { const expectedHeadersEndWithData = {
'connection': 'close', 'connection': 'keep-alive',
'content-length': String('hello world'.length) 'content-length': String('hello world'.length),
}; };
const expectedHeadersEndNoData = { const expectedHeadersEndNoData = {
'connection': 'close', 'connection': 'keep-alive',
'content-length': '0', 'content-length': '0',
}; };
@ -24,6 +24,7 @@ const countdown = new Countdown(3, () => server.close());
const server = http.createServer(function(req, res) { const server = http.createServer(function(req, res) {
res.removeHeader('Date'); res.removeHeader('Date');
res.setHeader('Keep-Alive', 'timeout=1');
switch (req.url.substr(1)) { switch (req.url.substr(1)) {
case 'multiple-writes': case 'multiple-writes':
@ -59,7 +60,8 @@ server.listen(0, function() {
req.write('hello '); req.write('hello ');
req.end('world'); req.end('world');
req.on('response', function(res) { req.on('response', function(res) {
assert.deepStrictEqual(res.headers, expectedHeadersMultipleWrites); assert.deepStrictEqual(res.headers, { ...expectedHeadersMultipleWrites, 'keep-alive': 'timeout=1' });
res.resume();
}); });
req = http.request({ req = http.request({
@ -71,7 +73,8 @@ server.listen(0, function() {
req.removeHeader('Host'); req.removeHeader('Host');
req.end('hello world'); req.end('hello world');
req.on('response', function(res) { req.on('response', function(res) {
assert.deepStrictEqual(res.headers, expectedHeadersEndWithData); assert.deepStrictEqual(res.headers, { ...expectedHeadersEndWithData, 'keep-alive': 'timeout=1' });
res.resume();
}); });
req = http.request({ req = http.request({
@ -83,7 +86,8 @@ server.listen(0, function() {
req.removeHeader('Host'); req.removeHeader('Host');
req.end(); req.end();
req.on('response', function(res) { req.on('response', function(res) {
assert.deepStrictEqual(res.headers, expectedHeadersEndNoData); assert.deepStrictEqual(res.headers, { ...expectedHeadersEndNoData, 'keep-alive': 'timeout=1' });
res.resume();
}); });
}); });

View file

@ -32,9 +32,9 @@ const server = http.Server((req, res) => {
req.on('data', (chunk) => { req.on('data', (chunk) => {
result += chunk; result += chunk;
}).on('end', () => { }).on('end', () => {
server.close();
res.writeHead(200); res.writeHead(200);
res.end('hello world\n'); res.end('hello world\n');
server.close();
}); });
}); });

View file

@ -48,7 +48,7 @@ const server = http.createServer(function(req, res) {
expected = maxAndExpected[requests][1]; expected = maxAndExpected[requests][1];
server.maxHeadersCount = max; server.maxHeadersCount = max;
} }
res.writeHead(200, headers); res.writeHead(200, { ...headers, 'Connection': 'close' });
res.end(); res.end();
}); });
server.maxHeadersCount = max; server.maxHeadersCount = max;

View file

@ -16,9 +16,11 @@ events.captureRejections = true;
res.socket.on('error', common.mustCall((err) => { res.socket.on('error', common.mustCall((err) => {
assert.strictEqual(err, _err); assert.strictEqual(err, _err);
server.close();
})); }));
// Write until there is space in the buffer // Write until there is space in the buffer
res.writeHead(200, { 'Connection': 'close' });
while (res.write('hello')); while (res.write('hello'));
})); }));
@ -37,7 +39,6 @@ events.captureRejections = true;
code: 'ECONNRESET' code: 'ECONNRESET'
})); }));
res.resume(); res.resume();
server.close();
})); }));
})); }));
} }

View file

@ -34,13 +34,13 @@ http.createServer(function(req, res) {
'x-BaR', 'x-BaR',
'yoyoyo', 'yoyoyo',
'Connection', 'Connection',
'close', 'keep-alive',
]; ];
const expectHeaders = { const expectHeaders = {
'host': `localhost:${this.address().port}`, 'host': `localhost:${this.address().port}`,
'transfer-encoding': 'CHUNKED', 'transfer-encoding': 'CHUNKED',
'x-bar': 'yoyoyo', 'x-bar': 'yoyoyo',
'connection': 'close' 'connection': 'keep-alive'
}; };
const expectRawTrailers = [ const expectRawTrailers = [
'x-bAr', 'x-bAr',
@ -65,6 +65,7 @@ http.createServer(function(req, res) {
}); });
req.resume(); req.resume();
res.setHeader('Keep-Alive', 'timeout=1');
res.setHeader('Trailer', 'x-foo'); res.setHeader('Trailer', 'x-foo');
res.addTrailers([ res.addTrailers([
['x-fOo', 'xOxOxOx'], ['x-fOo', 'xOxOxOx'],
@ -86,22 +87,25 @@ http.createServer(function(req, res) {
req.end('y b a r'); req.end('y b a r');
req.on('response', function(res) { req.on('response', function(res) {
const expectRawHeaders = [ const expectRawHeaders = [
'Keep-Alive',
'timeout=1',
'Trailer', 'Trailer',
'x-foo', 'x-foo',
'Date', 'Date',
null, null,
'Connection', 'Connection',
'close', 'keep-alive',
'Transfer-Encoding', 'Transfer-Encoding',
'chunked', 'chunked',
]; ];
const expectHeaders = { const expectHeaders = {
'keep-alive': 'timeout=1',
'trailer': 'x-foo', 'trailer': 'x-foo',
'date': null, 'date': null,
'connection': 'close', 'connection': 'keep-alive',
'transfer-encoding': 'chunked' 'transfer-encoding': 'chunked'
}; };
res.rawHeaders[3] = null; res.rawHeaders[5] = null;
res.headers.date = null; res.headers.date = null;
assert.deepStrictEqual(res.rawHeaders, expectRawHeaders); assert.deepStrictEqual(res.rawHeaders, expectRawHeaders);
assert.deepStrictEqual(res.headers, expectHeaders); assert.deepStrictEqual(res.headers, expectHeaders);

View file

@ -36,9 +36,9 @@ const server = http.Server(function(req, res) {
req.on('end', function() { req.on('end', function() {
assert.strictEqual(result, expected); assert.strictEqual(result, expected);
server.close();
res.writeHead(200); res.writeHead(200);
res.end('hello world\n'); res.end('hello world\n');
server.close();
}); });
}); });

View file

@ -50,6 +50,10 @@ const getCountdownIndex = () => SERVER_RESPONSES.length - countdown.remaining;
const server = net.createServer(function(socket) { const server = net.createServer(function(socket) {
socket.write(SERVER_RESPONSES[getCountdownIndex()]); socket.write(SERVER_RESPONSES[getCountdownIndex()]);
if (SHOULD_KEEP_ALIVE[getCountdownIndex()]) {
socket.end();
}
}).listen(0, function() { }).listen(0, function() {
function makeRequest() { function makeRequest() {
const req = http.get({ port: server.address().port }, function(res) { const req = http.get({ port: server.address().port }, function(res) {

View file

@ -20,7 +20,7 @@ server.listen(common.PIPE, common.mustCall(() =>
function asyncLoop(fn, times, cb) { function asyncLoop(fn, times, cb) {
fn(function handler() { fn(function handler() {
if (--times) { if (--times) {
fn(handler); setTimeout(() => fn(handler), common.platformTimeout(10));
} else { } else {
cb(); cb();
} }

View file

@ -19,6 +19,7 @@ const options = {
// Create TLS1.2 server // Create TLS1.2 server
https.createServer(options, function(req, res) { https.createServer(options, function(req, res) {
res.writeHead(200, { 'Connection': 'close' });
res.end('ohai'); res.end('ohai');
}).listen(0, function() { }).listen(0, function() {
first(this); first(this);
@ -44,6 +45,7 @@ function first(server) {
function faultyServer(port) { function faultyServer(port) {
options.secureProtocol = 'TLSv1_method'; options.secureProtocol = 'TLSv1_method';
https.createServer(options, function(req, res) { https.createServer(options, function(req, res) {
res.writeHead(200, { 'Connection': 'close' });
res.end('hello faulty'); res.end('hello faulty');
}).listen(port, function() { }).listen(port, function() {
second(this); second(this);

View file

@ -37,7 +37,7 @@ const server = https.createServer(serverOptions, common.mustCall((req, res) => {
expected = maxAndExpected[requests][1]; expected = maxAndExpected[requests][1];
server.maxHeadersCount = max; server.maxHeadersCount = max;
} }
res.writeHead(200, headers); res.writeHead(200, { ...headers, 'Connection': 'close' });
res.end(); res.end();
}, 3)); }, 3));
server.maxHeadersCount = max; server.maxHeadersCount = max;

View file

@ -62,7 +62,8 @@ const http = require('http');
server.listen(0, () => { server.listen(0, () => {
const req = http.request({ const req = http.request({
port: server.address().port port: server.address().port,
agent: new http.Agent()
}); });
req.write('asd'); req.write('asd');
@ -96,7 +97,8 @@ const http = require('http');
server.listen(0, () => { server.listen(0, () => {
const req = http.request({ const req = http.request({
port: server.address().port port: server.address().port,
agent: new http.Agent()
}); });
req.write('asd'); req.write('asd');

View file

@ -62,7 +62,7 @@ const proxy = net.createServer((clientSocket) => {
'HTTP/1.1\r\n' + 'HTTP/1.1\r\n' +
'Proxy-Connections: keep-alive\r\n' + 'Proxy-Connections: keep-alive\r\n' +
`Host: localhost:${proxy.address().port}\r\n` + `Host: localhost:${proxy.address().port}\r\n` +
'Connection: close\r\n\r\n'); 'Connection: keep-alive\r\n\r\n');
console.log('PROXY: got CONNECT request'); console.log('PROXY: got CONNECT request');
console.log('PROXY: creating a tunnel'); console.log('PROXY: creating a tunnel');

View file

@ -82,7 +82,8 @@ function makeRequest(port, id) {
rejectUnauthorized: true, rejectUnauthorized: true,
ca: credentialOptions[0].ca, ca: credentialOptions[0].ca,
servername: 'agent1', servername: 'agent1',
headers: { id } headers: { id },
agent: new https.Agent()
}; };
let errored = false; let errored = false;

View file

@ -43,7 +43,7 @@ const server = http.createServer(function(req, res) {
req.on('end', function() { req.on('end', function() {
assert.strictEqual(body, 'PING'); assert.strictEqual(body, 'PING');
res.writeHead(200); res.writeHead(200, { 'Connection': 'close' });
res.end('PONG'); res.end('PONG');
}); });
}); });