undici 7.29.0 → 7.30.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.
@@ -53,7 +53,8 @@ diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) =>
53
53
 
54
54
  ## `undici:request:headers`
55
55
 
56
- This message is published after the response headers have been received.
56
+ This message is published after the response headers have been received. This includes a successful CONNECT or
57
+ protocol upgrade response.
57
58
 
58
59
  ```js
59
60
  import diagnosticsChannel from 'diagnostics_channel'
@@ -82,6 +83,7 @@ diagnosticsChannel.channel('undici:request:bodyChunkReceived').subscribe(({ requ
82
83
  ## `undici:request:trailers`
83
84
 
84
85
  This message is published after the response body and trailers have been received, i.e. the response has been completed.
86
+ After an upgraded socket is passed to the request handler, this message is published with an empty `trailers` array.
85
87
 
86
88
  ```js
87
89
  import diagnosticsChannel from 'diagnostics_channel'
@@ -1153,6 +1153,7 @@ The `decompress` interceptor automatically decompresses response bodies that are
1153
1153
 
1154
1154
  - `skipErrorResponses` - Whether to skip decompression for error responses (status codes >= 400). Default: `true`.
1155
1155
  - `skipStatusCodes` - Array of status codes to skip decompression for. Default: `[204, 304]`.
1156
+ - `maxSize` - Maximum decompressed response size in bytes for each decompression stage. Set to `0` to disable the limit. Default: `0`.
1156
1157
 
1157
1158
  **Example - Basic Decompress Interceptor**
1158
1159
 
@@ -302,11 +302,32 @@ class Request {
302
302
  }
303
303
  }
304
304
 
