diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index 5ba16796b06..7f50ca2dbf3 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -1268,6 +1268,13 @@ Emitted when an error occurs during the processing of a stream on the server. Emitted when a stream is sent on the server. +`http2.server.stream.close` + +* `stream` {ServerHttp2Stream} + +Emitted when a stream is closed on the server. The HTTP/2 error code used when +closing the stream can be retrieved using the `stream.rstCode` property. + #### Modules > Stability: 1 - Experimental diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index f3f19025273..40f65a139be 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -196,6 +196,7 @@ const onServerStreamCreatedChannel = dc.channel('http2.server.stream.created'); const onServerStreamStartChannel = dc.channel('http2.server.stream.start'); const onServerStreamErrorChannel = dc.channel('http2.server.stream.error'); const onServerStreamFinishChannel = dc.channel('http2.server.stream.finish'); +const onServerStreamCloseChannel = dc.channel('http2.server.stream.close'); let debug = require('internal/util/debuglog').debuglog('http2', (fn) => { debug = fn; @@ -2011,9 +2012,12 @@ function closeStream(stream, code, rstStreamStatus = kSubmitRstStream) { stream.once('finish', finishFn); } - if (type === NGHTTP2_SESSION_CLIENT && - onClientStreamCloseChannel.hasSubscribers) { - onClientStreamCloseChannel.publish({ stream }); + if (type === NGHTTP2_SESSION_CLIENT) { + if (onClientStreamCloseChannel.hasSubscribers) { + onClientStreamCloseChannel.publish({ stream }); + } + } else if (onServerStreamCloseChannel.hasSubscribers) { + onServerStreamCloseChannel.publish({ stream }); } } diff --git a/test/parallel/test-diagnostics-channel-http2-server-stream-close-error.js b/test/parallel/test-diagnostics-channel-http2-server-stream-close-error.js new file mode 100644 index 00000000000..70dad2f928f --- /dev/null +++ b/test/parallel/test-diagnostics-channel-http2-server-stream-close-error.js @@ -0,0 +1,52 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +// This test ensures that the built-in HTTP/2 diagnostics channels are reporting +// the diagnostics messages for the 'http2.server.stream.close' channel when +// a ServerHttp2Stream is destroyed because of an error. + +const assert = require('assert'); +const dc = require('diagnostics_channel'); +const http2 = require('http2'); +const { Duplex } = require('stream'); + +dc.subscribe('http2.server.stream.close', common.mustCall(({ stream }) => { + // Since ServerHttp2Stream is not exported from any module, this just checks + // if the stream is an instance of Duplex and the constructor name is + // 'ServerHttp2Stream'. + assert.ok(stream instanceof Duplex); + assert.strictEqual(stream.constructor.name, 'ServerHttp2Stream'); + assert.strictEqual(stream.closed, true); + assert.strictEqual(stream.destroyed, true); + + assert.strictEqual(stream.rstCode, http2.constants.NGHTTP2_INTERNAL_ERROR); +})); + +const server = http2.createServer(); + +server.on('stream', common.mustCall((stream) => { + let expectedError; + + stream.on('error', common.mustCall((err) => { + assert.strictEqual(err, expectedError); + })); + + expectedError = new Error('HTTP/2 server stream error'); + stream.destroy(expectedError); +})); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = http2.connect(`http://localhost:${port}`); + + const stream = client.request({}); + stream.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_HTTP2_STREAM_ERROR'); + + client.close(); + server.close(); + })); +})); diff --git a/test/parallel/test-diagnostics-channel-http2-server-stream-close.js b/test/parallel/test-diagnostics-channel-http2-server-stream-close.js new file mode 100644 index 00000000000..baaae36ac1e --- /dev/null +++ b/test/parallel/test-diagnostics-channel-http2-server-stream-close.js @@ -0,0 +1,63 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +// This test ensures that the built-in HTTP/2 diagnostics channels are reporting +// the diagnostics messages for the 'http2.server.stream.close' channel when +// ServerHttp2Streams created by these actions are closed: +// - in response to an incoming 'stream' event from the client +// - the server calling ServerHttp2Stream#pushStream() + +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const dc = require('diagnostics_channel'); +const http2 = require('http2'); +const { Duplex } = require('stream'); + +const serverHttp2StreamCloseCount = 2; + +dc.subscribe('http2.server.stream.close', common.mustCall(({ stream }) => { + // Since ServerHttp2Stream is not exported from any module, this just checks + // if the stream is an instance of Duplex and the constructor name is + // 'ServerHttp2Stream'. + assert.ok(stream instanceof Duplex); + assert.strictEqual(stream.constructor.name, 'ServerHttp2Stream'); + assert.strictEqual(stream.closed, true); + assert.strictEqual(stream.destroyed, false); + + assert.strictEqual(stream.rstCode, http2.constants.NGHTTP2_NO_ERROR); +}, serverHttp2StreamCloseCount)); + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream) => { + stream.respond(); + stream.end(); + + stream.pushStream({}, common.mustSucceed((pushStream) => { + pushStream.respond(); + pushStream.end(); + })); +})); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = http2.connect(`http://localhost:${port}`); + + const countdown = new Countdown(serverHttp2StreamCloseCount, () => { + client.close(); + server.close(); + }); + + const stream = client.request({}); + stream.on('response', common.mustCall(() => { + countdown.dec(); + })); + + client.on('stream', common.mustCall((pushStream) => { + pushStream.on('push', common.mustCall(() => { + countdown.dec(); + })); + })); +}));