From d6d0e249fdf1a7e8227c58a86ecea9a0f8849541 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 22 Sep 2025 11:21:15 +0200 Subject: [PATCH] chore: make client-h2.js more resilient --- lib/dispatcher/client-h2.js | 66 ++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/lib/dispatcher/client-h2.js b/lib/dispatcher/client-h2.js index 5776f4fb207..2df9c0a8a94 100644 --- a/lib/dispatcher/client-h2.js +++ b/lib/dispatcher/client-h2.js @@ -32,6 +32,10 @@ const { } = require('../core/symbols.js') const { channels } = require('../core/diagnostics.js') +/** @typedef {import('../../types/client.js').default} Client */ +/** @typedef {import('http2').ClientHttp2Session} ClientHttp2Session */ +/** @typedef {import('node:net').Socket} Socket */ + const kOpenStreams = Symbol('open streams') let extractBody @@ -77,6 +81,10 @@ function parseH2Headers (headers) { return result } +/** + * @param {Client} client + * @param {Socket} socket + */ function connectH2 (client, socket) { client[kSocket] = socket @@ -151,6 +159,10 @@ function resumeH2 (client) { } } +/** + * @this {ClientHttp2Session} + * @param {Error} err + */ function onHttp2SessionError (err) { assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') @@ -158,6 +170,12 @@ function onHttp2SessionError (err) { this[kClient][kOnError](err) } +/** + * @this {ClientHttp2Session} + * @param {number} type + * @param {number} code + * @param {number} id + */ function onHttp2FrameError (type, code, id) { if (id === 0) { const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) @@ -166,6 +184,7 @@ function onHttp2FrameError (type, code, id) { } } +/** @this {ClientHttp2Session} */ function onHttp2SessionEnd () { const err = new SocketError('other side closed', util.getSocketInfo(this[kSocket])) this.destroy(err) @@ -177,23 +196,19 @@ function onHttp2SessionEnd () { * We need to handle GOAWAY frames properly, and trigger the session close * along with the socket right away * - * @this {import('http2').ClientHttp2Session} + * @this {ClientHttp2Session} * @param {number} errorCode + * @param {number} lastStreamID + * @param {Buffer} opaqueData */ -function onHttp2SessionGoAway (errorCode) { +function onHttp2SessionGoAway (errorCode, lastStreamID, opaqueData) { // TODO(mcollina): Verify if GOAWAY implements the spec correctly: // https://datatracker.ietf.org/doc/html/rfc7540#section-6.8 // Specifically, we do not verify the "valid" stream id. - - const err = this[kError] || new SocketError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`, util.getSocketInfo(this[kSocket])) + const err = this[kError] || new SocketError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`, this[kSocket] && util.getSocketInfo(this[kSocket])) const client = this[kClient] - client[kSocket] = null - client[kHTTPContext] = null - - // this is an HTTP2 session this.close() - this[kHTTP2Session] = null util.destroy(this[kSocket], err) @@ -213,9 +228,10 @@ function onHttp2SessionGoAway (errorCode) { client[kResume]() } +/** @this {ClientHttp2Session} */ function onHttp2SessionClose () { - const { [kClient]: client } = this - const { [kSocket]: socket } = client + const client = this[kClient] + const socket = client[kSocket] const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket)) @@ -234,6 +250,7 @@ function onHttp2SessionClose () { } } +/** @this {Socket} */ function onHttp2SocketClose () { const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) @@ -255,6 +272,10 @@ function onHttp2SocketClose () { client[kResume]() } +/** + * @this {Socket} + * @param {Error} err + */ function onHttp2SocketError (err) { assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') @@ -263,15 +284,30 @@ function onHttp2SocketError (err) { this[kClient][kOnError](err) } +/** @this {Socket} */ function onHttp2SocketEnd () { util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) } +/** @this {Socket} */ function onSocketClose () { this[kClosed] = true } -// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 +/** + * @overload + * @param {'GET' | 'HEAD' | 'OPTIONS' | 'TRACE' | 'CONNECT'} method + * @returns {false} + *//** + * @overload + * @param {Uppercase} method + * @returns {true} + *//** + * @param {Uppercase} method + * @returns {boolean} + * + * @see https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 + */ function shouldSendContentLength (method) { return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT' } @@ -318,7 +354,7 @@ function writeH2 (client, request) { } /** @type {import('node:http2').ClientHttp2Stream} */ - let stream = null + let stream const { hostname, port } = client[kUrl] @@ -334,7 +370,7 @@ function writeH2 (client, request) { util.errorRequest(client, request, err) - if (stream != null) { + if (stream !== undefined) { // Some chunks might still come after abort, // let's ignore them stream.removeAllListeners('data') @@ -371,7 +407,7 @@ function writeH2 (client, request) { // `ready` event is triggered // We disabled endStream to allow the user to write to the stream stream = session.request(headers, { endStream: false, signal }) - stream.on('response', headers => { + stream.on('response', (headers, flags, rawHeaders) => { const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)