From c16f526f55637a0063975755a15c9c10a1eb4c9e Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sat, 14 Feb 2026 12:06:55 +0100 Subject: [PATCH] http2: invoke pending write callback on stream destroy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an HTTP/2 stream is destroyed while a write is in progress, the pending write callback may never be called if the write data has already been consumed by nghttp2 and moved to the session's outgoing_buffers_. In this case, the C++ side's SetImmediate cleanup finds nothing in the stream's write queue, and the callback depends on session socket write completion — which may never happen during shutdown. This leaves the Writable stream's internal state stuck, preventing cleanup of buffered writes and keeping references alive that block event loop exit. Track the pending write callback on the stream and invoke it in _destroy() before calling handle.destroy(), ensuring the Writable stream can clean up properly. The callback is made idempotent so duplicate invocations from the C++ side are harmless no-ops. Fixes: https://github.com/nodejs/node/issues/58252 Refs: https://github.com/nodejs/node/pull/58253 --- lib/internal/http2/core.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 8b526c001004c5..242d337fd4b110 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -253,6 +253,7 @@ const kOptions = Symbol('options'); const kOwner = owner_symbol; const kOrigin = Symbol('origin'); const kPendingRequestCalls = Symbol('kPendingRequestCalls'); +const kPendingWriteCb = Symbol('kPendingWriteCb'); const kProceed = Symbol('proceed'); const kRemoteSettings = Symbol('remote-settings'); const kRequestAsyncResource = Symbol('requestAsyncResource'); @@ -2276,10 +2277,13 @@ class Http2Stream extends Duplex { cb(err); }; const writeCallback = (err) => { + if (!waitingForWriteCallback) return; + this[kPendingWriteCb] = null; waitingForWriteCallback = false; writeCallbackErr = err; done(); }; + this[kPendingWriteCb] = writeCallback; const endCheckCallback = (err) => { waitingForEndCheck = false; endCheckCallbackErr = err; @@ -2445,6 +2449,12 @@ class Http2Stream extends Duplex { closeStream(this, code, hasHandle ? kForceRstStream : kNoRstStream); this.push(null); + const pendingWriteCb = this[kPendingWriteCb]; + if (pendingWriteCb) { + this[kPendingWriteCb] = null; + pendingWriteCb(err); + } + if (hasHandle) { handle.destroy(); sessionState.streams.delete(id);