From ec237ddc96f7d511d2eec50a8d3e8df5ee41d51b Mon Sep 17 00:00:00 2001 From: Darshan Sen Date: Wed, 14 May 2025 13:19:57 +0530 Subject: [PATCH] http2: add diagnostics channel 'http2.client.stream.close' Signed-off-by: Darshan Sen --- doc/api/diagnostics_channel.md | 7 +++ lib/internal/http2/core.js | 7 +++ ...channel-http2-client-stream-close-error.js | 44 +++++++++++++ ...stics-channel-http2-client-stream-close.js | 63 +++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 test/parallel/test-diagnostics-channel-http2-client-stream-close-error.js create mode 100644 test/parallel/test-diagnostics-channel-http2-client-stream-close.js diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index aef7f3e6ec88e4..fd1b19652cd31a 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -1217,6 +1217,13 @@ Emitted when a stream is created on the client. Emitted when a stream is started on the client. +`http2.client.stream.close` + +* `stream` {ClientHttp2Stream} + +Emitted when a stream is closed on the client. 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 ce9f81bebc2494..e6246b57373a99 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -187,6 +187,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'); +const onClientStreamCloseChannel = dc.channel('http2.client.stream.close'); let debug = require('internal/util/debuglog').debuglog('http2', (fn) => { debug = fn; @@ -1968,6 +1969,7 @@ const kSubmitRstStream = 1; const kForceRstStream = 2; function closeStream(stream, code, rstStreamStatus = kSubmitRstStream) { + const type = stream[kSession][kType]; const state = stream[kState]; state.flags |= STREAM_FLAGS_CLOSED; state.rstCode = code; @@ -1998,6 +2000,11 @@ function closeStream(stream, code, rstStreamStatus = kSubmitRstStream) { else stream.once('finish', finishFn); } + + if (type === NGHTTP2_SESSION_CLIENT && + onClientStreamCloseChannel.hasSubscribers) { + onClientStreamCloseChannel.publish({ stream }); + } } function finishCloseStream(code) { diff --git a/test/parallel/test-diagnostics-channel-http2-client-stream-close-error.js b/test/parallel/test-diagnostics-channel-http2-client-stream-close-error.js new file mode 100644 index 00000000000000..b7b201eeaf3874 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-http2-client-stream-close-error.js @@ -0,0 +1,44 @@ +'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.close' channel when +// a ClientHttp2Stream 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.client.stream.close', common.mustCall(({ stream }) => { + // Since ClientHttp2Stream is not exported from any module, this just checks + // if the stream is an instance of Duplex and the constructor name is + // 'ClientHttp2Stream'. + assert.ok(stream instanceof Duplex); + assert.strictEqual(stream.constructor.name, 'ClientHttp2Stream'); + assert.strictEqual(stream.closed, true); + assert.strictEqual(stream.destroyed, true); + + assert.strictEqual(stream.rstCode, http2.constants.NGHTTP2_CANCEL); +})); + +const server = http2.createServer(); +server.listen(0, common.mustCall(() => { + const port = server.address().port; + const client = http2.connect(`http://localhost:${port}`); + + const ac = new AbortController(); + const stream = client.request({}, { signal: ac.signal }); + ac.abort(); + + stream.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ABORT_ERR'); + assert.strictEqual(err.name, 'AbortError'); + + client.close(); + server.close(); + })); +})); diff --git a/test/parallel/test-diagnostics-channel-http2-client-stream-close.js b/test/parallel/test-diagnostics-channel-http2-client-stream-close.js new file mode 100644 index 00000000000000..72fbe91195c7f4 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-http2-client-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.client.stream.close' channel when +// ClientHttp2Streams created by these actions are closed: +// - 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 clientHttp2StreamCloseCount = 2; + +dc.subscribe('http2.client.stream.close', common.mustCall(({ stream }) => { + // Since ClientHttp2Stream is not exported from any module, this just checks + // if the stream is an instance of Duplex and the constructor name is + // 'ClientHttp2Stream'. + assert.ok(stream instanceof Duplex); + assert.strictEqual(stream.constructor.name, 'ClientHttp2Stream'); + assert.strictEqual(stream.closed, true); + assert.strictEqual(stream.destroyed, false); + + assert.strictEqual(stream.rstCode, http2.constants.NGHTTP2_NO_ERROR); +}, clientHttp2StreamCloseCount)); + +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(clientHttp2StreamCloseCount, () => { + 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(); + })); + })); +}));