Repository navigation
Conversation
A free socket with an idle timeout is normally destroyed by its timer. When the event loop is blocked long enough for that timeout to expire, the timer cannot run, and `addRequest()` can reuse the socket even though the server may already have closed the connection. This can cause the request to fail with `ECONNRESET`. Record when a socket enters the free pool and, before reuse, remove and destroy a free socket whose idle timeout has expired according to the timers' clock. Refs: nodejs#54293 Assisted-by: a closed-source coding agent Signed-off-by: dennev <dennev13@gmail.com>
|
Review requested:
|
|
Welcome to Node.js, and thank you for your first contribution! Before review, please take a moment to read:
Please make sure every commit is signed off. For a first pull request, GitHub Actions require collaborator approval and Jenkins CI must be started by a collaborator or triager, so an initial wait is normal. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #67292 +/- ##
==========================================
- Coverage 90.69% 90.69% -0.01%
==========================================
Files 797 797
Lines 278787 279632 +845
Branches 53610 53781 +171
==========================================
+ Hits 252849 253604 +755
+ Misses 16310 16298 -12
- Partials 9628 9730 +102
🚀 New features to boost your workflow:
|
| socket = this.scheduling === 'fifo' ? | ||
| freeSockets.shift() : | ||
| freeSockets.pop(); | ||
| if (!isFreeSocketExpired(socket, now)) |
There was a problem hiding this comment.
| if (!isFreeSocketExpired(socket, now)) | |
| if (!socket.destroyed && !isFreeSocketExpired(socket, now)) |
Since this loop now walks the pool anyway, could it also skip sockets that are already destroyed? This will fix another bug
In LIFO mode a request made from setImmediate in that window still pops it and fails with econnreset
There was a problem hiding this comment.
Thanks, good catch. Applied in a1d6973, together with a test case for it.
I reproduced it on v26.12.1: with two sockets in the free pool and the default
lifo scheduling, a request made right after the most recently freed socket is
destroyed fails with ECONNRESET (socket hang up).
I ran the updated test on a release build on Linux, and it passed. Agent-run
validation: the new case fails without the change; with it, the regression
test, the HTTP/HTTPS tests and the full make -j12 test passed.
A destroyed socket can stay in the agent's free pool until its 'close' event. `addRequest()` only dropped destroyed sockets from the head of the pool, so a request made before that event could still take one, for example from the tail of the pool with the default `lifo` scheduling, and fail with `ECONNRESET`. Skip destroyed sockets wherever they are in the pool, and remove them from the agent at once, as is done for expired sockets. Assisted-by: a closed-source coding agent Signed-off-by: dennev <dennev13@gmail.com>
When the event loop is blocked long enough for a free socket's idle timeout
to expire, its timeout callback cannot run. The next request can then reuse
that socket from
agent.freeSocketseven though the server may already haveclosed the connection, causing the request to fail with
ECONNRESET.This change addresses the timeout-expiry case discussed in #54293. It records
when a socket enters the free pool using the timers' clock
(
timersBinding.getLibuvNow()). InaddRequest(), expired candidates areremoved and destroyed until an unexpired socket is found or the pool is empty.
The clock call preserves the binding object as its receiver, as required by
the fast API call. The regression test also exercises an optimized
addRequest().Not covered by this change:
without a
timeout(http: Agent with keepAlive and no timeout ignores the server's Keep-Alive timeout hint #66734, http: respect Keep-Alive timeout hint when agent has no explicit timeout #66779)keep-alive race discussed in Network request fails after event loop is blocked for a few seconds (regression in v20) #54293)
'free'handler to a queued requestin a response's
'data'or'end'handler: the idle timestamp is recordedonly when the socket enters the pool, so time spent blocked before that
point is not counted
Testing: I personally ran the regression test on a release build in a Linux
container, and it passed. I also ran a reproduction based on #54293, with
server.keepAliveTimeout = 5000and a 6-second block between two requeststhrough
http.globalAgent. It fails withECONNRESETon v26.12.1 and succeedswith this change, using a new socket.
Agent-run validation confirmed that the regression test fails without this
change and passes with it. It covers LIFO and FIFO expiry, reuse before expiry,
unchanged reuse without an idle timeout,
maxSocketsaccounting after a block,and optimized
addRequest().Agent-run validation on Linux also included the HTTP/HTTPS and agent-related
tests, and the full
make -j12 test, all of which passed. The full-suite runused a release build in a container started with
--init.AI disclosure: Claude Code (Claude Opus 5.5) was used to investigate the cause,
write and review the code, test and documentation changes, and run the
agent-run validation described above. This description started from a draft
by the agent, which I rewrote. Before opening this pull request, Claude and
Codex (gpt-6.1-sol) reviewed the final change, the commit message and this
description.
I read the change and the new test myself and checked them against the code
paths described above.
Refs: #54293