diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index b9904df6d9e058..5ba16796b06dae 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -1260,6 +1260,14 @@ Emitted when a stream is started on the server. Emitted when an error occurs during the processing of a stream on the server. +`http2.server.stream.finish` + +* `stream` {ServerHttp2Stream} +* `headers` {HTTP/2 Headers Object} +* `flags` {number} + +Emitted when a stream is sent on the server. + #### Modules > Stability: 1 - Experimental diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 8c63284467707b..14bb7e3d8597e1 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -193,6 +193,7 @@ const onClientStreamCloseChannel = dc.channel('http2.client.stream.close'); 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'); let debug = require('internal/util/debuglog').debuglog('http2', (fn) => { debug = fn; @@ -2917,8 +2918,17 @@ class ServerHttp2Stream extends Http2Stream { } const ret = this[kHandle].respond(headersList, streamOptions); - if (ret < 0) + if (ret < 0) { this.destroy(new NghttpError(ret)); + } else if (onServerStreamFinishChannel.hasSubscribers) { + // No point in running this if the respond() call above fails because + // that would mean that it is an invalid call. + onServerStreamFinishChannel.publish({ + stream: this, + headers, + flags: state.flags, + }); + } } // Initiate a response using an open FD. Note that there are fewer diff --git a/test/parallel/test-diagnostics-channel-http2-server-stream-finish.js b/test/parallel/test-diagnostics-channel-http2-server-stream-finish.js new file mode 100644 index 00000000000000..6a27cc1408ad77 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-http2-server-stream-finish.js @@ -0,0 +1,61 @@ +'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.finish' channel when +// ServerHttp2Streams#respond() sends a regular stream as well as a push stream. + +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const dc = require('diagnostics_channel'); +const http2 = require('http2'); +const { Duplex } = require('stream'); + +const serverHttp2StreamFinishCount = 2; + +dc.subscribe('http2.server.stream.finish', common.mustCall(({ stream, headers, flags }) => { + // 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.ok(headers && !Array.isArray(headers) && typeof headers === 'object'); + + assert.strictEqual(typeof flags, 'number'); +}, serverHttp2StreamFinishCount)); + +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(serverHttp2StreamFinishCount, () => { + 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(); + })); + })); +}));