You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
http: aborting a keep-alive response during async iteration emits an unhandled Socket error #65938
As of the preparation date, v24.21.0 is the latest LTS release and v26.8.1 is
the latest Current release in the official Node release index.
The v26.8.1 Linux x64 executable was downloaded from nodejs.org and its archive
was checked against the official SHASUMS256.txt.
Platform
Linux x64 under WSL2. Output of uname -srvmo (hostname omitted):
Linux 6.18.33.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun 18 21:54:43 UTC 2026 x86_64 GNU/Linux
Other operating systems have not been tested. The first affected Node release
has not been bisected.
Subsystem
node:http, ClientRequest, IncomingMessage, keep-alive socket lifecycle,
and cancellation through AbortSignal.
What steps will reproduce the bug?
Save the following as repro.mjs and run:
node repro.mjs
The script uses only Node built-ins, binds a local server to 127.0.0.1 on an
ephemeral port, and returns a one-byte HTTP response. It requires no packages,
external services, TLS certificates, DNS customization or application code.
Both the request and the response have persistent error handlers. Async
iteration is also inside a try/catch. The script does not remove any error
handlers or install a process-wide exception handler.
// Standalone upstream reproducer: only Node built-ins and a loopback HTTP server.importhttpfrom"node:http";import{once}from"node:events";import{setTimeoutasdelay}from"node:timers/promises";constagent=newhttp.Agent({keepAlive: true});constserver=http.createServer((_request,response)=>response.end("x"));server.listen(0,"127.0.0.1");awaitonce(server,"listening");try{constcontroller=newAbortController();constresponse=awaitnewPromise((resolve,reject)=>{constrequest=http.get(`http://127.0.0.1:${server.address().port}/`,{ agent,signal: controller.signal},(incoming)=>{incoming.on("error",(error)=>console.log("response error:",error.code));resolve(incoming);});request.on("error",(error)=>{console.log("request error:",error.code);reject(error);});});try{forawait(constchunkofresponse){console.log("received bytes:",chunk.length);controller.abort(newError("stop reading"));break;}}catch(error){console.log("iteration error:",error.code??error.message);}// Leave time for deferred events; a caught rejection alone proves too little.awaitdelay(50);console.log("survived");}finally{agent.destroy();server.closeAllConnections();awaitnewPromise((resolve)=>server.close(resolve));}
How often does it reproduce? Is there a required condition?
The exact script above failed in all 80 fresh-process runs across the four
versions in the table. Each run had a five-second external timeout; none of
these failures was a timeout. The process exited with code 1 and an unhandled ABORT_ERR on a Socket.
The trigger is cancellation from inside the response's async iteration while
using an agent with keepAlive: true. A one-byte response is sufficient. No
socket reuse by a second request, large response, concurrency or machine load
is required by this reproducer.
Control experiments changed one aspect of the script at a time. Each variant
was run ten times on each of v24.21.0 and v26.8.1:
Single change
Result on v24.21.0
Result on v26.8.1
Set the agent's keepAlive to false
10/10 exit 0, reaches survived
10/10 exit 0, reaches survived
Omit signal from http.get options
10/10 exit 0, reaches survived
10/10 exit 0, reaches survived
Remove the controller.abort(...) call
10/10 exit 0, reaches survived
10/10 exit 0, reaches survived
Remove break, retaining cancellation
10/10 unhandled Socket error
10/10 unhandled Socket error
The last control shows that explicit early termination of the iterator is not
required for this particular response. These observations describe this local
reproducer; they do not establish all affected network timings or environments.
What is the expected behavior? Why is that the expected behavior?
Cancellation should be delivered through the HTTP request/response error paths
or an iterator rejection that the application can handle. With those paths
handled, this script should reach survived and exit 0 instead of terminating
through an unhandled error on the underlying socket.
The HTTP request documentation
describes request cancellation with an AbortSignal as request destruction and
documents an error event carrying ABORT_ERR and the supplied cause. Here,
handlers are registered on both public HTTP objects, but the error escapes
through their underlying socket.
What do you see instead?
Representative output from v26.8.1; the local script path has been normalized
to .../repro.mjs:
received bytes: 1
node:events:505
throw er; // Unhandled 'error' event
^
AbortError: The operation was aborted
at stream.<computed>.AbortError.cause (node:internal/streams/add-abort-signal:47:22)
at [nodejs.internal.kHybridDispatch] (node:internal/event_target:848:20)
at AbortSignal.dispatchEvent (node:internal/event_target:789:26)
at runAbort (node:internal/abort_controller:554:10)
at abortSignal (node:internal/abort_controller:518:3)
at AbortController.abort (node:internal/abort_controller:573:5)
at file:///.../repro.mjs:31:18
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
Emitted 'error' event on Socket instance at:
at emitErrorNT (node:internal/streams/destroy:170:8)
at emitErrorCloseNT (node:internal/streams/destroy:129:3)
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
code: 'ABORT_ERR',
[cause]: Error: stop reading
at file:///.../repro.mjs:31:24
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
}
Node.js v26.8.1
Neither the request handler, the response handler nor the iterator catch handles
this socket error. The survived line is absent.
Additional information
Observed lifecycle and suspected cause
Additional diagnostic instrumentation on v24.21.0 showed:
Immediately before cancellation inside the loop, request.destroyed was false, response.complete was true, and response.destroyed was false.
The socket still had socketErrorListener registered.
After cancellation, Node removed socketErrorListener. A stack captured
from the socket's removeListener event identified responseKeepAlive()
called by IncomingMessage.responseOnEnd().
An observer using the public events.errorMonitor symbol then observed ABORT_ERR with socket.listenerCount("error") === 0. The observer did not
consume the error, and the process still exited 1.
Relevant v24.21.0 source
removes the socket's error listener during responseKeepAlive() and explicitly
notes a gap before the deferred free notification. The request destruction path
passes its error to the socket. These observations suggest a race between
response completion/keep-alive cleanup and deferred error emission after abort.
This is a diagnosis hypothesis, not a bisected root cause or proposed core patch.
Application impact
This was originally found in an HTTP(S) client that aborts downloads when its
response-size limit is exceeded. The application handled its ordinary request
failure, but a later unhandled socket error terminated the whole process.
The larger application reproduction also failed over HTTPS with a TLSSocket.
The standalone HTTP example above removes that application's body limit and
shows that a large response or TLS is not necessary.
The one-line keepAlive: false control avoids the failure in this example, but
changes connection reuse. In the application, a local workaround owns abort
handling, rejects with the original reason, and closes the request/response
without supplying an error to their destroy() calls. Neither observation is
presented as the required Node.js fix.
Related issues and pull requests checked
Search and review date: 2026-09-09. Both open and closed entries were included.
I did not find an exact current report containing this AbortSignal/async-iteration
reproducer. The following entries appear related, but equivalence has not been
established:
Historical unhandled socket error after explicit socket.destroy(error) on Node 12. The closing comment cites the end of Node 12 releases, not a verified fix. This may share the socket-listener lifecycle gap; the new reproduction uses a request AbortSignal on supported releases.
Uses fetch, Readable.fromWeb() and .pipe(), with an error on a Readable. The present example uses core HTTP objects directly and handles errors on both of them.
Addresses two calls to responseKeepAlive() through request-finish ordering. Related lifecycle area; this report has a pending abort error on a destroyed socket.
Addresses server-side request iteration leaving an incomplete connection reusable. The present report concerns client response cancellation and process termination.
Searches included combinations of AbortError, AbortController, keepAlive, keep-alive, http.request, IncomingMessage, unhandled, socketErrorListener and responseKeepAlive, scoped to repo:nodejs/node.
This search is not proof that no duplicate exists. Please link or redirect this
report if an existing issue covers the same behavior.
Activity
AboEl3iz commentedon Sep 9, 2026
/assign
http: don't destroy socket after request completes
http: don't destroy socket after request completes
http: don't destroy socket after request completes
http: don't destroy socket after request completes