diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index 188d76c6a42328..ad1e9c40733573 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -1210,6 +1210,13 @@ Emitted when server sends a response. Emitted when a stream is created on the client. +`http2.client.stream.start` + +* `stream` {ClientHttp2Stream} +* `headers` {HTTP/2 Headers Object} + +Emitted when a stream is started on the client. + #### Modules `module.require.start` diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index e43c43fcaf2f8d..ce9f81bebc2494 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -186,6 +186,7 @@ const { _connectionListener: httpConnectionListener } = http; const dc = require('diagnostics_channel'); const onClientStreamCreatedChannel = dc.channel('http2.client.stream.created'); +const onClientStreamStartChannel = dc.channel('http2.client.stream.start'); let debug = require('internal/util/debuglog').debuglog('http2', (fn) => { debug = fn; @@ -381,6 +382,12 @@ function onSessionHeaders(handle, id, cat, flags, headers, sensitiveHeaders) { headers: obj, }); } + if (onClientStreamStartChannel.hasSubscribers) { + onClientStreamStartChannel.publish({ + stream, + headers: obj, + }); + } if (endOfStream) { stream.push(null); } @@ -717,7 +724,7 @@ function onGoawayData(code, lastStreamID, buf) { // When a ClientHttp2Session is first created, the socket may not yet be // connected. If request() is called during this time, the actual request // will be deferred until the socket is ready to go. -function requestOnConnect(headers, options) { +function requestOnConnect(headersList, headersParam, options) { const session = this[kSession]; // At this point, the stream should have already been destroyed during @@ -744,7 +751,7 @@ function requestOnConnect(headers, options) { // `ret` will be either the reserved stream ID (if positive) // or an error code (if negative) - const ret = session[kHandle].request(headers, + const ret = session[kHandle].request(headersList, streamOptions, options.parent | 0, options.weight | 0, @@ -776,6 +783,12 @@ function requestOnConnect(headers, options) { return; } this[kInit](ret.id(), ret); + if (onClientStreamStartChannel.hasSubscribers) { + onClientStreamStartChannel.publish({ + stream: this, + headers: headersParam, + }); + } } // Validates that priority options are correct, specifically: @@ -1859,7 +1872,7 @@ class ClientHttp2Session extends Http2Session { } } - const onConnect = reqAsync.bind(requestOnConnect.bind(stream, headersList, options)); + const onConnect = reqAsync.bind(requestOnConnect.bind(stream, headersList, headersParam, options)); if (this.connecting) { if (this[kPendingRequestCalls] !== null) { this[kPendingRequestCalls].push(onConnect); diff --git a/test/parallel/test-diagnostics-channel-http2-client-stream-start.js b/test/parallel/test-diagnostics-channel-http2-client-stream-start.js new file mode 100644 index 00000000000000..e8edf3a0a49f4a --- /dev/null +++ b/test/parallel/test-diagnostics-channel-http2-client-stream-start.js @@ -0,0 +1,59 @@ +'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.client.stream.start' channel when +// ClientHttp2Streams are started by both: +// - the client calling ClientHttp2Session#request() +// - in response to an incoming 'push' event from the server + +const Countdown = require('../common/countdown'); +const assert = require('assert'); +const dc = require('diagnostics_channel'); +const http2 = require('http2'); +const { Duplex } = require('stream'); + +const clientHttp2StreamStartCount = 2; + +dc.subscribe('http2.client.stream.start', common.mustCall(({ stream, headers }) => { + // Since ClientHttp2Stream is not exported from any module, this just checks + // if the stream is an instance of Duplex. + assert.ok(stream instanceof Duplex); + assert.strictEqual(stream.constructor.name, 'ClientHttp2Stream'); + assert.ok(headers && !Array.isArray(headers) && typeof headers === 'object'); +}, clientHttp2StreamStartCount)); + +const server = http2.createServer(); +server.on('stream', common.mustCall((stream) => { + stream.respond(); + stream.end(); + + stream.pushStream({}, common.mustSucceed((pushStream) => { + pushStream.respond(); + pushStream.end(); + }, 1)); +}, 1)); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = http2.connect(`http://localhost:${port}`); + + const countdown = new Countdown(clientHttp2StreamStartCount, () => { + 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(); + }, 1)); + }, 1)); +}, 1));