http2: settle pending write callbacks on destroy

When a stream is destroyed while a write is still in flight, nghttp2 may
have handed the data to the socket and never report the write completion
once the stream or session tears down. That leaves the write callback
unresolved, the Writable stuck, and the event loop never drains, which
times out test-http2-close-while-writing on macOS.

Settle the in-flight write in Http2Stream._destroy by invoking its
callback with the destroy error and resetting writePending. A later
native completion callback is then a no-op because writeCb is null and
writePending is 0, so the settle is idempotent. Passing the error rather
than null also drains any buffered writes.

Fixes: https://github.com/nodejs/node/issues/58252

Signed-off-by: Matteo Collina <hello@matteocollina.com>
Assisted-by: pi-coding-agent
PR-URL: https://github.com/nodejs/node/pull/66016
Fixes: https://github.com/nodejs/node/issues/58252
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Xuguang Mei <meixuguang@gmail.com>
This commit is contained in:
Matteo Collina authored and GitHub committed 2026-09-15 16:06:11 +00:00
1 parent 84e91744b9
commit e13c138a2c
2 files changed
+21 -2

No files matched your search

+9
View File
@@ -2730,6 +2730,15 @@ class Http2Stream extends Duplex {
});
}
}
// A write in flight may never get its native completion callback once the
// stream tears down; settle it so the Writable can clean up.
if (state.writeCb !== null) {
const writeCb = state.writeCb;
state.writeCb = null;
state.writePending = 0;
writeCb(err);
}
callback(err);
}
// The Http2Stream can be destroyed if it has closed and if the readable
@@ -29,11 +29,21 @@ server.on('session', common.mustCall(function(session) {
stream.on('error', common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ABORTED');
}));
stream.resume();
// Every write dispatched before close must have its callback invoked.
let writes = 0;
let writeCallbacks = 0;
stream.on('data', function() {
this.write(Buffer.alloc(1));
writes++;
this.write(Buffer.alloc(1), () => {
writeCallbacks++;
});
process.nextTick(() => client_stream.destroy());
});
stream.on('close', common.mustCall(() => {
assert.strictEqual(writeCallbacks, writes);
}));
stream.resume();
}));
}));