From 7123bf7ca42ec3a73700eab5bb4ecabfb292ede6 Mon Sep 17 00:00:00 2001 From: jazelly Date: Mon, 9 Sep 2024 22:47:10 +0930 Subject: [PATCH] http: reduce likelihood of race conditions on keep-alive timeout Fixes: https://github.com/nodejs/node/issues/52649 Refs: https://github.com/nodejs/node/issues/54293 Co-authored-by: Arrigo Zanette PR-URL: https://github.com/nodejs/node/pull/54863 Reviewed-By: James M Snell Reviewed-By: LiviaMedeiros Reviewed-By: Jake Yuesong Li Reviewed-By: Ethan Arrowood --- lib/_http_agent.js | 18 +++++++-- lib/_http_server.js | 6 ++- ...-http-keep-alive-timeout-race-condition.js | 38 +++++++++++++++++++ 3 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 test/parallel/test-http-keep-alive-timeout-race-condition.js diff --git a/lib/_http_agent.js b/lib/_http_agent.js index ad8eb227f6b513..1f7989b843c073 100644 --- a/lib/_http_agent.js +++ b/lib/_http_agent.js @@ -49,6 +49,9 @@ const { const kOnKeylog = Symbol('onkeylog'); const kRequestOptions = Symbol('requestOptions'); const kRequestAsyncResource = Symbol('requestAsyncResource'); + +// TODO(jazelly): make this configurable +const HTTP_AGENT_KEEP_ALIVE_TIMEOUT_BUFFER = 1000; // New Agent code. // The largest departure from the previous implementation is that @@ -473,6 +476,7 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) { socket.unref(); let agentTimeout = this.options.timeout || 0; + let canKeepSocketAlive = true; if (socket._httpMessage?.res) { const keepAliveHint = socket._httpMessage.res.headers['keep-alive']; @@ -481,9 +485,15 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) { const hint = /^timeout=(\d+)/.exec(keepAliveHint)?.[1]; if (hint) { - const serverHintTimeout = NumberParseInt(hint) * 1000; - - if (serverHintTimeout < agentTimeout) { + // Let the timer expire before the announced timeout to reduce + // the likelihood of ECONNRESET errors + let serverHintTimeout = (NumberParseInt(hint) * 1000) - HTTP_AGENT_KEEP_ALIVE_TIMEOUT_BUFFER; + serverHintTimeout = serverHintTimeout > 0 ? serverHintTimeout : 0; + if (serverHintTimeout === 0) { + // Cannot safely reuse the socket because the server timeout is + // too short + canKeepSocketAlive = false; + } else if (serverHintTimeout < agentTimeout) { agentTimeout = serverHintTimeout; } } @@ -494,7 +504,7 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) { socket.setTimeout(agentTimeout); } - return true; + return canKeepSocketAlive; }; Agent.prototype.reuseSocket = function reuseSocket(socket, req) { diff --git a/lib/_http_server.js b/lib/_http_server.js index a9616740dd6f86..200e7a731daf08 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -182,6 +182,8 @@ const kConnections = Symbol('http.server.connections'); const kConnectionsCheckingInterval = Symbol('http.server.connectionsCheckingInterval'); const HTTP_SERVER_TRACE_EVENT_NAME = 'http.server.request'; +// TODO(jazelly): make this configurable +const HTTP_SERVER_KEEP_ALIVE_TIMEOUT_BUFFER = 1000; class HTTPServerAsyncResource { constructor(type, socket) { @@ -1007,7 +1009,9 @@ function resOnFinish(req, res, socket, state, server) { } } else if (state.outgoing.length === 0) { if (server.keepAliveTimeout && typeof socket.setTimeout === 'function') { - socket.setTimeout(server.keepAliveTimeout); + // Increase the internal timeout wrt the advertised value to reduce + // the likelihood of ECONNRESET errors. + socket.setTimeout(server.keepAliveTimeout + HTTP_SERVER_KEEP_ALIVE_TIMEOUT_BUFFER); state.keepAliveTimeoutSet = true; } } else { diff --git a/test/parallel/test-http-keep-alive-timeout-race-condition.js b/test/parallel/test-http-keep-alive-timeout-race-condition.js new file mode 100644 index 00000000000000..08fa84265baa49 --- /dev/null +++ b/test/parallel/test-http-keep-alive-timeout-race-condition.js @@ -0,0 +1,38 @@ +'use strict'; + +const common = require('../common'); +const http = require('http'); + +const makeRequest = (port, agent) => + new Promise((resolve, reject) => { + const req = http.get( + { path: '/', port, agent }, + (res) => { + res.resume(); + res.on('end', () => resolve()); + }, + ); + req.on('error', (e) => reject(e)); + req.end(); + }); + +const server = http.createServer( + { keepAliveTimeout: common.platformTimeout(2000), keepAlive: true }, + common.mustCall((req, res) => { + const body = 'hello world\n'; + res.writeHead(200, { 'Content-Length': body.length }); + res.write(body); + res.end(); + }, 2) +); + +const agent = new http.Agent({ maxSockets: 5, keepAlive: true }); + +server.listen(0, common.mustCall(async function() { + await makeRequest(this.address().port, agent); + // Block the event loop for 2 seconds + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000); + await makeRequest(this.address().port, agent); + server.close(); + agent.destroy(); +}));