undici 8.8.0 → 8.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -111,22 +111,35 @@ added: v1.0.0
111
111
  `autoSelectFamily` is enabled. **Default:** `250`.
112
112
  * `allowH2` {boolean} Enables HTTP/2 support when the server assigns it a
113
113
  higher priority through ALPN negotiation. **Default:** `true`.
114
- * `useH2c` {boolean} Enforces h2c (HTTP/2 cleartext) for non-HTTPS
115
- connections. **Default:** `false`.
116
- * `maxConcurrentStreams` {number} The maximum number of concurrent HTTP/2
114
+ * `useH2c` {boolean} _Deprecated: use h2Options.useH2c instead_ Enforces h2c (HTTP/2 cleartext) for non-HTTPS
115
+ connections. **Default:** `false`.
116
+ * `maxConcurrentStreams` {number} _Deprecated: use h2Options.useH2c instead_ The maximum number of concurrent HTTP/2
117
117
  streams for a single session. Once h2 is negotiated this — not `pipelining`,
118
118
  which is HTTP/1.1 only — is the ceiling used to dispatch in-flight requests.
119
119
  It may be overridden by the server's `SETTINGS_MAX_CONCURRENT_STREAMS`
120
120
  frame. **Default:** `100`.
121
- * `initialWindowSize` {number} The HTTP/2 stream-level flow-control window
122
- size (`SETTINGS_INITIAL_WINDOW_SIZE`). Must be a positive integer.
123
- **Default:** `262144`.
124
- * `connectionWindowSize` {number} The HTTP/2 connection-level flow-control
121
+ * `connectionWindowSize` {number} _Deprecated: use h2Options.connectionWindowSize instead_ The HTTP/2 connection-level flow-control
125
122
  window size set via `ClientHttp2Session.setLocalWindowSize()`. Must be a
126
123
  positive integer. **Default:** `524288`.
127
- * `pingInterval` {number} The time interval, in milliseconds, between HTTP/2
124
+ * `pingInterval` {number} _Deprecated: use h2Options.pingInterval instead_ The time interval, in milliseconds, between HTTP/2
128
125
  PING frames. Set to `0` to disable PING frames. Applies only to HTTP/2
129
126
  connections and emits a `ping` event on the client. **Default:** `60e3`.
