88 RequestAbortedError,
99 SocketError,
1010 InformationalError,
11- InvalidArgumentError
11+ InvalidArgumentError,
12+ HeadersTimeoutError,
13+ BodyTimeoutError
1214} = require ( '../core/errors.js' )
1315const {
1416 kUrl,
@@ -33,6 +35,7 @@ const {
3335 kHTTPContext,
3436 kClosed,
3537 kBodyTimeout,
38+ kHeadersTimeout,
3639 kEnableConnectProtocol,
3740 kRemoteSettings,
3841 kHTTP2Stream,
@@ -219,7 +222,11 @@ function resumeH2 (client) {
219222 const socket = client [ kSocket ]
220223
221224 if ( socket ?. destroyed === false ) {
222- if ( client [ kSize ] === 0 || client [ kMaxConcurrentStreams ] === 0 ) {
225+ // Only let the process exit when there is genuinely nothing outstanding.
226+ // Unreffing because the peer advertised MAX_CONCURRENT_STREAMS = 0 left
227+ // queued requests with nothing holding the event loop open, so the process
228+ // could exit with status 0 while an awaited request never settled.
229+ if ( client [ kSize ] === 0 ) {
223230 socket . unref ( )
224231 client [ kHTTP2Session ] . unref ( )
225232 } else {
@@ -314,6 +321,36 @@ function onHttp2SessionEnd () {
314321 * @this {import('http2').ClientHttp2Session}
315322 * @param {number } errorCode
316323 */
324+ // Backport of #5410 and #5569. HTTP/2 multiplexes, so requests complete out of
325+ // order; advancing kRunningIdx blindly retired whichever request happened to
326+ // sit at the head instead of the one that actually finished, which both lost
327+ // requests and left phantom running slots behind.
328+ function completeRequest ( client , request , resetPendingIdx = false ) {
329+ const queue = client [ kQueue ]
330+ const runningIdx = client [ kRunningIdx ]
331+
332+ // In-order completion: clear the request and advance without splicing.
333+ // The client's resume loop compacts cleared slots once the index grows.
334+ if ( runningIdx < client [ kPendingIdx ] && queue [ runningIdx ] === request ) {
335+ queue [ runningIdx ] = null
336+ client [ kRunningIdx ] = runningIdx + 1
337+ return
338+ }
339+
340+ const index = queue . indexOf ( request , runningIdx )
341+
342+ if ( index === - 1 || index >= client [ kPendingIdx ] ) {
343+ return
344+ }
345+
346+ queue . splice ( index , 1 )
347+ client [ kPendingIdx ] --
348+
349+ if ( resetPendingIdx && client [ kPendingIdx ] < client [ kRunningIdx ] ) {
350+ client [ kPendingIdx ] = client [ kRunningIdx ]
351+ }
352+ }
353+
317354function onHttp2SessionGoAway ( errorCode ) {
318355 // TODO(mcollina): Verify if GOAWAY implements the spec correctly:
319356 // https://datatracker.ietf.org/doc/html/rfc7540#section-6.8
@@ -335,7 +372,9 @@ function onHttp2SessionGoAway (errorCode) {
335372 if ( client [ kRunningIdx ] < client [ kQueue ] . length ) {
336373 const request = client [ kQueue ] [ client [ kRunningIdx ] ]
337374 client [ kQueue ] [ client [ kRunningIdx ] ++ ] = null
338- util . errorRequest ( client , request , err )
375+ if ( request != null ) {
376+ util . errorRequest ( client , request , err )
377+ }
339378 client [ kPendingIdx ] = client [ kRunningIdx ]
340379 }
341380
@@ -368,7 +407,9 @@ function onHttp2SessionClose () {
368407 const requests = client [ kQueue ] . splice ( client [ kRunningIdx ] )
369408 for ( let i = 0 ; i < requests . length ; i ++ ) {
370409 const request = requests [ i ]
371- util . errorRequest ( client , request , err )
410+ if ( request != null ) {
411+ util . errorRequest ( client , request , err )
412+ }
372413 }
373414 }
374415}
@@ -416,7 +457,10 @@ function shouldSendContentLength (method) {
416457}
417458
418459function writeH2 ( client , request ) {
419- const requestTimeout = request . bodyTimeout ?? client [ kBodyTimeout ]
460+ // Time to the response headers, then time between body chunks. Using
461+ // bodyTimeout for both made headersTimeout a no-op over HTTP/2.
462+ const headersTimeout = request . headersTimeout ?? client [ kHeadersTimeout ]
463+ const bodyTimeout = request . bodyTimeout ?? client [ kBodyTimeout ]
420464 const session = client [ kHTTP2Session ]
421465 const { method, path, host, upgrade, expectContinue, signal, protocol, headers : reqHeaders } = request
422466 let { body } = request
@@ -483,6 +527,7 @@ function writeH2 (client, request) {
483527
484528 // We move the running index to the next request
485529 client [ kOnError ] ( err )
530+ completeRequest ( client , request )
486531 client [ kResume ] ( )
487532 }
488533
@@ -537,7 +582,7 @@ function writeH2 (client, request) {
537582 request . onUpgrade ( statusCode , parseH2Headers ( realHeaders ) , stream )
538583
539584 ++ session [ kOpenStreams ]
540- client [ kQueue ] [ client [ kRunningIdx ] ++ ] = null
585+ completeRequest ( client , request )
541586 } )
542587
543588 stream . on ( 'error' , ( ) => {
@@ -554,7 +599,7 @@ function writeH2 (client, request) {
554599 if ( session [ kOpenStreams ] === 0 ) session . unref ( )
555600 } )
556601
557- stream . setTimeout ( requestTimeout )
602+ stream . setTimeout ( headersTimeout )
558603 return true
559604 }
560605
@@ -570,13 +615,14 @@ function writeH2 (client, request) {
570615
571616 request . onUpgrade ( statusCode , parseH2Headers ( realHeaders ) , stream )
572617 ++ session [ kOpenStreams ]
573- client [ kQueue ] [ client [ kRunningIdx ] ++ ] = null
618+ completeRequest ( client , request )
574619 } )
620+ stream . on ( 'error' , abort )
575621 stream . once ( 'close' , ( ) => {
576622 session [ kOpenStreams ] -= 1
577623 if ( session [ kOpenStreams ] === 0 ) session . unref ( )
578624 } )
579- stream . setTimeout ( requestTimeout )
625+ stream . setTimeout ( headersTimeout )
580626
581627 return true
582628 }
@@ -677,7 +723,7 @@ function writeH2 (client, request) {
677723
678724 // Increment counter as we have new streams open
679725 ++ session [ kOpenStreams ]
680- stream . setTimeout ( requestTimeout )
726+ stream . setTimeout ( headersTimeout )
681727
682728 // Track whether we received a response (headers)
683729 let responseReceived = false
@@ -686,6 +732,7 @@ function writeH2 (client, request) {
686732 const { [ HTTP2_HEADER_STATUS ] : statusCode , ...realHeaders } = headers
687733 request . onResponseStarted ( )
688734 responseReceived = true
735+ stream . setTimeout ( bodyTimeout )
689736
690737 // Due to the stream nature, it is possible we face a race condition
691738 // where the stream has been assigned, but the request has been aborted
@@ -720,14 +767,13 @@ function writeH2 (client, request) {
720767 request . onComplete ( { } )
721768 }
722769
723- client [ kQueue ] [ client [ kRunningIdx ] ++ ] = null
770+ completeRequest ( client , request )
724771 client [ kResume ] ( )
725772 } else {
726773 // Stream ended without receiving a response - this is an error
727774 // (e.g., server destroyed the stream before sending headers)
728775 abort ( new InformationalError ( 'HTTP/2: stream half-closed (remote)' ) )
729- client [ kQueue ] [ client [ kRunningIdx ] ++ ] = null
730- client [ kPendingIdx ] = client [ kRunningIdx ]
776+ completeRequest ( client , request , true )
731777 client [ kResume ] ( )
732778 }
733779 } )
@@ -738,6 +784,14 @@ function writeH2 (client, request) {
738784 if ( session [ kOpenStreams ] === 0 ) {
739785 session . unref ( )
740786 }
787+
788+ // A stream can close without ever emitting 'end' or 'error': a peer's
789+ // RST_STREAM(CANCEL) received before the response is reported by Node as a
790+ // bare 'close', and destroying the stream unenrolls its timeout, so no
791+ // 'timeout' follows either. Nothing else would ever settle this request.
792+ if ( ! request . aborted && ! request . completed ) {
793+ abort ( new InformationalError ( 'HTTP/2: stream closed before the response was complete' ) )
794+ }
741795 } )
742796
743797 stream . once ( 'error' , function ( err ) {
@@ -755,7 +809,9 @@ function writeH2 (client, request) {
755809 } )
756810
757811 stream . on ( 'timeout' , ( ) => {
758- const err = new InformationalError ( `HTTP/2: "stream timeout after ${ requestTimeout } "` )
812+ const err = responseReceived
813+ ? new BodyTimeoutError ( `HTTP/2: "body timeout after ${ bodyTimeout } "` )
814+ : new HeadersTimeoutError ( `HTTP/2: "headers timeout after ${ headersTimeout } "` )
759815 stream . removeAllListeners ( 'data' )
760816 session [ kOpenStreams ] -= 1
761817
0 commit comments