Skip to content

Commit 7123bf7

Browse files
jazellyzanettea
authored andcommitted
http: reduce likelihood of race conditions on keep-alive timeout
Fixes: #52649 Refs: #54293 Co-authored-by: Arrigo Zanette <zanettea@gmail.com> PR-URL: #54863 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: LiviaMedeiros <livia@cirno.name> Reviewed-By: Jake Yuesong Li <jake.yuesong@gmail.com> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
1 parent 69ec9d8 commit 7123bf7

File tree

3 files changed

+57
-5
lines changed

3 files changed

+57
-5
lines changed

lib/_http_agent.js

+14-4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ const {
4949
const kOnKeylog = Symbol('onkeylog');
5050
const kRequestOptions = Symbol('requestOptions');
5151
const kRequestAsyncResource = Symbol('requestAsyncResource');
52+
53+
// TODO(jazelly): make this configurable
54+
const HTTP_AGENT_KEEP_ALIVE_TIMEOUT_BUFFER = 1000;
5255
// New Agent code.
5356

5457
// The largest departure from the previous implementation is that
@@ -473,6 +476,7 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
473476
socket.unref();
474477

475478
let agentTimeout = this.options.timeout || 0;
479+
let canKeepSocketAlive = true;
476480

477481
if (socket._httpMessage?.res) {
478482
const keepAliveHint = socket._httpMessage.res.headers['keep-alive'];
@@ -481,9 +485,15 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
481485
const hint = /^timeout=(\d+)/.exec(keepAliveHint)?.[1];
482486

483487
if (hint) {
484-
const serverHintTimeout = NumberParseInt(hint) * 1000;
485-
486-
if (serverHintTimeout < agentTimeout) {
488+
// Let the timer expire before the announced timeout to reduce
489+
// the likelihood of ECONNRESET errors
490+
let serverHintTimeout = (NumberParseInt(hint) * 1000) - HTTP_AGENT_KEEP_ALIVE_TIMEOUT_BUFFER;
491+
serverHintTimeout = serverHintTimeout > 0 ? serverHintTimeout : 0;
492+
if (serverHintTimeout === 0) {
493+
// Cannot safely reuse the socket because the server timeout is
494+
// too short
495+
canKeepSocketAlive = false;
496+
} else if (serverHintTimeout < agentTimeout) {
487497
agentTimeout = serverHintTimeout;
488498
}
489499
}
@@ -494,7 +504,7 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
494504
socket.setTimeout(agentTimeout);
495505
}
496506

497-
return true;
507+
return canKeepSocketAlive;
498508
};
499509

500510
Agent.prototype.reuseSocket = function reuseSocket(socket, req) {

lib/_http_server.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ const kConnections = Symbol('http.server.connections');
182182
const kConnectionsCheckingInterval = Symbol('http.server.connectionsCheckingInterval');
183183

184184
const HTTP_SERVER_TRACE_EVENT_NAME = 'http.server.request';
185+
// TODO(jazelly): make this configurable
186+
const HTTP_SERVER_KEEP_ALIVE_TIMEOUT_BUFFER = 1000;
185187

186188
class HTTPServerAsyncResource {
187189
constructor(type, socket) {
@@ -1007,7 +1009,9 @@ function resOnFinish(req, res, socket, state, server) {
10071009
}
10081010
} else if (state.outgoing.length === 0) {
10091011
if (server.keepAliveTimeout && typeof socket.setTimeout === 'function') {
1010-
socket.setTimeout(server.keepAliveTimeout);
1012+
// Increase the internal timeout wrt the advertised value to reduce
1013+
// the likelihood of ECONNRESET errors.
1014+
socket.setTimeout(server.keepAliveTimeout + HTTP_SERVER_KEEP_ALIVE_TIMEOUT_BUFFER);
10111015
state.keepAliveTimeoutSet = true;
10121016
}
10131017
} else {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const http = require('http');
5+
6+
const makeRequest = (port, agent) =>
7+
new Promise((resolve, reject) => {
8+
const req = http.get(
9+
{ path: '/', port, agent },
10+
(res) => {
11+
res.resume();
12+
res.on('end', () => resolve());
13+
},
14+
);
15+
req.on('error', (e) => reject(e));
16+
req.end();
17+
});
18+
19+
const server = http.createServer(
20+
{ keepAliveTimeout: common.platformTimeout(2000), keepAlive: true },
21+
common.mustCall((req, res) => {
22+
const body = 'hello world\n';
23+
res.writeHead(200, { 'Content-Length': body.length });
24+
res.write(body);
25+
res.end();
26+
}, 2)
27+
);
28+
29+
const agent = new http.Agent({ maxSockets: 5, keepAlive: true });
30+
31+
server.listen(0, common.mustCall(async function() {
32+
await makeRequest(this.address().port, agent);
33+
// Block the event loop for 2 seconds
34+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
35+
await makeRequest(this.address().port, agent);
36+
server.close();
37+
agent.destroy();
38+
}));

0 commit comments

Comments
 (0)