node:net: emit 'error' once when a buffered write on an adopted fd fails
RO
20d ago+74+74+01234567890123456789-20-20-01234567890123456789
Problem
- A
net.Socketthat adopts an fd (socket.connect({ fd })) emits'error'two times when the flush of a buffered write fails with a fatalsend()errno. Every connection that anode:clusterworker accepts is such a socket. Node emits'error'one time. Observed:["write:EPIPE","error:EPIPE","error:EPIPE","close:true"]. - The cause is
SocketHandlers.errorinsrc/js/node/net.ts:538. It fails the parked write callback with the error. The Writable then destroys the stream and queues'error'. After that, the handler callsself.emit("error", error)a second time.
Fix
SocketHandlers.errornow callsself.destroy(error)when the socket is not destroyed yet, instead ofself.emit("error", error). This is the shape thatServerHandlers.erroralready uses for plain TCP.destroy(error)owns the single'error'emission through the stream'serrorEmittedguard. Whencallback(error)already destroyed the stream, thedestroyedcheck makes the second step a no-op. The socket now also closes withhadErrortrue, as Node does.- Verified:
test/js/node/net/net-syscall-fault.test.ts(new cluster worker case, fails on main with twoerror:EPIPEevents). Alsotest/js/node/net/node-net.test.ts(same result set as main) and all 88test-cluster-*.jsfiles in the Node suite.
Background
node:netbinds one of three native handler tables to a socket.SocketHandlersis bound bysocket.connect({ fd }), whichonClusterConnectionuses for every accepted connection in a worker.SocketHandlers2is bound by a normalnet.connect().ServerHandlersis bound by accepted sockets of anet.createServerin the same process. The other two tables already route the error throughdestroy(error).- The native caller is the fatal-flush path of
on_writableinsrc/runtime/socket/socket_body.rs. When a flush ofbuffered_data_for_node_netfails with a fatal errno, it calls the error handler with awriteerrno, then closes the socket. The test uses the in-tree socket fault injection to make the firstsend()short and the second one fail withEPIPE.
Fixes #43030
Notes
- The same handler also receives an exception that a JS callback throws (drain, end, open). Those now destroy the socket with the exception instead of leaving it open after an
'error'emit. A thrown'data'listener does not reach this handler:pushDataToSocketreports it as an uncaught exception. node-net.test.tshas 11 failures in this container both with and without the change. They areECONNREFUSEDon fixed ports and a 60 s timeout, unrelated to the handler.- #43026 adds the tail of
on_openas a second caller of the same handler. It does not change the handler. Both PRs add tests tonet-syscall-fault.test.ts.
no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/net/net-syscall-fault.test.ts