undici 7.28.0 → 7.29.1

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.
@@ -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
@@ -454,7 +454,13 @@ function headerValueEquals (lhs, rhs) {
454
454
  return false
455
455
  }
456
456
 
457
- return lhs.every((x, i) => x === rhs[i])
457
+ for (let i = 0; i < lhs.length; i++) {
458
+ if (lhs[i] !== rhs[i]) {
459
+ return false
460
+ }
461
+ }
462
+
463
+ return true
458
464
  }
459
465
 
460
466
  return lhs === rhs
@@ -390,7 +390,13 @@ function processHeader (request, key, val) {
390
390
  } else if (typeof val[i] === 'object') {
391
391
  throw new InvalidArgumentError(`invalid ${key} header`)
392
392
  } else {
393
- arr.push(`${val[i]}`)
393
+ // Coerce primitives (and reject unsafe coercions such as functions
394
+ // with a crafted toString/Symbol.toPrimitive).
395
+ const str = `${val[i]}`
396
+ if (!isValidHeaderValue(str)) {
397
+ throw new InvalidArgumentError(`invalid ${key} header`)
398
+ }
399
+ arr.push(str)
394
400
  }
395
401
  }
396
402
  val = arr
@@ -401,7 +407,12 @@ function processHeader (request, key, val) {
401
407
  } else if (val === null) {
402
408
  val = ''
403
409
  } else {
410
+ // Coerce primitives (and reject unsafe coercions such as functions
411
+ // with a crafted toString/Symbol.toPrimitive).
404
412
  val = `${val}`
413
+ if (!isValidHeaderValue(val)) {
414
+ throw new InvalidArgumentError(`invalid ${key} header`)
415
+ }
405
416
  }
406
417
 
407
418
  if (headerName === 'host') {
@@ -49,14 +49,16 @@ function defaultFactory (origin, opts) {
49
49
  }
50
50
 
51
51
  class BalancedPool extends PoolBase {
52
- constructor (upstreams = [], { factory = defaultFactory, ...opts } = {}) {
52
+ constructor (upstreams = [], { factory = defaultFactory, connect, tls, ...opts } = {}) {
53
53
  if (typeof factory !== 'function') {
54
54
  throw new InvalidArgumentError('factory must be a function.')
55
55
  }
56
56
 
57
57
  super(opts)
58
58
 
59
- this[kOptions] = { ...util.deepClone(opts) }
59
+ if (connect && typeof connect !== 'function') connect = { ...connect }
60
+ if (tls && typeof tls !== 'function') tls = { ...tls }
61
+ this[kOptions] = { ...util.deepClone(opts), connect, tls }
60
62
  this[kOptions].interceptors = opts.interceptors
61
63
  ? { ...opts.interceptors }
62
64
  : undefined
@@ -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,
@@ -1011,7 +1012,7 @@ function onSocketClose () {
1011
1012
 
1012
1013
  function clearIdleSocketValidation (socket) {
1013
1014
  if (socket[kIdleSocketValidationTimeout]) {
1014
- clearTimeout(socket[kIdleSocketValidationTimeout])
1015
+ clearImmediate(socket[kIdleSocketValidationTimeout])
1015
1016
  socket[kIdleSocketValidationTimeout] = null
1016
1017
  }
1017
1018
 
@@ -1020,15 +1021,23 @@ function clearIdleSocketValidation (socket) {
1020
1021
 
1021
1022
  function scheduleIdleSocketValidation (client, socket) {
1022
1023
  socket[kIdleSocketValidation] = 1
1023
- socket[kIdleSocketValidationTimeout] = setTimeout(() => {
1024
+ // Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
1025
+ // already pending on this idle keep-alive socket are processed before the
1026
+ // next request is written (GHSA-35p6-xmwp-9g52).
1027
+ //
1028
+ // setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
1029
+ // (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
1030
+ // block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
1031
+ // A ref'd Immediate both keeps the pending request alive and makes poll
1032
+ // return immediately — the hybrid those issues asked for.
1033
+ socket[kIdleSocketValidationTimeout] = setImmediate(() => {
1024
1034
  socket[kIdleSocketValidationTimeout] = null
1025
1035
  socket[kIdleSocketValidation] = 2
1026
1036
 
1027
1037
  if (client[kSocket] === socket && !socket.destroyed) {
1028
1038
  client[kResume]()
1029
1039
  }
1030
- }, 0)
1031
- socket[kIdleSocketValidationTimeout].unref?.()
1040
+ })
1032
1041
  }
1033
1042
 
1034
1043
  /**
@@ -1134,8 +1143,16 @@ function writeH1 (client, request) {
1134
1143
  }
1135
1144
  body = bodyStream.stream
1136
1145
  contentLength = bodyStream.length
1137
- } else if (util.isBlobLike(body) && request.contentType == null && body.type) {
1138
- headers.push('content-type', body.type)
1146
+ } else if (util.isBlobLike(body) && request.contentType == null) {
1147
+ const contentType = body.type
1148
+ if (contentType) {
1149
+ const contentTypeValue = `${contentType}`
1150
+ if (!util.isValidHeaderValue(contentTypeValue)) {
1151
+ util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header'))
1152
+ return false
1153
+ }
1154
+ headers.push('content-type', contentTypeValue)
1155
+ }
1139
1156
  }
1140
1157
 
1141
1158
  if (body && typeof body.read === 'function') {
@@ -8,7 +8,9 @@ const {
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
 
@@ -374,7 +374,9 @@ class Client extends DispatcherBase {
374
374
  const requests = this[kQueue].splice(this[kPendingIdx])
375
375
  for (let i = 0; i < requests.length; i++) {
376
376
  const request = requests[i]
377
- util.errorRequest(this, request, err)
377
+ if (request != null) {
378
+ util.errorRequest(this, request, err)
379
+ }
378
380
  }
379
381
 
380
382
  const callback = () => {
@@ -413,7 +415,9 @@ function onError (client, err) {
413
415
 
414
416
  for (let i = 0; i < requests.length; i++) {
415
417
  const request = requests[i]
416
- util.errorRequest(client, request, err)
418
+ if (request != null) {
419
+ util.errorRequest(client, request, err)
420
+ }
417
421
  }
418
422
  assert(client[kSize] === 0)
419
423
  }