From cc13d6538136a8761c9478de4dbc604da12bb50e Mon Sep 17 00:00:00 2001 From: Divyanshu Sharma Date: Wed, 14 Oct 2026 23:49:02 +0530 Subject: [PATCH 1/3] http: respect Keep-Alive timeout hint when agent has no explicit timeout When http.Agent is created with keepAlive: true but no explicit timeout option, the server's Keep-Alive: timeout=N hint was being ignored. In keepSocketAlive(), agentTimeout is initialized as: let agentTimeout = this.options.timeout || 0; When no timeout is configured, this evaluates to 0. The condition that applies the server hint: } else if (serverHintTimeout < agentTimeout) { is never true when agentTimeout is 0, because serverHintTimeout is always a positive value after buffer subtraction. As a result, socket.setTimeout(0) is called, which disables the idle timer entirely. The socket remains in freeSockets after the server closes it, and the next request that reuses it receives ECONNRESET. Fix by also applying the server hint when agentTimeout is 0, meaning no client-side timeout is configured. Fixes: https://github.com/nodejs/node/issues/66734 Signed-off-by: Divyanshu Sharma --- lib/_http_agent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/_http_agent.js b/lib/_http_agent.js index 3871c5b56a2f..daaee3d8fdd4 100644 --- a/lib/_http_agent.js +++ b/lib/_http_agent.js @@ -654,7 +654,7 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) { // Cannot safely reuse the socket because the server timeout is // too short canKeepSocketAlive = false; - } else if (serverHintTimeout < agentTimeout) { + } else if (agentTimeout === 0 || serverHintTimeout < agentTimeout) { agentTimeout = serverHintTimeout; } } From 5a3ecad28507b308728932ce7965ca6ba2390497 Mon Sep 17 00:00:00 2001 From: Divyanshu Sharma Date: Thu, 15 Oct 2026 00:11:33 +0530 Subject: [PATCH 2/3] test: add regression test for http agent Keep-Alive server hint Add test-http-agent-keep-alive-server-hint.js covering four cases for Agent.prototype.keepSocketAlive() server hint handling: - agent with no explicit timeout respects server Keep-Alive hint - agent timeout longer than server hint: server hint wins - agent timeout shorter than server hint: agent timeout wins - no Keep-Alive header: socket.timeout stays 0 (no timer) Refs: https://github.com/nodejs/node/issues/66734 Signed-off-by: Divyanshu Sharma --- .../test-http-agent-keep-alive-server-hint.js | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 test/parallel/test-http-agent-keep-alive-server-hint.js diff --git a/test/parallel/test-http-agent-keep-alive-server-hint.js b/test/parallel/test-http-agent-keep-alive-server-hint.js new file mode 100644 index 000000000000..50611cde5476 --- /dev/null +++ b/test/parallel/test-http-agent-keep-alive-server-hint.js @@ -0,0 +1,158 @@ +'use strict'; + +// Regression test for https://github.com/nodejs/node/issues/66734 +// +// When http.Agent is created with keepAlive: true but no explicit timeout, +// the server's Keep-Alive: timeout=N hint must be applied to the idle socket. +// Previously, agentTimeout defaulted to 0 and the condition +// `serverHintTimeout < agentTimeout` was never true, so setTimeout(0) was +// called, disabling the timer and leaving stale sockets in freeSockets. + +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); + +// Helper: returns the first idle socket for the agent, after the 'free' +// event has been processed (one setImmediate tick after response end). +function getIdleSocket(agent) { + const key = Object.keys(agent.freeSockets)[0]; + return key ? agent.freeSockets[key][0] : undefined; +} + +// Case 1: Agent has no explicit timeout. +// Server sends Keep-Alive: timeout=5. +// Expected: socket.timeout === 5000 - 1000 (default buffer) === 4000. +{ + const SERVER_HINT_S = 5; + const EXPECTED_TIMEOUT = (SERVER_HINT_S * 1000) - 1000; // 4000ms + + const server = http.createServer(common.mustCall((req, res) => { + res.writeHead(200, { 'Keep-Alive': `timeout=${SERVER_HINT_S}` }); + res.end('ok'); + })); + + server.listen(0, common.mustCall(() => { + const agent = new http.Agent({ keepAlive: true }); + + http.get({ port: server.address().port, agent }, common.mustCall((res) => { + res.resume(); + res.on('end', common.mustCall(() => { + setImmediate(common.mustCall(() => { + const socket = getIdleSocket(agent); + assert.ok(socket, 'socket should be in freeSockets'); + assert.strictEqual( + socket.timeout, + EXPECTED_TIMEOUT, + `Case 1: expected socket.timeout ${EXPECTED_TIMEOUT}, ` + + `got ${socket.timeout}` + ); + agent.destroy(); + server.close(); + })); + })); + })); + })); +} + +// Case 2: Agent has an explicit timeout longer than the server hint. +// Server sends Keep-Alive: timeout=5 (4000ms after buffer). +// Agent timeout is 10000ms. +// Expected: server hint wins → socket.timeout === 4000. +{ + const SERVER_HINT_S = 5; + const AGENT_TIMEOUT = 10000; + const EXPECTED_TIMEOUT = (SERVER_HINT_S * 1000) - 1000; // 4000ms + + const server = http.createServer(common.mustCall((req, res) => { + res.writeHead(200, { 'Keep-Alive': `timeout=${SERVER_HINT_S}` }); + res.end('ok'); + })); + + server.listen(0, common.mustCall(() => { + const agent = new http.Agent({ keepAlive: true, timeout: AGENT_TIMEOUT }); + + http.get({ port: server.address().port, agent }, common.mustCall((res) => { + res.resume(); + res.on('end', common.mustCall(() => { + setImmediate(common.mustCall(() => { + const socket = getIdleSocket(agent); + assert.ok(socket, 'socket should be in freeSockets'); + assert.strictEqual( + socket.timeout, + EXPECTED_TIMEOUT, + `Case 2: expected socket.timeout ${EXPECTED_TIMEOUT}, ` + + `got ${socket.timeout}` + ); + agent.destroy(); + server.close(); + })); + })); + })); + })); +} + +// Case 3: Agent has an explicit timeout shorter than the server hint. +// Server sends Keep-Alive: timeout=10 (9000ms after buffer). +// Agent timeout is 3000ms. +// Expected: agent timeout wins → socket.timeout === 3000. +{ + const SERVER_HINT_S = 10; + const AGENT_TIMEOUT = 3000; + const EXPECTED_TIMEOUT = AGENT_TIMEOUT; // 3000ms + + const server = http.createServer(common.mustCall((req, res) => { + res.writeHead(200, { 'Keep-Alive': `timeout=${SERVER_HINT_S}` }); + res.end('ok'); + })); + + server.listen(0, common.mustCall(() => { + const agent = new http.Agent({ keepAlive: true, timeout: AGENT_TIMEOUT }); + + http.get({ port: server.address().port, agent }, common.mustCall((res) => { + res.resume(); + res.on('end', common.mustCall(() => { + setImmediate(common.mustCall(() => { + const socket = getIdleSocket(agent); + assert.ok(socket, 'socket should be in freeSockets'); + assert.strictEqual( + socket.timeout, + EXPECTED_TIMEOUT, + `Case 3: expected socket.timeout ${EXPECTED_TIMEOUT}, ` + + `got ${socket.timeout}` + ); + agent.destroy(); + server.close(); + })); + })); + })); + })); +} + +// Case 4: No Keep-Alive header from server, no agent timeout. +// Expected: socket.timeout === 0 (no timer set — existing behaviour unchanged). +{ + const server = http.createServer(common.mustCall((req, res) => { + res.end('ok'); + })); + + server.listen(0, common.mustCall(() => { + const agent = new http.Agent({ keepAlive: true }); + + http.get({ port: server.address().port, agent }, common.mustCall((res) => { + res.resume(); + res.on('end', common.mustCall(() => { + setImmediate(common.mustCall(() => { + const socket = getIdleSocket(agent); + assert.ok(socket, 'socket should be in freeSockets'); + assert.strictEqual( + socket.timeout, + 0, + `Case 4: expected socket.timeout 0, got ${socket.timeout}` + ); + agent.destroy(); + server.close(); + })); + })); + })); + })); +} From ef5f42f4b0fa4b9aaa665baed7658f0bae6d4e32 Mon Sep 17 00:00:00 2001 From: Divyanshu Sharma Date: Thu, 15 Oct 2026 02:07:07 +0530 Subject: [PATCH 3/3] test: fix http agent keep-alive server hint test case 4 Node's http.Server automatically sends Keep-Alive: timeout=65 on every keep-alive response unless keepAliveTimeout is set to 0. Case 4 assumed no hint would be sent with a plain createServer(), which was incorrect. Use { keepAliveTimeout: 0 } to suppress the automatic header and correctly test the no-hint code path. Refs: https://github.com/nodejs/node/issues/66734 Signed-off-by: Divyanshu Sharma --- .../test-http-agent-keep-alive-server-hint.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/parallel/test-http-agent-keep-alive-server-hint.js b/test/parallel/test-http-agent-keep-alive-server-hint.js index 50611cde5476..0394d5d60bfe 100644 --- a/test/parallel/test-http-agent-keep-alive-server-hint.js +++ b/test/parallel/test-http-agent-keep-alive-server-hint.js @@ -128,12 +128,16 @@ function getIdleSocket(agent) { })); } -// Case 4: No Keep-Alive header from server, no agent timeout. +// Case 4: Server sends no Keep-Alive hint (keepAliveTimeout: 0 suppresses the +// automatic header), no agent timeout. // Expected: socket.timeout === 0 (no timer set — existing behaviour unchanged). { - const server = http.createServer(common.mustCall((req, res) => { - res.end('ok'); - })); + const server = http.createServer( + { keepAliveTimeout: 0 }, + common.mustCall((req, res) => { + res.end('ok'); + }) + ); server.listen(0, common.mustCall(() => { const agent = new http.Agent({ keepAlive: true });