127
+ * `h2Options` {object} Set of options for HTTP/2 sessions
128
+ * `useH2c` {boolean} Enforces h2c (HTTP/2 cleartext) for non-HTTPS
129
+ connections. **Default:** `false`.
130
+ * `maxConcurrentStreams` {number} The maximum number of concurrent HTTP/2
131
+ streams for a single session. Once h2 is negotiated this — not `pipelining`,
132
+ which is HTTP/1.1 only — is the ceiling used to dispatch in-flight requests.
133
+ It may be overridden by the server's `SETTINGS_MAX_CONCURRENT_STREAMS`
134
+ frame. **Default:** `100`.
135
+ * `connectionWindowSize` {number} The HTTP/2 connection-level flow-control
136
+ window size set via `ClientHttp2Session.setLocalWindowSize()`. Must be a
137
+ positive integer. **Default:** `524288`.
138
+ * `pingInterval` {number} The time interval, in milliseconds, between HTTP/2
139
+ PING frames. Set to `0` to disable PING frames. Applies only to HTTP/2
140
+ connections and emits a `ping` event on the client. **Default:** `60e3`.
141
+ * `settings` {object} `SETTINGS` frame options. For full reference, take a
142
+ look to [HTTP/2#Settings Object](https://nodejs.org/api/http2.html#settings-object)
130
143
  * `webSocket` {Object} (optional) WebSocket-specific configuration.
131
144
  * `maxFragments` {number} The maximum number of fragments in a message. Set
132
145
  to `0` to disable the limit. **Default:** `131072`.
@@ -15,7 +15,6 @@ const kContentType = Symbol('kContentType')
15
15
  const kContentLength = Symbol('kContentLength')
16
16
  const kUsed = Symbol('kUsed')
17
17
  const kBytesRead = Symbol('kBytesRead')
18
- const kPreservedBuffer = Symbol('kPreservedBuffer')
19
18
 
20
19
  const noop = () => {}
21
20
 
@@ -326,36 +325,14 @@ class BodyReadable extends Readable {
326
325
  */
327
326
  setEncoding (encoding) {
328
327
  if (Buffer.isEncoding(encoding)) {
329
- // Preserve raw Buffer chunks for the consume path (body.text(),
330
- // body.json(), etc.) before super.setEncoding() replaces them
331
- // with decoded strings. Without this, the consume path would
332
- // lose access to the original bytes — some of which may be held
333
- // by the decoder for incomplete multi-byte sequences, and the
334
- // rest converted to strings that can't be safely concatenated
335
- // byte-wise.
336
- const state = this._readableState
337
- const buffer = state.buffer
338
- if (buffer && state.length > 0) {
339
- const bufferIndex = state.bufferIndex ?? 0
340
- const preserved = []
341
- const source = typeof buffer.slice === 'function'
342
- ? buffer.slice(bufferIndex)
343
- : buffer
344
- for (const data of source) {
345
- if (Buffer.isBuffer(data)) {
346
- preserved.push(data)
347
- }
348
- }
349
- if (preserved.length > 0) {
350
- this[kPreservedBuffer] = (this[kPreservedBuffer] || []).concat(preserved)
351
- }
352
- }
353
-
354
328
  // Delegate to Node.js Readable.setEncoding() which initializes a
355
329
  // StringDecoder and re-encodes already-buffered chunks. This properly
356
330
  // handles multi-byte sequences split at chunk boundaries for the
357
331
  // for-await / on('data') paths. Without this, Node.js uses
358
332
  // buf.toString(encoding) on each chunk, producing U+FFFD for split chars.
333
+ //
334
+ // The consume path (body.text(), body.json(), ...) copes with the
335
+ // decoded strings this leaves in state.buffer, see consumeStart().
359
336
  super.setEncoding(encoding)
360
337
  }
361
338
  return this
@@ -464,17 +441,7 @@ function consumeStart (consume) {
464
441
 
465
442
  const { _readableState: state } = consume.stream
466
443
 
467
- // If setEncoding() was called, state.buffer may contain decoded strings
468
- // (which would break Buffer.concat in chunksDecode). Use the preserved
469
- // raw Buffers (saved before super.setEncoding() in setEncoding()) for
470
- // byte-level accurate consumption. Otherwise read from state.buffer.
471
- const preserved = consume.stream[kPreservedBuffer]
472
- if (preserved && preserved.length > 0) {
473
- for (const chunk of preserved) {
474
- consumePush(consume, chunk)
475
- }
476
- consume.stream[kPreservedBuffer] = null
477
- } else if (state.bufferIndex) {
444
+ if (state.bufferIndex) {
478
445
  const start = state.bufferIndex
479
446
  const end = state.buffer.length
480
447
  for (let n = start; n < end; n++) {
@@ -486,14 +453,29 @@ function consumeStart (consume) {
486
453
  }
487
454
  }
488
455
 
456
+ // If setEncoding() was called, state.buffer holds decoded strings, which
457
+ // consumePush() turns back into bytes. The trailing bytes of a multi-byte
458
+ // sequence split across a chunk boundary are not part of any of those
459
+ // strings, they are held inside the decoder until the rest arrives, so
460
+ // take them from there.
461
+ const decoder = state.decoder
462
+ if (decoder != null && decoder.lastNeed > 0) {
463
+ consumePush(consume, Buffer.from(decoder.lastChar.subarray(0, decoder.lastTotal - decoder.lastNeed)))
464
+ }
465
+
489
466
  if (state.endEmitted) {
490
- consumeEnd(this[kConsume], this._readableState.encoding)
491
- } else {
492
- consume.stream.on('end', function () {
493
- consumeEnd(this[kConsume], this._readableState.encoding)
494
- })
467
+ // No `this` to read the consume off here: consumeStart is a free function, called from
468
+ // the queueMicrotask above. The callback below does have one, because the emitter passes
469
+ // the stream as its receiver. Returning matters too - consumeEnd() clears consume.stream,
470
+ // which the resume() below would then dereference.
471
+ consumeEnd(consume, state.encoding)
472
+ return
495
473
  }
496
474
 
475
+ consume.stream.on('end', function () {
476
+ consumeEnd(this[kConsume], this._readableState.encoding)
477
+ })
478
+
497
479
  consume.stream.resume()
498
480
 
499
481
  while (consume.stream.read() != null) {
@@ -583,7 +565,7 @@ function consumeEnd (consume, encoding) {
583
565
 
584
566
  /**
585
567
  * @param {Consume} consume
586
- * @param {Buffer} chunk
568
+ * @param {Buffer|string} chunk
587
569
  * @returns {void}
588
570
  */
589
571
  function consumePush (consume, chunk) {
@@ -591,6 +573,14 @@ function consumePush (consume, chunk) {
591
573
  return
592
574
  }
593
575
 
576
+ if (typeof chunk === 'string') {
577
+ // Buffered before the consume started, while an encoding was set.
578
+ // consume.length has to stay a byte count and chunksDecode()/chunksConcat()
579
+ // only work on bytes, so re-encode. A string's own length is in UTF-16 code
580
+ // units and Uint8Array.prototype.set() ignores a string argument entirely.
581
+ chunk = Buffer.from(chunk, consume.stream._readableState.encoding)
582
+ }
583
+
594
584
  consume.length += chunk.length
595
585
  consume.body.push(chunk)
596
586
  }
@@ -218,17 +218,62 @@ class MemoryCacheStore extends EventEmitter {
218
218
  }
219
219
 
220
220
  function findEntry (key, entries, now) {
221
- return entries.find((entry) => (
222
- entry.deleteAt > now &&
223
- entry.method === key.method &&
224
- (entry.vary == null || Object.keys(entry.vary).every(headerName => {
225
- if (entry.vary[headerName] === null) {
226
- return key.headers[headerName] === undefined
221
+ for (let i = 0; i < entries.length; i++) {
222
+ const entry = entries[i]
223
+ if (
224
+ entry.deleteAt > now &&
225
+ entry.method === key.method &&
226
+ varyMatches(key, entry)
227
+ ) {
228
+ return entry
229
+ }
230
+ }
231
+ }
232
+
233
+ function varyMatches (key, entry) {
234
+ if (entry.vary == null) {
235
+ return true
236
+ }
237
+
238
+ for (const headerName in entry.vary) {
239
+ if (Object.hasOwn(entry.vary, headerName) && !headerValueEquals(key.headers?.[headerName], entry.vary[headerName])) {
240
+ return false
241
+ }
242
+ }
243
+
244
+ return true
245
+ }
246
+
247
+ /**
248
+ * @param {string|string[]|null|undefined} lhs
249
+ * @param {string|string[]|null|undefined} rhs
250
+ * @returns {boolean}
251
+ */
252
+ function headerValueEquals (lhs, rhs) {
253
+ if (lhs == null && rhs == null) {
254
+ return true
255
+ }
256
+
257
+ if ((lhs == null && rhs != null) ||
258
+ (lhs != null && rhs == null)) {
259
+ return false
260
+ }
261
+
262
+ if (Array.isArray(lhs) && Array.isArray(rhs)) {
263
+ if (lhs.length !== rhs.length) {
264
+ return false
265
+ }
266
+
267
+ for (let i = 0; i < lhs.length; i++) {
268
+ if (lhs[i] !== rhs[i]) {
269
+ return false
227
270
  }
271
+ }
272
+
273
+ return true
274
+ }
228
275
 
229
- return entry.vary[headerName] === key.headers[headerName]
230
- }))
231
- ))
276
+ return lhs === rhs
232
277
  }
233
278
 
234
279
  module.exports = MemoryCacheStore
@@ -456,7 +456,13 @@ function headerValueEquals (lhs, rhs) {
456
456
  return false
457
457
  }
458
458
 
459
- return lhs.every((x, i) => x === rhs[i])
459
+ for (let i = 0; i < lhs.length; i++) {
460
+ if (lhs[i] !== rhs[i]) {
461
+ return false
462
+ }
463
+ }
464
+
465
+ return true
460
466
  }
461
467
 
462
468
  return lhs === rhs
@@ -105,13 +105,27 @@ function buildConnector ({ allowH2, preferH2, useH2c, maxCachedSessions, socketP
105
105
 
106
106
  port = port || 80
107
107
 
108
- socket = net.connect({
108
+ const connectOptions = {
109
109
  highWaterMark: 64 * 1024, // Same as nodejs fs streams.
110
110
  ...options,
111
111
  localAddress,
112
112
  port,
113
113
  host: hostname
114
- })
114
+ }
115
+
116
+ const family = net.isIP(hostname)
117
+ if (family !== 0 && servername && servername !== hostname) {
118
+ connectOptions.host = servername
119
+ connectOptions.lookup = (_hostname, lookupOptions, cb) => {
120
+ if (lookupOptions.all) {
121
+ cb(null, [{ address: hostname, family }])
122
+ } else {
123
+ cb(null, hostname, family)
124
+ }
125
+ }
126
+ }
127
+
128
+ socket = net.connect(connectOptions)
115
129
  if (useH2c === true) {
116
130
  socket.alpnProtocol = 'h2'
117
131
  }
@@ -472,7 +472,13 @@ function processHeader (request, key, val) {
472
472
  } else if (typeof val[i] === 'object') {
473
473
  throw new InvalidArgumentError(`invalid ${key} header`)
474
474
  } else {
475
- arr.push(`${val[i]}`)
475
+ // Coerce primitives (and reject unsafe coercions such as functions
476
+ // with a crafted toString/Symbol.toPrimitive).
477
+ const str = `${val[i]}`
478
+ if (!isValidHeaderValue(str)) {
479
+ throw new InvalidArgumentError(`invalid ${key} header`)
480
+ }
481
+ arr.push(str)
476
482
  }
477
483
  }
478
484
  val = arr
@@ -483,7 +489,12 @@ function processHeader (request, key, val) {
483
489
  } else if (val === null) {
484
490
  val = ''
485
491
  } else {
492
+ // Coerce primitives (and reject unsafe coercions such as functions
493
+ // with a crafted toString/Symbol.toPrimitive).
486
494
  val = `${val}`
495
+ if (!isValidHeaderValue(val)) {
496
+ throw new InvalidArgumentError(`invalid ${key} header`)
497
+ }
487
498
  }
488
499
 
489
500
  if (headerName === 'host') {
@@ -56,6 +56,7 @@ module.exports = {
56
56
  kCounter: Symbol('socket request counter'),
57
57
  kMaxResponseSize: Symbol('max response size'),
58
58
  kHTTP2Session: Symbol('http2Session'),
59
+ kHTTP2Options: Symbol('http2 options'),
59
60
  kHTTP2SessionState: Symbol('http2Session state'),
60
61
  kRetryHandlerDefaultRetry: Symbol('retry agent default retry'),
61
62
  kConstruct: Symbol('constructable'),
@@ -10,6 +10,7 @@ const {
10
10
  RequestContentLengthMismatchError,
11
11
  ResponseContentLengthMismatchError,
12
12
  RequestAbortedError,
13
+ InvalidArgumentError,
13
14
  HeadersTimeoutError,
14
15
  HeadersOverflowError,
15
16
  SocketError,
@@ -1051,7 +1052,7 @@ function onSocketClose () {
1051
1052
 
1052
1053
  function clearIdleSocketValidation (socket) {
1053
1054
  if (socket[kIdleSocketValidationTimeout]) {
1054
- clearImmediate(socket[kIdleSocketValidationTimeout])
1055
+ clearTimeout(socket[kIdleSocketValidationTimeout])
1055
1056
  socket[kIdleSocketValidationTimeout] = null
1056
1057
  }
1057
1058
 
@@ -1060,14 +1061,14 @@ function clearIdleSocketValidation (socket) {
1060
1061
 
1061
1062
  function scheduleIdleSocketValidation (client, socket) {
1062
1063
  socket[kIdleSocketValidation] = 1
1063
- socket[kIdleSocketValidationTimeout] = setImmediate(() => {
1064
+ socket[kIdleSocketValidationTimeout] = setTimeout(() => {
1064
1065
  socket[kIdleSocketValidationTimeout] = null
1065
1066
  socket[kIdleSocketValidation] = 2
1066
1067
 
1067
1068
  if (client[kSocket] === socket && !socket.destroyed) {
1068
1069
  client[kResume]()
1069
1070
  }
1070
- })
1071
+ }, 0)
1071
1072
  socket[kIdleSocketValidationTimeout].unref?.()
1072
1073
  }
1073
1074
 
@@ -1200,8 +1201,16 @@ function writeH1 (client, request) {
1200
1201
  }
1201
1202
  body = bodyStream.stream
1202
1203
  contentLength = bodyStream.length
1203
- } else if (util.isBlobLike(body) && request.contentType == null && body.type) {
1204
- headers.push('content-type', body.type)
1204
+ } else if (util.isBlobLike(body) && request.contentType == null) {
1205
+ const contentType = body.type
1206
+ if (contentType) {
1207
+ const contentTypeValue = `${contentType}`
1208
+ if (!util.isValidHeaderValue(contentTypeValue)) {
1209
+ util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header'))
1210
+ return false
1211
+ }
1212
+ headers.push('content-type', contentTypeValue)
1213
+ }
1205
1214
  }
1206
1215
 
1207
1216
  if (body && typeof body.read === 'function') {
@@ -26,10 +26,7 @@ const {
26
26
  kStrictContentLength,
27
27
  kOnError,
28
28
  kMaxConcurrentStreams,
29
- kPingInterval,
30
29
  kHTTP2Session,
31
- kHTTP2InitialWindowSize,
32
- kHTTP2ConnectionWindowSize,
33
30
  kHostAuthority,
34
31
  kResume,
35
32
  kSize,
@@ -41,7 +38,8 @@ const {
41
38
  kEnableConnectProtocol,
42
39
  kRemoteSettings,
43
40
  kHTTP2Stream,
44
- kHTTP2SessionState
41
+ kHTTP2SessionState,
42
+ kHTTP2Options
45
43
  } = require('../core/symbols.js')
46
44
  const { channels } = require('../core/diagnostics.js')
47
45
 
@@ -51,6 +49,14 @@ const kRequestStream = Symbol('request stream')
51
49
  const kRequestStreamCleanup = Symbol('request stream cleanup')
52
50
  const kRequestStreamState = Symbol('request stream state')
53
51
  const kReceivedGoAway = Symbol('received goaway')
52
+ const kGoAwayReplayAttempts = Symbol('goaway replay attempts')
53
+ const kRefusedStreamRetry = Symbol('refused stream retry')
54
+
55
+ // RFC 9113 section 8.7: a client SHOULD NOT automatically retry a request more
56
+ // than once. Without a budget a peer that keeps refusing turns one request into
57
+ // an unbounded connect/refuse/reconnect loop that never settles and starves the
58
+ // event loop.
59
+ const MAX_GOAWAY_REPLAY_ATTEMPTS = 1
54
60
 
55
61
  let extractBody
56
62
 
@@ -179,12 +185,24 @@ function completeRequest (client, request, resetPendingIdx = false) {
179
185
  }
180
186
  }
181
187
 
182
- function canRetryRequestAfterGoAway (request) {
188
+ function canReplayRequest (request) {
183
189
  const { body } = request
184
190
 
185
191
  return body == null || util.isBuffer(body) || util.isBlobLike(body)
186
192
  }
187
193
 
194
+ // Count a GOAWAY refusal against the request's replay budget. A peer that
195
+ // refuses every connection must eventually surface an error to the caller
196
+ // rather than being retried forever. Kept separate from canReplayRequest so
197
+ // that the REFUSED_STREAM retry, which has its own single-attempt limit, does
198
+ // not consume this budget just by asking whether the body can be replayed.
199
+ function registerGoAwayRefusal (request) {
200
+ const attempts = (request[kGoAwayReplayAttempts] ?? 0) + 1
201
+ request[kGoAwayReplayAttempts] = attempts
202
+
203
+ return attempts <= MAX_GOAWAY_REPLAY_ATTEMPTS
204
+ }
205
+
188
206
  function closeStream (stream, code = NGHTTP2_REFUSED_STREAM) {
189
207
  if (stream != null && !stream.destroyed && !stream.closed) {
190
208
  try {
@@ -197,19 +215,44 @@ function detachRequestStreamForClose (request) {
197
215
  const stream = request[kRequestStream]
198
216
 
199
217
  clearRequestStream(request)
218
+ severRequestStream(stream)
200
219
 
201
220
  return stream
202
221
  }
203
222
 
223
+ // Unbind a stream from its request for good. releaseRequestStream() alone
224
+ // leaves the 'close' listener attached and kRequestStreamState populated, so a
225
+ // stream abandoned here would still run completeRequestStream() later — and
226
+ // splice out the request that has since been requeued onto another session.
227
+ function severRequestStream (stream) {
228
+ if (stream == null || stream[kRequestStreamState] == null) {
229
+ return
230
+ }
231
+
232
+ stream[kRequestStreamState] = null
233
+ stream.off('close', completeRequestStream)
234
+ // Upgrade streams use their own close cleanup, which would otherwise release
235
+ // the session a second time after the stream has been severed for GOAWAY.
236
+ stream.off('close', onUpgradeStreamClose)
237
+
238
+ if (stream[kHTTP2Session] != null) {
239
+ closeStreamSession(stream)
240
+ }
241
+
242
+ if (!stream.destroyed && !stream.closed) {
243
+ stream.once('error', noop)
244
+ }
245
+ }
246
+
204
247
  function connectH2 (client, socket) {
205
248
  client[kSocket] = socket
206
249
 
207
- const http2InitialWindowSize = client[kHTTP2InitialWindowSize]
208
- const http2ConnectionWindowSize = client[kHTTP2ConnectionWindowSize]
250
+ const http2InitialWindowSize = client[kHTTP2Options].sessionOptions?.initialWindowSize
251
+ const http2ConnectionWindowSize = client[kHTTP2Options].connectionWindowSize
209
252
 
210
253
  const session = http2.connect(client[kUrl], {
211
254
  createConnection: () => socket,
212
- peerMaxConcurrentStreams: client[kMaxConcurrentStreams],
255
+ peerMaxConcurrentStreams: client[kHTTP2Options].maxConcurrentStreams,
213
256
  settings: {
214
257
  // TODO(metcoder95): add support for PUSH
215
258
  enablePush: false,
@@ -223,13 +266,16 @@ function connectH2 (client, socket) {
223
266
  session[kSocket] = socket
224
267
  session[kHTTP2SessionState] = {
225
268
  idleTimeout: null,
269
+ // Armed while the peer advertises MAX_CONCURRENT_STREAMS = 0 and we have
270
+ // work that cannot start. See setNoStreamsTimeout.
271
+ noStreamsTimeout: null,
226
272
  // Sockets start out ref'd. Session ref/unref proxies to the socket, so a
227
273
  // single cached flag lets us skip redundant uv ref/unref calls, provided
228
274
  // every ref/unref of the session or its socket goes through
229
275
  // refH2Session/unrefH2Session.
230
276
  refed: true,
231
277
  ping: {
232
- interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
278
+ interval: client[kHTTP2Options].pingInterval === 0 ? null : setInterval(onHttp2SendPing, client[kHTTP2Options].pingInterval, session).unref()
233
279
  }
234
280
  }
235
281
  session[kReceivedGoAway] = false
@@ -369,7 +415,74 @@ function resumeH2 (client) {
369
415
  } else {
370
416
  clearHttp2IdleTimeout(session)
371
417
  }
418
+
419
+ if (client[kMaxConcurrentStreams] === 0 && client[kRunning] === 0 && client[kPending] > 0) {
420
+ setNoStreamsTimeout(session)
421
+ } else {
422
+ clearNoStreamsTimeout(session)
423
+ }
424
+ }
425
+ }
426
+
427
+ function clearNoStreamsTimeout (session) {
428
+ const state = session[kHTTP2SessionState]
429
+
430
+ if (state?.noStreamsTimeout != null) {
431
+ clearTimeout(state.noStreamsTimeout)
432
+ state.noStreamsTimeout = null
433
+ }
434
+ }
435
+
436
+ // A peer is allowed to advertise SETTINGS_MAX_CONCURRENT_STREAMS = 0 to refuse
437
+ // new streams (RFC 9113 §6.5.2), and is expected to raise it again later. Until
438
+ // it does, busy() reports the client as permanently busy and queued requests
439
+ // cannot open a stream — which means no per-stream timeout covers them, and no
440
+ // reconnect can happen either, so the SETTINGS frame that would lift the limit
441
+ // can never arrive. Give the peer headersTimeout to start honouring requests
442
+ // before failing them; a request that cannot even be sent has missed the same
443
+ // deadline as one whose headers never arrive.
444
+ function setNoStreamsTimeout (session) {
445
+ const client = session[kClient]
446
+ const state = session[kHTTP2SessionState]
447
+ const timeout = client[kHeadersTimeout]
448
+
449
+ if (!timeout || state.noStreamsTimeout != null) {
450
+ return
451
+ }
452
+
453
+ state.noStreamsTimeout = setTimeout(onNoStreamsTimeout, timeout, session).unref()
454
+ }
455
+
456
+ function onNoStreamsTimeout (session) {
457
+ const client = session[kClient]
458
+ const state = session[kHTTP2SessionState]
459
+
460
+ state.noStreamsTimeout = null
461
+
462
+ if (
463
+ client[kHTTP2Session] !== session ||
464
+ client[kMaxConcurrentStreams] !== 0 ||
465
+ client[kRunning] !== 0 ||
466
+ client[kPending] === 0
467
+ ) {
468
+ return
469
+ }
470
+
471
+ const err = new HeadersTimeoutError(
472
+ `HTTP/2: server did not accept a new stream within ${client[kHeadersTimeout]}`
473
+ )
474
+
475
+ const requests = client[kQueue].splice(client[kPendingIdx])
476
+ for (let i = 0; i < requests.length; i++) {
477
+ if (requests[i] != null) {
478
+ util.errorRequest(client, requests[i], err)
479
+ }
372
480
  }
481
+
482
+ // Drop the unusable session so the next request gets a fresh connection,
483
+ // whose SETTINGS may well allow streams again.
484
+ session[kError] = err
485
+ resetHttp2Session(session, err)
373
486
  }
374
487
 
375
488
  function clearHttp2IdleTimeout (session) {
@@ -527,7 +640,7 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
527
640
  if (request != null) {
528
641
  streamsToClose.push(detachRequestStreamForClose(request))
529
642
 
530
- if (canRetryRequestAfterGoAway(request)) {
643
+ if (canReplayRequest(request) && registerGoAwayRefusal(request)) {
531
644
  retriableRequests.push(request)
532
645
  } else {
533
646
  util.errorRequest(client, request, err)
@@ -552,6 +665,7 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
552
665
  }
553
666
 
554
667
  clearHttp2IdleTimeout(this)
668
+ clearNoStreamsTimeout(this)
555
669
 
556
670
  if (!this.closed && !this.destroyed) {
557
671
  this.close()
@@ -576,6 +690,7 @@ function onHttp2SessionClose () {
576
690
  }
577
691
 
578
692
  clearHttp2IdleTimeout(this)
693
+ clearNoStreamsTimeout(this)
579
694
 
580
695
  if (state.ping.interval != null) {
581
696
  clearInterval(state.ping.interval)
@@ -687,6 +802,16 @@ function completeRequestStream () {
687
802
 
688
803
  if (state.pendingEnd && !state.request.aborted && !state.request.completed) {
689
804
  state.request.onResponseEnd(state.trailers || {})
805
+ } else if (!state.request.aborted && !state.request.completed) {
806
+ // The stream closed without a complete response and without reporting an
807
+ // error. finalizeRequest() below frees the queue slot either way, so
808
+ // without this the request would simply vanish and its caller would never
809
+ // hear back.
810
+ util.errorRequest(
811
+ state.client,
812
+ state.request,
813
+ new InformationalError('HTTP/2: stream closed before the response was complete')
814
+ )
690
815
  }
691
816
 
692
817
  finalizeRequest(state)
@@ -1286,6 +1411,38 @@ function onEnd () {
1286
1411
  }
1287
1412
  }
1288
1413
 
1414
+ function retryRefusedStream (stream, state) {
1415
+ const { client, request } = state
1416
+
1417
+ if (
1418
+ state.responseReceived ||
1419
+ request.aborted ||
1420
+ request.completed ||
1421
+ request[kRefusedStreamRetry] ||
1422
+ !canReplayRequest(request)
1423
+ ) {
1424
+ return false
1425
+ }
1426
+
1427
+ // RFC 9113 section 8.7 permits retrying REFUSED_STREAM, but says clients
1428
+ // SHOULD NOT automatically retry the same request more than once.
1429
+ request[kRefusedStreamRetry] = true
1430
+
1431
+ // Detach the failed attempt before moving the request back to the pending
1432
+ // queue. The peer only reset this stream, so the HTTP/2 session remains
1433
+ // usable for the retry. Severing also drops the 'close' listener, so the
1434
+ // abandoned stream cannot later complete the retried request.
1435
+ detachRequestStreamForClose(request)
1436
+ state.stream = null
1437
+ state.requestFinalized = true
1438
+
1439
+ completeRequest(client, request)
1440
+ client[kQueue].splice(client[kPendingIdx], 0, request)
1441
+ client[kResume]()
1442
+
1443
+ return true
1444
+ }
1445
+
1289
1446
  function onError (err) {
1290
1447
  const stream = this
1291
1448
  const state = stream[kRequestStreamState]
@@ -1295,6 +1452,18 @@ function onError (err) {
1295
1452
  }
1296
1453
 
1297
1454
  stream.off('error', onError)
1455
+
1456
+ if (typeof stream.rstCode === 'number' && stream.rstCode !== NGHTTP2_NO_ERROR) {
1457
+ err.http2ErrorCode = stream.rstCode
1458
+ }
1459
+
1460
+ if (
1461
+ stream.rstCode === NGHTTP2_REFUSED_STREAM &&
1462
+ retryRefusedStream(stream, state)
1463
+ ) {
1464
+ return
1465
+ }
1466
+
1298
1467
  state.abort(err)
1299
1468
  }
1300
1469