Skip to content

Commit 596940c

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 d288ec3 commit 596940c

File tree

3 files changed

+57
-5
lines changed

3 files changed

+57
-5
lines changed

lib/_http_agent.js

Lines changed: 14 additions & 4 deletions
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

Lines changed: 5 additions & 1 deletion
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) {
@@ -998,7 +1000,9 @@ function resOnFinish(req, res, socket, state, server) {
9981000
}
9991001
} else if (state.outgoing.length === 0) {
10001002
if (server.keepAliveTimeout && typeof socket.setTimeout === 'function') {
1001-
socket.setTimeout(server.keepAliveTimeout);
1003+
// Increase the internal timeout wrt the advertised value to reduce
1004+
// the likelihood of ECONNRESET errors.
1005+
socket.setTimeout(server.keepAliveTimeout + HTTP_SERVER_KEEP_ALIVE_TIMEOUT_BUFFER);
10021006
state.keepAliveTimeoutSet = true;
10031007
}
10041008
} else {
Lines changed: 38 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy