http: emit 'drain' after flushing coalesced chunks

Merged
JU
2d ago
+86-0

Since #64987, chunked writes made while the response is auto-corked are coalesced, and write() returns false based on the byte length of the buffered chunks. When uncork() flushes them, net.Socket counts string chunks in UTF-16 code units, so with multi-byte text the socket can stay below its own highWaterMark and never emits 'drain'. If the flushed writes complete asynchronously, the check at the end of uncork() also runs while bytes are still pending, so the response stalls with writableNeedDrain === true and writableLength === 0.

This re-checks the drain condition in the write callback of the flushed data, unless the message or its socket has been destroyed. It does not change the accounting itself (e.g. encoding the coalesced strings to Buffers), to avoid adding cost to the write path that #64987 optimized.

The new test uses a Duplex that counts strings like net.Socket and completes writes asynchronously, so it does not depend on OS socket buffer sizes. It hangs on main without the change and passes with it. It also checks that 'drain' is not emitted after the response is destroyed from a write callback. parallel/test-http*, parallel/test-stream*, parallel/test-net*, sequential/test-http* and make lint-js pass locally.

Fixes: https://github.com/nodejs/node/issues/66781
Refs: https://github.com/nodejs/node/pull/64987

I used an AI coding agent to help investigate and write this change. I reproduced the issue and verified the fix and the tests myself.