diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 0b9be932af3479..27fe4f26e31757 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -1464,6 +1464,14 @@ function afterShutdown() { } function finishSendTrailers(stream, headersList) { + // The stream might be destroyed and in that case + // there is nothing to do. + // This can happen because finishSendTrailers is + // scheduled via setImmediate. + if (stream.destroyed) { + return; + } + stream[kState].flags &= ~STREAM_FLAGS_HAS_TRAILERS; const ret = stream[kHandle].trailers(headersList); diff --git a/test/parallel/test-http2-compat-socket-destroy-delayed.js b/test/parallel/test-http2-compat-socket-destroy-delayed.js new file mode 100644 index 00000000000000..62405047d8266e --- /dev/null +++ b/test/parallel/test-http2-compat-socket-destroy-delayed.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common'); +const { mustCall } = common; + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const http2 = require('http2'); +const assert = require('assert'); + +const { + HTTP2_HEADER_PATH, + HTTP2_HEADER_METHOD, +} = http2.constants; + +// This tests verifies that calling `req.socket.destroy()` via +// setImmediate does not crash. +// Fixes https://github.com/nodejs/node/issues/22855. + +const app = http2.createServer(mustCall((req, res) => { + res.end('hello'); + setImmediate(() => req.socket.destroy()); +})); + +app.listen(0, mustCall(() => { + const session = http2.connect(`http://localhost:${app.address().port}`); + const request = session.request({ + [HTTP2_HEADER_PATH]: '/', + [HTTP2_HEADER_METHOD]: 'get' + }); + request.once('response', mustCall((headers, flags) => { + let data = ''; + request.on('data', (chunk) => { data += chunk; }); + request.on('end', mustCall(() => { + assert.strictEqual(data, 'hello'); + session.close(); + app.close(); + })); + })); + request.end(); +}));