Closed mweberxyz closed 2 weeks ago
@nodejs/http
1
I'm experiencing this exact issue when trying to run ionic start
to start a new ionic project.
@EthanTuning it's possible though unlikely related to this issue. ECONNRESET is a fairly general error code for when the server closes the socket unexpectedly. This issue is concerned with when an ECONNRESET occurs in the exact same tick of the event loop as a http request resuming on an previously used socket - which seems unlikely to be reproduced repeatedly except in the kind of narrow circumstances proposed in my post.
Is Node ignoring server sent Keep Alive hint headers on purpose? This method in _http_agent.js
is suspect:
Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
socket.setKeepAlive(true, this.keepAliveMsecs);
socket.unref();
let agentTimeout = this.options.timeout || 0;
if (socket._httpMessage?.res) {
const keepAliveHint = socket._httpMessage.res.headers['keep-alive'];
if (keepAliveHint) {
const hint = RegExpPrototypeExec(/^timeout=(\d+)/, keepAliveHint)?.[1];
if (hint) {
const serverHintTimeout = NumberParseInt(hint) * 1000;
if (serverHintTimeout < agentTimeout) {
agentTimeout = serverHintTimeout;
}
}
}
}
if (socket.timeout !== agentTimeout) {
socket.setTimeout(agentTimeout);
}
return true;
};
The serverHintTimeout < agentTimeout
conditional always evaluates to false (in the default conditions) because the default agentTimeout
is 0 -- which means Node will never opportunistically close a connection and instead always wait for the server to close it first.
https://github.com/nodejs/node/pull/52653 seems relevant.
The serverHintTimeout < agentTimeout conditional always evaluates to false (in the default conditions) because the default agentTimeout is 0 -- which means Node will never opportunistically close a connection and instead always wait for the server to close it first.
I think we should close them actually. PR?
@EthanTuning it's possible though unlikely related to this issue. ECONNRESET is a fairly general error code for when the server closes the socket unexpectedly. This issue is concerned with when an ECONNRESET occurs in the exact same tick of the event loop as a http request resuming on an previously used socket - which seems unlikely to be reproduced repeatedly except in the kind of narrow circumstances proposed in my post.
Oh thanks, yeah I was very unsure, but googling got me here haha!
The
serverHintTimeout < agentTimeout
conditional always evaluates to false (in the default conditions) because the defaultagentTimeout
is 0 -- which means Node will never opportunistically close a connection and instead always wait for the server to close it first.
I realize default in this context meant two different things.
Using the default http.globalAgent -- the default timeout is 5000ms.
Using a non-default agent, with keepAlive on, but otherwise default parameters, (ie- new http.Agent({ keepAlive: true });
-- the issue arises.
Version
v21.7.3
Platform
Darwin [me] 23.4.0 Darwin Kernel Version 23.4.0: Fri Mar 15 00:11:08 PDT 2024; root:xnu-10063.101.17~1/RELEASE_ARM64_T8122 arm64
Subsystem
http
What steps will reproduce the bug?
Run
server.js
:Run
client.js
:How often does it reproduce? Is there a required condition?
Fairly reliably, might take a few runs to see it, or adjust the timeouts to account for device differences.
What is the expected behavior? Why is that the expected behavior?
It is expected that (generally) every request should succeed.
What do you see instead?
In the full debug logs - http.log - the server connection close is apparent, followed by the attempt to use the closed socket for the next request:
Additional information
The server is closing the connection at it's own 1000ms timeout, but the keepAlive timers in the client either:
The issue existed prior to Node 20, but it's incidence is more pronounced now that keepAlive is defaulted to true in http.globalAgent.
A similar issue was identified in https://github.com/nodejs/undici/issues/3141. Undici already had logic in place to close connections on the client side 1000ms prior to the keep-alive value provided by the server (to account for timer/transport latency) but said logic was failing to run due to an unrelated issue.