|
8 | 8 | RequestAbortedError, |
9 | 9 | SocketError, |
10 | 10 | InformationalError, |
11 | | - InvalidArgumentError |
| 11 | + InvalidArgumentError, |
| 12 | + HeadersTimeoutError, |
| 13 | + BodyTimeoutError |
12 | 14 | } = require('../core/errors.js') |
13 | 15 | const { |
14 | 16 | kUrl, |
@@ -33,6 +35,7 @@ const { |
33 | 35 | kHTTPContext, |
34 | 36 | kClosed, |
35 | 37 | kBodyTimeout, |
| 38 | + kHeadersTimeout, |
36 | 39 | kEnableConnectProtocol, |
37 | 40 | kRemoteSettings, |
38 | 41 | kHTTP2Stream, |
@@ -219,7 +222,11 @@ function resumeH2 (client) { |
219 | 222 | const socket = client[kSocket] |
220 | 223 |
|
221 | 224 | 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) { |
223 | 230 | socket.unref() |
224 | 231 | client[kHTTP2Session].unref() |
225 | 232 | } else { |
@@ -314,6 +321,36 @@ function onHttp2SessionEnd () { |
314 | 321 | * @this {import('http2').ClientHttp2Session} |
315 | 322 | * @param {number} errorCode |
316 | 323 | */ |
| 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 | + |
317 | 354 | function onHttp2SessionGoAway (errorCode) { |
318 | 355 | // TODO(mcollina): Verify if GOAWAY implements the spec correctly: |
319 | 356 | // https://datatracker.ietf.org/doc/html/rfc7540#section-6.8 |
@@ -335,7 +372,9 @@ function onHttp2SessionGoAway (errorCode) { |
335 | 372 | if (client[kRunningIdx] < client[kQueue].length) { |
336 | 373 | const request = client[kQueue][client[kRunningIdx]] |
337 | 374 | client[kQueue][client[kRunningIdx]++] = null |
338 | | - util.errorRequest(client, request, err) |
| 375 | + if (request != null) { |
| 376 | + util.errorRequest(client, request, err) |
| 377 | + } |
339 | 378 | client[kPendingIdx] = client[kRunningIdx] |
340 | 379 | } |
341 | 380 |
|
@@ -368,7 +407,9 @@ function onHttp2SessionClose () { |
368 | 407 | const requests = client[kQueue].splice(client[kRunningIdx]) |
369 | 408 | for (let i = 0; i < requests.length; i++) { |
370 | 409 | const request = requests[i] |
371 | | - util.errorRequest(client, request, err) |
| 410 | + if (request != null) { |
| 411 | + util.errorRequest(client, request, err) |
| 412 | + } |
372 | 413 | } |
373 | 414 | } |
374 | 415 | } |
@@ -416,7 +457,10 @@ function shouldSendContentLength (method) { |
416 | 457 | } |
417 | 458 |
|
418 | 459 | function 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] |
420 | 464 | const session = client[kHTTP2Session] |
421 | 465 | const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request |
422 | 466 | let { body } = request |
@@ -483,6 +527,7 @@ function writeH2 (client, request) { |
483 | 527 |
|
484 | 528 | // We move the running index to the next request |
485 | 529 | client[kOnError](err) |
| 530 | + completeRequest(client, request) |
486 | 531 | client[kResume]() |
487 | 532 | } |
488 | 533 |
|
@@ -537,7 +582,7 @@ function writeH2 (client, request) { |
537 | 582 | request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream) |
538 | 583 |
|
539 | 584 | ++session[kOpenStreams] |
540 | | - client[kQueue][client[kRunningIdx]++] = null |
| 585 | + completeRequest(client, request) |
541 | 586 | }) |
542 | 587 |
|
543 | 588 | stream.on('error', () => { |
@@ -554,7 +599,7 @@ function writeH2 (client, request) { |
554 | 599 | if (session[kOpenStreams] === 0) session.unref() |
555 | 600 | }) |
556 | 601 |
|
557 | | - stream.setTimeout(requestTimeout) |
| 602 | + stream.setTimeout(headersTimeout) |
558 | 603 | return true |
559 | 604 | } |
560 | 605 |
|
@@ -570,13 +615,14 @@ function writeH2 (client, request) { |
570 | 615 |
|
571 | 616 | request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream) |
572 | 617 | ++session[kOpenStreams] |
573 | | - client[kQueue][client[kRunningIdx]++] = null |
| 618 | + completeRequest(client, request) |
574 | 619 | }) |
| 620 | + stream.on('error', abort) |
575 | 621 | stream.once('close', () => { |
576 | 622 | session[kOpenStreams] -= 1 |
577 | 623 | if (session[kOpenStreams] === 0) session.unref() |
578 | 624 | }) |
579 | | - stream.setTimeout(requestTimeout) |
| 625 | + stream.setTimeout(headersTimeout) |
580 | 626 |
|
581 | 627 | return true |
582 | 628 | } |
@@ -677,7 +723,7 @@ function writeH2 (client, request) { |
677 | 723 |
|
678 | 724 | // Increment counter as we have new streams open |
679 | 725 | ++session[kOpenStreams] |
680 | | - stream.setTimeout(requestTimeout) |
| 726 | + stream.setTimeout(headersTimeout) |
681 | 727 |
|
682 | 728 | // Track whether we received a response (headers) |
683 | 729 | let responseReceived = false |
@@ -686,6 +732,7 @@ function writeH2 (client, request) { |
686 | 732 | const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers |
687 | 733 | request.onResponseStarted() |
688 | 734 | responseReceived = true |
| 735 | + stream.setTimeout(bodyTimeout) |
689 | 736 |
|
690 | 737 | // Due to the stream nature, it is possible we face a race condition |
691 | 738 | // where the stream has been assigned, but the request has been aborted |
@@ -720,14 +767,13 @@ function writeH2 (client, request) { |
720 | 767 | request.onComplete({}) |
721 | 768 | } |
722 | 769 |
|
723 | | - client[kQueue][client[kRunningIdx]++] = null |
| 770 | + completeRequest(client, request) |
724 | 771 | client[kResume]() |
725 | 772 | } else { |
726 | 773 | // Stream ended without receiving a response - this is an error |
727 | 774 | // (e.g., server destroyed the stream before sending headers) |
728 | 775 | 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) |
731 | 777 | client[kResume]() |
732 | 778 | } |
733 | 779 | }) |
@@ -738,6 +784,14 @@ function writeH2 (client, request) { |
738 | 784 | if (session[kOpenStreams] === 0) { |
739 | 785 | session.unref() |
740 | 786 | } |
| 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 | + } |
741 | 795 | }) |
742 | 796 |
|
743 | 797 | stream.once('error', function (err) { |
@@ -755,7 +809,9 @@ function writeH2 (client, request) { |
755 | 809 | }) |
756 | 810 |
|
757 | 811 | 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}"`) |
759 | 815 | stream.removeAllListeners('data') |
760 | 816 | session[kOpenStreams] -= 1 |
761 | 817 |
|
|