Skip to content

Commit e6dfcdd

Browse files
jazellyZanette Arrigo
andcommitted
http: reduce likelihood of race conditions on keep-alive timeout
Fixes: #52649 Refs: #54293 Co-authored-by: Zanette Arrigo <[email protected]>
1 parent 79a33a7 commit e6dfcdd

File tree

3 files changed

+53
-5
lines changed

3 files changed

+53
-5
lines changed

lib/_http_agent.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
473473
socket.unref();
474474

475475
let agentTimeout = this.options.timeout || 0;
476+
let canKeepSocketAlive = true;
476477

477478
if (socket._httpMessage?.res) {
478479
const keepAliveHint = socket._httpMessage.res.headers['keep-alive'];
@@ -481,9 +482,15 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
481482
const hint = /^timeout=(\d+)/.exec(keepAliveHint)?.[1];
482483

483484
if (hint) {
484-
const serverHintTimeout = NumberParseInt(hint) * 1000;
485-
486-
if (serverHintTimeout < agentTimeout) {
485+
// Let the timer expire before the announced timeout to reduce
486+
// the likelihood of ECONNRESET errors
487+
let serverHintTimeout = (NumberParseInt(hint) * 1000) - 1000;
488+
serverHintTimeout = serverHintTimeout > 0 ? serverHintTimeout : 0;
489+
if (serverHintTimeout === 0) {
490+
// Cannot safely reuse the socket because the server timeout is
491+
// too short
492+
canKeepSocketAlive = false;
493+
} else if (serverHintTimeout < agentTimeout) {
487494
agentTimeout = serverHintTimeout;
488495
}
489496
}
@@ -494,7 +501,7 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
494501
socket.setTimeout(agentTimeout);
495502
}
496503

497-
return true;
504+
return canKeepSocketAlive;
498505
};
499506

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

lib/_http_server.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1007,7 +1007,9 @@ function resOnFinish(req, res, socket, state, server) {
10071007
}
10081008
} else if (state.outgoing.length === 0) {
10091009
if (server.keepAliveTimeout && typeof socket.setTimeout === 'function') {
1010-
socket.setTimeout(server.keepAliveTimeout);
1010+
// Increase the internal timeout wrt the advertised value to reduce
1011+
// the likelihood of ECONNRESET errors.
1012+
socket.setTimeout(server.keepAliveTimeout + 1000);
10111013
state.keepAliveTimeoutSet = true;
10121014
}
10131015
} else {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
const timestamp = Date.now();
34+
// Block the event loop for 2 seconds
35+
while (Date.now() < timestamp + 2000);
36+
await makeRequest(this.address().port, agent);
37+
server.close();
38+
agent.destroy();
39+
}));

0 commit comments

Comments
 (0)