305
- onUpgrade (statusCode, headers, socket) {
305
+ /**
306
+ * @param {number} statusCode
307
+ * @param {Buffer[]|string[]} headers
308
+ * @param {import('node:stream').Duplex} socket
309
+ * @param {string} [statusText]
310
+ */
311
+ onUpgrade (statusCode, headers, socket, statusText = '') {
312
+ this.onFinally()
313
+
306
314
  assert(!this.aborted)
307
315
  assert(!this.completed)
308
316
 
309
- return this[kHandler].onUpgrade(statusCode, headers, socket)
317
+ if (channels.headers.hasSubscribers) {
318
+ channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
319
+ }
320
+
321
+ const result = this[kHandler].onUpgrade(statusCode, headers, socket)
322
+
323
+ if (!this.aborted) {
324
+ this.completed = true
325
+ if (channels.trailers.hasSubscribers) {
326
+ channels.trailers.publish({ request: this, trailers: [] })
327
+ }
328
+ }
329
+
330
+ return result
310
331
  }
311
332
 
312
333
  onComplete (trailers) {
@@ -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
@@ -69,8 +69,14 @@ function lazyllhttp () {
69
69
 
70
70
  let mod
71
71
 
72
- // We disable wasm SIMD on ppc64 as it seems to be broken on Power 9 architectures.
73
- let useWasmSIMD = process.arch !== 'ppc64'
72
+ // We disable wasm SIMD on older versions of Node.js on ppc64 that are broken on Power >=9 architectures.
73
+ let useWasmSIMD = true
74
+ if (process.arch === 'ppc64') {
75
+ const [major, minor] = process.versions.node.split('.').map(n => parseInt(n, 10))
76
+ if (major < 24 || (major === 24 && minor < 12)) {
77
+ useWasmSIMD = false
78
+ }
79
+ }
74
80
  // The Env Variable UNDICI_NO_WASM_SIMD allows explicitly overriding the default behavior
75
81
  if (process.env.UNDICI_NO_WASM_SIMD === '1') {
76
82
  useWasmSIMD = false
@@ -521,7 +527,7 @@ class Parser {
521
527
  * @param {Buffer} head
522
528
  */
523
529
  onUpgrade (head) {
524
- const { upgrade, client, socket, headers, statusCode } = this
530
+ const { upgrade, client, socket, headers, statusCode, statusText } = this
525
531
 
526
532
  assert(upgrade)
527
533
  assert(client[kSocket] === socket)
@@ -556,8 +562,9 @@ class Parser {
556
562
  client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))
557
563
 
558
564
  try {
559
- request.onUpgrade(statusCode, headers, socket)
565
+ request.onUpgrade(statusCode, headers, socket, statusText)
560
566
  } catch (err) {
567
+ util.errorRequest(client, request, err)
561
568
  util.destroy(socket, err)
562
569
  }
563
570
 
@@ -1012,7 +1019,7 @@ function onSocketClose () {
1012
1019
 
1013
1020
  function clearIdleSocketValidation (socket) {
1014
1021
  if (socket[kIdleSocketValidationTimeout]) {
1015
- clearTimeout(socket[kIdleSocketValidationTimeout])
1022
+ clearImmediate(socket[kIdleSocketValidationTimeout])
1016
1023
  socket[kIdleSocketValidationTimeout] = null
1017
1024
  }
1018
1025
 
@@ -1021,15 +1028,23 @@ function clearIdleSocketValidation (socket) {
1021
1028
 
1022
1029
  function scheduleIdleSocketValidation (client, socket) {
1023
1030
  socket[kIdleSocketValidation] = 1
1024
- socket[kIdleSocketValidationTimeout] = setTimeout(() => {
1031
+ // Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
1032
+ // already pending on this idle keep-alive socket are processed before the
1033
+ // next request is written (GHSA-35p6-xmwp-9g52).
1034
+ //
1035
+ // setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
1036
+ // (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
1037
+ // block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
1038
+ // A ref'd Immediate both keeps the pending request alive and makes poll
1039
+ // return immediately — the hybrid those issues asked for.
1040
+ socket[kIdleSocketValidationTimeout] = setImmediate(() => {
1025
1041
  socket[kIdleSocketValidationTimeout] = null
1026
1042
  socket[kIdleSocketValidation] = 2
1027
1043
 
1028
1044
  if (client[kSocket] === socket && !socket.destroyed) {
1029
1045
  client[kResume]()
1030
1046
  }
1031
- }, 0)
1032
- socket[kIdleSocketValidationTimeout].unref?.()
1047
+ })
1033
1048
  }
1034
1049
 
1035
1050
  /**
@@ -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
 
@@ -530,14 +575,18 @@ function writeH2 (client, request) {
530
575
 
531
576
  stream = session.request(headers, { endStream: false, signal })
532
577
  stream[kHTTP2Stream] = true
578
+ ++session[kOpenStreams]
533
579
 
534
580
  stream.once('response', (headers, _flags) => {
535
581
  const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
536
582
 
537
- request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
538
-
539
- ++session[kOpenStreams]
540
- client[kQueue][client[kRunningIdx]++] = null
583
+ try {
584
+ request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
585
+ } catch (err) {
586
+ abort(err)
587
+ return
588
+ }
589
+ completeRequest(client, request)
541
590
  })
542
591
 
543
592
  stream.on('error', () => {
@@ -554,7 +603,7 @@ function writeH2 (client, request) {
554
603
  if (session[kOpenStreams] === 0) session.unref()
555
604
  })
556
605
 
557
- stream.setTimeout(requestTimeout)
606
+ stream.setTimeout(headersTimeout)
558
607
  return true
559
608
  }
560
609
 
@@ -565,18 +614,24 @@ function writeH2 (client, request) {
565
614
  // We disabled endStream to allow the user to write to the stream
566
615
  stream = session.request(headers, { endStream: false, signal })
567
616
  stream[kHTTP2Stream] = true
617
+ ++session[kOpenStreams]
568
618
  stream.on('response', headers => {
569
619
  const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
570
620
 
571
- request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
572
- ++session[kOpenStreams]
573
- client[kQueue][client[kRunningIdx]++] = null
621
+ try {
622
+ request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
623
+ } catch (err) {
624
+ abort(err)
625
+ return
626
+ }
627
+ completeRequest(client, request)
574
628
  })
629
+ stream.on('error', abort)
575
630
  stream.once('close', () => {
576
631
  session[kOpenStreams] -= 1
577
632
  if (session[kOpenStreams] === 0) session.unref()
578
633
  })
579
- stream.setTimeout(requestTimeout)
634
+ stream.setTimeout(headersTimeout)
580
635
 
581
636
  return true
582
637
  }
@@ -677,7 +732,7 @@ function writeH2 (client, request) {
677
732
 
678
733
  // Increment counter as we have new streams open
679
734
  ++session[kOpenStreams]
680
- stream.setTimeout(requestTimeout)
735
+ stream.setTimeout(headersTimeout)
681
736
 
682
737
  // Track whether we received a response (headers)
683
738
  let responseReceived = false
@@ -686,6 +741,7 @@ function writeH2 (client, request) {
686
741
  const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
687
742
  request.onResponseStarted()
688
743
  responseReceived = true
744
+ stream.setTimeout(bodyTimeout)
689
745
 
690
746
  // Due to the stream nature, it is possible we face a race condition
691
747
  // where the stream has been assigned, but the request has been aborted
@@ -720,14 +776,13 @@ function writeH2 (client, request) {
720
776
  request.onComplete({})
721
777
  }
722
778
 
723
- client[kQueue][client[kRunningIdx]++] = null
779
+ completeRequest(client, request)
724
780
  client[kResume]()
725
781
  } else {
726
782
  // Stream ended without receiving a response - this is an error
727
783
  // (e.g., server destroyed the stream before sending headers)
728
784
  abort(new InformationalError('HTTP/2: stream half-closed (remote)'))
729
- client[kQueue][client[kRunningIdx]++] = null
730
- client[kPendingIdx] = client[kRunningIdx]
785
+ completeRequest(client, request, true)
731
786
  client[kResume]()
732
787
  }
733
788
  })
@@ -738,6 +793,14 @@ function writeH2 (client, request) {
738
793
  if (session[kOpenStreams] === 0) {
739
794
  session.unref()
740
795
  }
796
+
797
+ // A stream can close without ever emitting 'end' or 'error': a peer's
798
+ // RST_STREAM(CANCEL) received before the response is reported by Node as a
799
+ // bare 'close', and destroying the stream unenrolls its timeout, so no
800
+ // 'timeout' follows either. Nothing else would ever settle this request.
801
+ if (!request.aborted && !request.completed) {
802
+ abort(new InformationalError('HTTP/2: stream closed before the response was complete'))
803
+ }
741
804
  })
742
805
 
743
806
  stream.once('error', function (err) {
@@ -755,7 +818,9 @@ function writeH2 (client, request) {
755
818
  })
756
819
 
757
820
  stream.on('timeout', () => {
758
- const err = new InformationalError(`HTTP/2: "stream timeout after ${requestTimeout}"`)
821
+ const err = responseReceived
822
+ ? new BodyTimeoutError(`HTTP/2: "body timeout after ${bodyTimeout}"`)
823
+ : new HeadersTimeoutError(`HTTP/2: "headers timeout after ${headersTimeout}"`)
759
824
  stream.removeAllListeners('data')
760
825
  session[kOpenStreams] -= 1
761
826
 
@@ -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
  }
@@ -207,6 +207,13 @@ class CacheHandler {
207
207
  }
208
208
 
209
209
  const cacheControlHeader = resHeaders['cache-control']
210
+ const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
211
+
212
+ if (revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives)) {
213
+ deleteCachedValue(this.#store, this.#cacheKey)
214
+ return downstreamOnHeaders()
215
+ }
216
+
210
217
  const heuristicallyCacheable = resHeaders['last-modified'] && arrayIncludes(HEURISTICALLY_CACHEABLE_STATUS_CODES, statusCode)
211
218
  if (
212
219
  !cacheControlHeader &&
@@ -223,8 +230,7 @@ class CacheHandler {
223
230
  return downstreamOnHeaders()
224
231
  }
225
232
 
226
- const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
227
- if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) {
233
+ if (!canCacheResponse(this.#cacheType, this.#cacheKey.method, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) {
228
234
  if (statusCode === 304 && (cacheControlHeader || revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives))) {
229
235
  deleteCachedValue(this.#store, this.#cacheKey)
230
236
  }
@@ -465,7 +471,10 @@ function deleteCachedValueIfNotModified (statusCode, store, cacheKey) {
465
471
  */
466
472
  function revalidationResponseDisallowsCachedReuse (cacheType, resHeaders, cacheControlDirectives) {
467
473
  return cacheControlDirectives['no-store'] === true ||
468
- (cacheType === 'shared' && cacheControlDirectives.private === true) ||
474
+ (cacheType === 'shared' && (
475
+ cacheControlDirectives.private === true ||
476
+ Object.hasOwn(resHeaders, 'set-cookie')
477
+ )) ||
469
478
  (resHeaders.vary ? isInvalidOrWildcardVaryHeader(resHeaders.vary) : false)
470
479
  }
471
480
 
@@ -473,12 +482,16 @@ function revalidationResponseDisallowsCachedReuse (cacheType, resHeaders, cacheC
473
482
  * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
474
483
  *
475
484
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
485
+ * @param {string} method
476
486
  * @param {number} statusCode
477
487
  * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
478
488
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
479
489
  * @param {import('../../types/header.d.ts').IncomingHttpHeaders} [reqHeaders]
480
490
  */
481
- function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives, reqHeaders) {
491
+ function canCacheResponse (cacheType, method, statusCode, resHeaders, cacheControlDirectives, reqHeaders) {
492
+ if (!arrayIncludes(util.safeHTTPMethods, method)) {
493
+ return false
494
+ }
482
495
  // Status code must be final and understood.
483
496
  if (statusCode < 200 || arrayIncludes(NOT_UNDERSTOOD_STATUS_CODES, statusCode)) {
484
497
  return false
@@ -499,7 +512,10 @@ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirect
499
512
  return false
500
513
  }
501
514
 
502
- if (cacheType === 'shared' && cacheControlDirectives.private === true) {
515
+ if (cacheType === 'shared' && (
516
+ cacheControlDirectives.private === true ||
517
+ Object.hasOwn(resHeaders, 'set-cookie')
518
+ )) {
503
519
  return false
504
520
  }
505
521