Commit d1a16216fda for nodejs

commit d1a16216fda39bf65931e65d0a04cf9f3bb23e7d
Author: Node.js GitHub Bot <github-bot@iojs.org>
Date:   Sat Nov 7 05:42:45 2026 -0500

    deps: update undici to 8.12.4

    PR-URL: https://github.com/nodejs/node/pull/67272
    Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
    Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
    Reviewed-By: Jake Yuesong Li <jake.yuesong@gmail.com>

diff --git a/deps/undici/src/README.md b/deps/undici/src/README.md
index 479059fbc1a..7662b905bdf 100644
--- a/deps/undici/src/README.md
+++ b/deps/undici/src/README.md
@@ -171,7 +171,7 @@ const response = await fetch('https://api.example.com/data');
 - Building isomorphic code that runs in browsers and Node.js
 - Publishing to npm and want to maximize compatibility with JS runtimes
 - Simple HTTP requests without advanced configuration
-- You're publishing to npm and you want to maximize compatiblity
+- You're publishing to npm and you want to maximize compatibility
 - You don't depend on features from a specific version of undici

 #### Use Undici Module When:
diff --git a/deps/undici/src/docs/docs/api/Dispatcher.md b/deps/undici/src/docs/docs/api/Dispatcher.md
index 4e3c1293130..61c0d64f53d 100644
--- a/deps/undici/src/docs/docs/api/Dispatcher.md
+++ b/deps/undici/src/docs/docs/api/Dispatcher.md
@@ -205,7 +205,10 @@ added: v4.0.0
 -->

 * `options` {Object}
-  * `origin` {string|URL} (optional) The origin to dispatch the request to.
+  * `origin` {string|URL} The origin to dispatch the request to. Optional for a
+    dispatcher that is already bound to a single origin, such as `Client` or
+    `Pool`, which then use their own origin. Required on `Agent`, which is not
+    bound to one origin and throws an `InvalidArgumentError` when it is missing.
   * `path` {string} The request path.
   * `method` {string} The HTTP method, e.g. `'GET'` or `'POST'`.
   * `body` {string|Buffer|Uint8Array|Readable|FormData|null} The request body.
diff --git a/deps/undici/src/docs/docs/api/RetryAgent.md b/deps/undici/src/docs/docs/api/RetryAgent.md
index 726620b72f2..bcc8582b908 100644
--- a/deps/undici/src/docs/docs/api/RetryAgent.md
+++ b/deps/undici/src/docs/docs/api/RetryAgent.md
@@ -60,7 +60,7 @@ added: v6.7.0
   * `retryAfter` {boolean} When `true`, the delay between retries is inferred from
     the `Retry-After` response header when present. **Default:** `true`.
   * `methods` {string[]} HTTP methods that are eligible for retry.
-    **Default:** `['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE']`.
+    **Default:** `['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE', 'QUERY']`.
   * `statusCodes` {number[]} HTTP status codes that trigger a retry.
     **Default:** `[500, 502, 503, 504, 429]`.
   * `errorCodes` {string[]} Transport error codes that trigger a retry.
diff --git a/deps/undici/src/docs/docs/api/RetryHandler.md b/deps/undici/src/docs/docs/api/RetryHandler.md
index fa028ef1d59..3557ab73334 100644
--- a/deps/undici/src/docs/docs/api/RetryHandler.md
+++ b/deps/undici/src/docs/docs/api/RetryHandler.md
@@ -85,7 +85,7 @@ changes:
 * `retryAfter` {boolean} When `true`, the delay before the next retry is inferred
   from the `Retry-After` response header when present. **Default:** `true`.
 * `methods` {string[]} HTTP methods that are eligible for retrying. **Default:**
-  `['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE']`.
+  `['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE', 'QUERY']`.
 * `statusCodes` {number[]} HTTP status codes that trigger a retry. **Default:**
   `[500, 502, 503, 504, 429]`.
 * `errorCodes` {string[]} Network error codes that trigger a retry. **Default:**
diff --git a/deps/undici/src/docs/docs/index.md b/deps/undici/src/docs/docs/index.md
index bb402702ee7..dee24547d53 100644
--- a/deps/undici/src/docs/docs/index.md
+++ b/deps/undici/src/docs/docs/index.md
@@ -155,7 +155,7 @@ const response = await fetch('https://api.example.com/data');
 - Building isomorphic code that runs in browsers and Node.js
 - Publishing to npm and want to maximize compatibility with JS runtimes
 - Simple HTTP requests without advanced configuration
-- You're publishing to npm and you want to maximize compatiblity
+- You're publishing to npm and you want to maximize compatibility
 - You don't depend on features from a specific version of undici

 #### Use Undici Module When:
diff --git a/deps/undici/src/lib/cache/sqlite-cache-store.js b/deps/undici/src/lib/cache/sqlite-cache-store.js
index d17b13241b1..1024cb6a791 100644
--- a/deps/undici/src/lib/cache/sqlite-cache-store.js
+++ b/deps/undici/src/lib/cache/sqlite-cache-store.js
@@ -6,7 +6,9 @@ const { zstdCompressSync, zstdDecompressSync } = require('node:zlib')

 let DatabaseSync

-const VERSION = 4
+// Bump when entries written by an older cache policy are unsafe to reuse.
+// Version 5 invalidates entries that may have been poisoned through redirects.
+const VERSION = 5

 // 2gb
 const MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000
@@ -18,6 +20,20 @@ const COMPRESS_THRESHOLD = 1024 * 1024
 // node:sqlite is available.
 const hasZstd = typeof zstdCompressSync === 'function' && typeof zstdDecompressSync === 'function'

+// SQLite reports a database that another connection is writing as SQLITE_BUSY
+// (5) or SQLITE_LOCKED (6).
+// https://www.sqlite.org/rescode.html#busy
+const SQLITE_BUSY = 5
+const SQLITE_LOCKED = 6
+
+/**
+ * @param {Error & { errcode?: number }} err
+ * @returns {boolean}
+ */
+function isLockedError (err) {
+  return err?.errcode === SQLITE_BUSY || err?.errcode === SQLITE_LOCKED
+}
+
 /**
  * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
  * @implements {CacheStore}
@@ -259,7 +275,7 @@ module.exports = class SqliteCacheStore {
   get (key) {
     assertCacheKey(key)

-    const value = this.#findValue(key)
+    const value = this.#lockSafe(() => this.#findValue(key))
     return value
       ? {
           body: value.body
@@ -307,10 +323,10 @@ module.exports = class SqliteCacheStore {
       }
     }

-    const existingValue = this.#findValue(key, true)
+    const existingValue = this.#lockSafe(() => this.#findValue(key, true))
     if (existingValue) {
       // Updating an existing response, let's overwrite it
-      this.#updateValueQuery.run(
+      this.#lockSafe(() => this.#updateValueQuery.run(
         storedBody,
         compressed,
         value.deleteAt,
@@ -323,10 +339,10 @@ module.exports = class SqliteCacheStore {
         value.cachedAt,
         value.staleAt,
         existingValue.id
-      )
+      ))
     } else {
       // New response, let's insert it
-      this.#insertValueQuery.run(
+      this.#lockSafe(() => this.#insertValueQuery.run(
         url,
         key.method,
         storedBody,
@@ -340,7 +356,7 @@ module.exports = class SqliteCacheStore {
         value.vary ? JSON.stringify(value.vary) : null,
         value.cachedAt,
         value.staleAt
-      )
+      ))
       this.#prune()
     }
   }
@@ -389,23 +405,53 @@ module.exports = class SqliteCacheStore {
       throw new TypeError(`expected key to be object, got ${typeof key}`)
     }

-    this.#deleteByUrlQuery.run(this.#makeValueUrl(key))
+    this.#lockSafe(() => this.#deleteByUrlQuery.run(this.#makeValueUrl(key)))
+  }
+
+  /**
+   * Runs a database operation, skipping it when another connection holds the
+   * database lock.
+   *
+   * The cache store is meant to be shared between processes, and SQLite reports
+   * a concurrent writer as SQLITE_BUSY/SQLITE_LOCKED. Those errors are
+   * transient, and caching is best effort: the entry is skipped and the next
+   * request retries, rather than failing the request that triggered the write.
+   *
+   * @template T
+   * @param {() => T} fn
+   * @returns {T | undefined}
+   */
+  #lockSafe (fn) {
+    try {
+      return fn()
+    } catch (err) {
+      if (!isLockedError(err)) {
+        throw err
+      }
+
+      return undefined
+    }
   }

   #prune () {
-    if (Number.isFinite(this.#maxCount) && this.size <= this.#maxCount) {
-      return 0
+    if (Number.isFinite(this.#maxCount)) {
+      // A locked database cannot be measured, so skip pruning instead of
+      // failing the write that triggered it.
+      const size = this.#lockSafe(() => this.size)
+      if (size === undefined || size <= this.#maxCount) {
+        return 0
+      }
     }

     {
-      const removed = this.#deleteExpiredValuesQuery.run(Date.now()).changes
+      const removed = this.#lockSafe(() => this.#deleteExpiredValuesQuery.run(Date.now()).changes)
       if (removed) {
         return removed
       }
     }

     {
-      const removed = this.#deleteOldValuesQuery?.run(Math.max(Math.floor(this.#maxCount * 0.1), 1)).changes
+      const removed = this.#lockSafe(() => this.#deleteOldValuesQuery?.run(Math.max(Math.floor(this.#maxCount * 0.1), 1)).changes)
       if (removed) {
         return removed
       }
diff --git a/deps/undici/src/lib/dispatcher/client-h1.js b/deps/undici/src/lib/dispatcher/client-h1.js
index cbbdaaf3543..a26b9c014e8 100644
--- a/deps/undici/src/lib/dispatcher/client-h1.js
+++ b/deps/undici/src/lib/dispatcher/client-h1.js
@@ -83,7 +83,9 @@ function lazyllhttp () {

   // We disable wasm SIMD on older versions of Node.js on ppc64 that are broken on Power >=9 architectures.
   let useWasmSIMD = true
-  if (process.arch === 'ppc64') {
+  if (process.arch === 'riscv64') {
+    useWasmSIMD = false
+  } else if (process.arch === 'ppc64') {
     const [major, minor] = process.versions.node.split('.').map(n => parseInt(n, 10))
     if (major < 24 || (major === 24 && minor < 12)) {
       useWasmSIMD = false
diff --git a/deps/undici/src/lib/dispatcher/client-h2.js b/deps/undici/src/lib/dispatcher/client-h2.js
index f0d3a9ca38a..9857b80b1e2 100644
--- a/deps/undici/src/lib/dispatcher/client-h2.js
+++ b/deps/undici/src/lib/dispatcher/client-h2.js
@@ -47,7 +47,6 @@ const {
 const { channels } = require('../core/diagnostics.js')

 const kOpenStreams = Symbol('open streams')
-const kRequestStreamId = Symbol('request stream id')
 const kRequestStream = Symbol('request stream')
 const kRequestStreamCleanup = Symbol('request stream cleanup')
 const kRequestStreamState = Symbol('request stream state')
@@ -61,6 +60,9 @@ const kRefusedStreamRetry = Symbol('refused stream retry')
 // event loop.
 const MAX_GOAWAY_REPLAY_ATTEMPTS = 1

+// RFC 9113 section 6.2. node:http2 reports frame types as raw numbers.
+const HTTP2_FRAME_TYPE_HEADERS = 1
+
 // Lower bound on how long a liveness PING may take to be acked. Stream
 // timeouts can be tiny (tens of ms), while an ack on a live connection can take
 // longer than that on a loaded event loop; without a floor a slow but healthy
@@ -133,7 +135,9 @@ function getGoAwayPendingIdx (client, lastStreamID) {
       continue
     }

-    if (typeof request[kRequestStreamId] !== 'number' || request[kRequestStreamId] > maxAcceptedStreamID) {
+    // A pending stream may not have had an ID when it was bound to the request.
+    const streamId = request[kRequestStream]?.id
+    if (typeof streamId !== 'number' || streamId > maxAcceptedStreamID) {
       return i
     }
   }
@@ -142,7 +146,6 @@ function getGoAwayPendingIdx (client, lastStreamID) {
 }

 function detachRequestFromStream (request) {
-  request[kRequestStreamId] = null
   request[kRequestStream] = null
   request[kRequestStreamCleanup] = null
 }
@@ -152,7 +155,6 @@ function bindRequestToStream (request, stream, cleanup) {
   const previousStream = request[kRequestStream]
   detachRequestFromStream(request)
   previousCleanup?.(previousStream)
-  request[kRequestStreamId] = stream.id
   request[kRequestStream] = stream
   request[kRequestStreamCleanup] = cleanup
 }
@@ -253,8 +255,14 @@ function severRequestStream (stream) {
     closeStreamSession(stream)
   }

+  // #5936: Node can still deliver a session-level error to a native stream
+  // after undici has stopped tracking it (e.g. on idle session teardown). Keep
+  // a persistent noop error sink so the 'error' event is never emitted with no
+  // listener, which would crash the process. A single sink is enough even if
+  // one was already attached by the onError path, so clear it first.
   if (!stream.destroyed && !stream.closed) {
-    stream.once('error', noop)
+    stream.off('error', noop)
+    stream.on('error', noop)
   }
 }

@@ -732,16 +740,18 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
     }
   }

-  for (let i = 0; i < streamsToClose.length; i++) {
-    closeStream(streamsToClose[i])
-  }
-
   if (pendingIdx !== previousPendingIdx) {
     const remainingPendingRequests = client[kQueue].slice(previousPendingIdx)
     client[kQueue].length = pendingIdx
     client[kQueue].push(...retriableRequests, ...remainingPendingRequests)
   }

+  // Publish the rebuilt queue and detach the session before closing anything.
+  // A synchronous sibling HEADERS frameError can requeue an accepted-but-unsent
+  // request and resume the client. It must see the final indices and must not
+  // dispatch its retry on the session that is being retired.
+  client[kPendingIdx] = pendingIdx
+
   if (client[kHTTP2Session] === this) {
     client[kSocket] = null
     client[kHTTPContext] = null
@@ -756,12 +766,14 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
   clearNoStreamsTimeout(this)
   clearHttp2ProbeTimeout(this)

+  for (let i = 0; i < streamsToClose.length; i++) {
+    closeStream(streamsToClose[i])
+  }
+
   if (!this.closed && !this.destroyed) {
     this.close()
   }

-  client[kPendingIdx] = pendingIdx
-
   client.emit('disconnect', client[kUrl], [client], err)

   client[kResume]()
@@ -868,8 +880,8 @@ function closeStreamSession (stream) {
 }

 function onUpgradeStreamClose () {
-  this.off('error', noop)
-
+  // #5936: Retain the noop error sink so a session-level error delivered to
+  // this stream after undici detaches it cannot crash via an unhandled 'error'.
   const state = this[kRequestStreamState]
   this[kRequestStreamState] = null

@@ -973,8 +985,11 @@ function releaseUpgradeStream (stream) {

   removeUpgradeStreamListeners(stream)

+  // #5936: removeUpgradeStreamListeners() dropped the noop sink; re-attach a
+  // persistent one so a late session-level error on this (possibly still native
+  // tracked) stream cannot crash via an unhandled 'error'.
   if (!stream.destroyed && !stream.closed) {
-    stream.once('error', noop)
+    stream.on('error', noop)
   }
 }

@@ -1407,12 +1422,14 @@ function releaseRequestStream (stream) {
     detachRequestFromStream(request)
   }

-  // A closed or destroyed stream cannot emit further events; leaving the
-  // listeners in place saves the removal scans (they are collected with
-  // the stream). All handlers bail out when the stream state is gone.
+  // #5936: Node can deliver a session-level error to a released stream before
+  // its native handle is dropped (e.g. on idle session teardown). Attach a
+  // persistent noop error sink so a late 'error' always has a listener and
+  // cannot crash the process. A closed/destroyed stream cannot emit further
+  // events and is no longer tracked natively, so skip the listener teardown.
   if (!stream.destroyed && !stream.closed) {
     removeRequestStreamListeners(stream)
-    stream.once('error', noop)
+    stream.on('error', noop)
   }
 }

@@ -1607,10 +1624,43 @@ function retryRefusedStream (stream, state) {
   // SHOULD NOT automatically retry the same request more than once.
   request[kRefusedStreamRetry] = true

+  // The peer only reset this stream, so the HTTP/2 session remains usable for
+  // the retry.
+  requeueStreamRequest(client, state)
+
+  return true
+}
+
+// nghttp2 refuses to send the HEADERS of a stream opened before a GOAWAY was
+// received but flushed after it. The peer never saw the request, so it can be
+// replayed on a new connection whatever its method. A GOAWAY whose
+// lastStreamID covers the stream (e.g. 2^31-1, a graceful shutdown notice)
+// leaves the stream attached to the session, so this is the only signal.
+function retryUnsentStream (state) {
+  const { client, request, session } = state
+
+  if (
+    !session[kReceivedGoAway] ||
+    state.responseReceived ||
+    request.aborted ||
+    request.completed ||
+    !canReplayRequest(request) ||
+    !registerGoAwayRefusal(request)
+  ) {
+    return false
+  }
+
+  requeueStreamRequest(client, state)
+
+  return true
+}
+
+function requeueStreamRequest (client, state) {
+  const { request } = state
+
   // Detach the failed attempt before moving the request back to the pending
-  // queue. The peer only reset this stream, so the HTTP/2 session remains
-  // usable for the retry. Severing also drops the 'close' listener, so the
-  // abandoned stream cannot later complete the retried request.
+  // queue. Severing also drops the 'close' listener, so the abandoned stream
+  // cannot later complete the retried request.
   detachRequestStreamForClose(request)
   state.stream = null
   state.requestFinalized = true
@@ -1618,8 +1668,6 @@ function retryRefusedStream (stream, state) {
   completeRequest(client, request)
   client[kQueue].splice(client[kPendingIdx], 0, request)
   client[kResume]()
-
-  return true
 }

 function onError (err) {
@@ -1631,6 +1679,10 @@ function onError (err) {
   }

   stream.off('error', onError)
+  // #5936: The stream has already errored, but Node may destroy it again with a
+  // session-level error before the native handle is released. Keep a persistent
+  // noop error sink so that re-delivery cannot go unhandled.
+  stream.on('error', noop)

   // Node's HTTP/2 implementation can turn an incomplete Content-Length body
   // into a protocol stream error instead of emitting 'end'. Prefer the
@@ -1662,6 +1714,11 @@ function onFrameError (type, code) {
   }

   stream.off('frameError', onFrameError)
+
+  if (type === HTTP2_FRAME_TYPE_HEADERS && retryUnsentStream(state)) {
+    return
+  }
+
   state.abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
 }

diff --git a/deps/undici/src/lib/dispatcher/parser-h1.js b/deps/undici/src/lib/dispatcher/parser-h1.js
index 263d67e7763..c1440170fd2 100644
--- a/deps/undici/src/lib/dispatcher/parser-h1.js
+++ b/deps/undici/src/lib/dispatcher/parser-h1.js
@@ -52,7 +52,8 @@ const EVENT_HEADERS_SIZE = 19
 let miloPackage

 // We disable wasm SIMD on ppc64 as it seems to be broken on Power 9 architectures.
-let useWasmSIMD = process.arch !== 'ppc64'
+// We disable wasm SIMD on riscv64 as it seems to be broken on there as well.
+let useWasmSIMD = (process.arch !== 'riscv64' && process.arch !== 'ppc64')
 // The Env Variable UNDICI_NO_WASM_SIMD allows explicitly overriding the default behavior.
 if (process.env.UNDICI_NO_WASM_SIMD === '1') {
   useWasmSIMD = false
diff --git a/deps/undici/src/lib/handler/cache-handler.js b/deps/undici/src/lib/handler/cache-handler.js
index fed01a1aa0c..20c96def267 100644
--- a/deps/undici/src/lib/handler/cache-handler.js
+++ b/deps/undici/src/lib/handler/cache-handler.js
@@ -10,6 +10,17 @@ const {
   isInvalidOrWildcardVaryHeader,
   isEtagUsable
 } = require('../util/cache')
+
+/**
+ * A suppressed redirect hop recorded by the redirect handler. Carried on
+ * {@link kRedirectCacheControl} and inspected by the cache handler before the
+ * final response is stored under the original request key.
+ *
+ * @typedef {object} RedirectHop
+ * @property {number} statusCode The hop's response status code (e.g. 302).
+ * @property {import('../../types/header.d.ts').IncomingHttpHeaders} headers The hop's response headers.
+ * @property {number} receivedAt The time the hop's response headers were received.
+ */
 const { parseHttpDate } = require('../util/date.js')

 function noop () {}
@@ -105,6 +116,16 @@ function arrayIncludes (array, value) {
   return false
 }

+function appendVaryValues (target, varyValue) {
+  if (Array.isArray(varyValue)) {
+    for (let i = 0; i < varyValue.length; i++) {
+      target.push(varyValue[i])
+    }
+  } else if (varyValue != null) {
+    target.push(varyValue)
+  }
+}
+
 function appendConnectionHeaderTokens (headersToRemove, connectionHeader) {
   const values = Array.isArray(connectionHeader) ? connectionHeader : [connectionHeader]

@@ -176,6 +197,26 @@ function invalidateUnsafeRequest (store, cacheKey, resHeaders) {
   deleteLocationTargets(store, cacheKey, resHeaders.location)
   deleteLocationTargets(store, cacheKey, resHeaders['content-location'])
 }
+
+/**
+ * Whether the original request carried a credential that is not reflected in
+ * the cache key. The memory/sqlite stores key an entry on origin, method, path
+ * and `vary` only, so a credentialed request that is redirected to a resource
+ * that varies per principal must not be stored under the original (alias) key:
+ * a later caller would be served the first principal's body. The set mirrors the
+ * request credentials the redirect handler strips across origins (`authorization`,
+ * `cookie`, `proxy-authorization`).
+ *
+ * @param {import('../../types/header.d.ts').IncomingHttpHeaders | undefined} headers
+ * @returns {boolean}
+ */
+function requestCarriesCredentials (headers) {
+  return headers != null && (
+    Object.hasOwn(headers, 'authorization') ||
+    Object.hasOwn(headers, 'cookie') ||
+    Object.hasOwn(headers, 'proxy-authorization')
+  )
+}
 /**
  * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler
  *
@@ -220,7 +261,7 @@ class CacheHandler {
   /**
    * The URLs visited while following redirects, shared by reference from the
    * redirect handler through the request context. Carries the suppressed hops'
-   * cache-control under {@link kRedirectCacheControl}.
+   * status/headers under {@link kRedirectCacheControl}.
    * @type {URL[] | undefined}
    */
   #history
@@ -296,35 +337,62 @@ class CacheHandler {
       return downstreamOnHeaders()
     }

+    // A credential-bearing request that was redirected must not have the final
+    // hop's response stored under the original request's key. The cache key
+    // does not include the credential, and the redirect target may vary per
+    // principal (e.g. `/me` -> `/profile/{user}`), so replaying the body under
+    // the alias key would leak one principal's data to another — or to an
+    // unauthenticated caller — from a shared cache. The final hop's permissive
+    // cache-control (e.g. `public`) does not authorize this: the credential was
+    // presented to the original hop, not the resource that actually produced
+    // the body. Fail closed. A history with more than one entry means at least
+    // one redirect was actually followed (a single non-redirected hop records
+    // exactly one entry), so a plain revalidation is not mistaken for a redirect.
+    if (
+      this.#history && this.#history.length > 1 &&
+      requestCarriesCredentials(this.#cacheKey.headers)
+    ) {
+      return downstreamOnHeaders()
+    }
+
     // A cross-origin redirect can change the origin that actually produced the
     // final response. Keying that response on the original (trusted) origin
     // would let a redirect target control an entry for a URL the application
-    // trusts. The final request is the last URL in the redirect history; only
-    // cache when its origin still matches the cache key origin.
+    // trusts. Every hop in the redirect chain must stay on the cache key's
+    // origin. An intermediate cross-origin hop can bounce back to the trusted
+    // origin (or to any cacheable asset there) even when the final response
+    // comes from the trusted origin, so checking only the final URL is not
+    // enough.
     if (this.#history && this.#history.length > 0) {
-      const finalUrl = this.#history[this.#history.length - 1]
-      if (!sameOrigin(finalUrl.origin, this.#cacheKey.origin)) {
-        return downstreamOnHeaders()
-      }
-    }
-
-    // A redirect hop that is not forwarded can still carry a restrictive
-    // storage policy (for example `no-store`, or `private` for a shared cache).
-    // Honor it, otherwise a redirect that must not be stored could be laundered
-    // into a cacheable final response under the original key.
-    const redirectCacheControl = this.#history?.[kRedirectCacheControl]
-    if (redirectCacheControl) {
-      for (const hopCacheControl of redirectCacheControl) {
-        const hopDirectives = hopCacheControl ? parseCacheControlHeader(hopCacheControl) : {}
-        if (
-          hopDirectives['no-store'] === true ||
-          (this.#cacheType === 'shared' && hopDirectives.private === true)
-        ) {
+      for (const hopUrl of this.#history) {
+        if (!sameOrigin(hopUrl.origin, this.#cacheKey.origin)) {
           return downstreamOnHeaders()
         }
       }
     }

+    // A redirect hop that is not forwarded can still carry a storage policy
+    // (for example `no-store`, `private` for a shared cache, `no-cache`,
+    // `must-revalidate`, zero freshness, or `Vary: *`) that the final response
+    // body does not inherit. Honor it, otherwise a redirect that must not be
+    // stored (or that selects a private/authenticated target) could be
+    // laundered into a cacheable final response under the original key.
+    const now = Date.now()
+    const redirectHopInfo = this.#history?.[kRedirectCacheControl]
+    const redirectPolicy = redirectHopInfo?.length > 0
+      ? getRedirectPolicy(
+        this.#cacheType,
+        this.#cacheKey.method,
+        redirectHopInfo,
+        this.#cacheKey.headers,
+        this.#cacheByDefault,
+        now
+      )
+      : undefined
+    if (redirectHopInfo?.length > 0 && redirectPolicy === undefined) {
+      return downstreamOnHeaders()
+    }
+
     const cacheControlHeader = resHeaders['cache-control']
     const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}

@@ -357,7 +425,6 @@ class CacheHandler {
       return downstreamOnHeaders()
     }

-    const now = Date.now()
     const resAge = Object.hasOwn(resHeaders, 'age') ? getAge(resHeaders.age) : undefined
     if (resAge !== undefined && resAge >= MAX_RESPONSE_AGE) {
       // Response considered stale
@@ -394,24 +461,65 @@ class CacheHandler {
     }

     const baseTime = now - currentAge
-    const absoluteStaleAt = staleAt + baseTime
+    let absoluteStaleAt = staleAt + baseTime
+    if (redirectPolicy !== undefined) {
+      absoluteStaleAt = Math.min(absoluteStaleAt, redirectPolicy.staleAt)
+    }
     if (now >= absoluteStaleAt && !revalidationOnly) {
       // Response is already stale
       deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey)
       return downstreamOnHeaders()
     }

+    // Union the `Vary` of every suppressed redirect hop with the final
+    // response's `Vary`. A 3xx hop that negotiates its target on `Authorization`
+    // / `Cookie` (or any other header) must make the stored entry vary on those
+    // headers too, or an authenticated user's private response would be served
+    // to unrelated callers.
     let varyDirectives
-    if (this.#cacheKey.headers && resHeaders.vary) {
-      varyDirectives = parseVaryHeader(resHeaders.vary, this.#cacheKey.headers)
-      if (!varyDirectives) {
-        // Parse error
-        return downstreamOnHeaders()
+    const combinedVary = []
+    if (redirectHopInfo) {
+      for (const hop of redirectHopInfo) {
+        appendVaryValues(combinedVary, hop?.headers?.vary)
+      }
+    }
+    appendVaryValues(combinedVary, resHeaders.vary)
+    if (combinedVary.length > 0) {
+      for (const varyHeader of combinedVary) {
+        if (hasVaryStar(varyHeader)) {
+          // `Vary: *` means the response can never be reused (RFC 9111 §4.1)
+          return downstreamOnHeaders()
+        }
+      }
+      if (this.#cacheKey.headers) {
+        varyDirectives = parseVaryHeader(combinedVary, this.#cacheKey.headers)
+        if (!varyDirectives) {
+          // Parse error
+          return downstreamOnHeaders()
+        }
+      }
+    }
+
+    // When the entry was folded from a redirect chain, the final response's
+    // stale-serving directives (RFC 5861) must not outlive the hops' freshness.
+    // A hop that grants no `stale-while-revalidate` / `stale-if-error` caps
+    // stale serving at its freshness, so the final response cannot launder a
+    // stale-serving window past the hop-bounded `staleAt`. Drop those directives
+    // (and keep the `proxy-revalidate` the hop policy may require) so the entry
+    // revalidates at `staleAt` instead of being served stale for the final
+    // response's window.
+    let effectiveCacheControlDirectives = cacheControlDirectives
+    if (redirectPolicy !== undefined) {
+      effectiveCacheControlDirectives = { ...cacheControlDirectives }
+      delete effectiveCacheControlDirectives['stale-while-revalidate']
+      delete effectiveCacheControlDirectives['stale-if-error']
+      if (redirectPolicy.requiresRevalidation === true) {
+        effectiveCacheControlDirectives['proxy-revalidate'] = true
       }
     }

     const cachedAt = baseTime
-    const deleteAt = determineDeleteAt(baseTime, now, cacheControlDirectives, absoluteStaleAt)
+    const deleteAt = determineDeleteAt(baseTime, now, effectiveCacheControlDirectives, absoluteStaleAt)
     const strippedHeaders = stripNecessaryHeaders(resHeaders, cacheControlDirectives)

     /**
@@ -422,7 +530,7 @@ class CacheHandler {
       statusMessage,
       headers: strippedHeaders,
       vary: varyDirectives,
-      cacheControlDirectives,
+      cacheControlDirectives: effectiveCacheControlDirectives,
       cachedAt,
       staleAt: absoluteStaleAt,
       deleteAt
@@ -440,6 +548,22 @@ class CacheHandler {
         this.#downstreamController.release()
       }

+      // The cached body below is replayed to the downstream handler from later
+      // stream ticks (or an iterator loop), outside the dispatcher's own error
+      // handling, so a throwing downstream handler must be converged to a
+      // per-request error instead of escaping as an uncaught exception.
+      const failReplay = (err) => {
+        if (done) {
+          return
+        }
+        done = true
+        // Abort the revalidation request: this releases the paused origin
+        // response (freeing the connection) and routes the error through
+        // CacheHandler.onResponseError, which destroys the cache write stream
+        // and forwards the error downstream exactly once.
+        controller.abort(err)
+      }
+
       const handle304 = (cachedValue) => {
         if (!cachedValue) {
           // Do not create a new cache entry, as a 304 won't have a body - so cannot be cached.
@@ -471,15 +595,19 @@ class CacheHandler {
             if (done) {
               return
             }
-            for (const chunk of bodyIterator) {
-              const full = this.#writeStream?.write(chunk) === false
-              this.#handler.onResponseData?.(this.#downstreamController, chunk)
-              // when stream is full stop writing until we get a 'drain' event
-              if (full) {
-                return
+            try {
+              for (const chunk of bodyIterator) {
+                const full = this.#writeStream?.write(chunk) === false
+                this.#handler.onResponseData?.(this.#downstreamController, chunk)
+                // when stream is full stop writing until we get a 'drain' event
+                if (full) {
+                  return
+                }
               }
+              finish304()
+            } catch (err) {
+              failReplay(err)
             }
-            finish304()
           }

           this.#writeStream
@@ -505,8 +633,13 @@ class CacheHandler {
           // Readable stream body (e.g. from async/remote cache stores)
           cachedValue.body
             .on('data', (chunk) => {
-              this.#writeStream?.write(chunk)
-              this.#handler.onResponseData?.(this.#downstreamController, chunk)
+              try {
+                this.#writeStream?.write(chunk)
+                this.#handler.onResponseData?.(this.#downstreamController, chunk)
+              } catch (err) {
+                cachedValue.body.destroy()
+                failReplay(err)
+              }
             })
             .on('end', () => {
               this.#writeStream?.end()
@@ -713,6 +846,139 @@ function canCacheResponse (cacheType, method, statusCode, resHeaders, cacheContr
   return true
 }

+/**
+ * Determine the absolute freshness deadline imposed by all suppressed redirect
+ * hops. Returning `undefined` means at least one hop cannot safely be folded
+ * into the final response under the original request key.
+ *
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
+ * @param {string} method
+ * @param {RedirectHop[]} redirectHops
+ * @param {import('../../types/header.d.ts').IncomingHttpHeaders} reqHeaders
+ * @param {number | undefined} cacheByDefault
+ * @param {number} now
+ * @returns {{ staleAt: number, requiresRevalidation: boolean } | undefined}
+ */
+function getRedirectPolicy (cacheType, method, redirectHops, reqHeaders, cacheByDefault, now) {
+  let staleAt = Infinity
+  let requiresRevalidation = false
+
+  for (const hop of redirectHops) {
+    const hopPolicy = getRedirectHopPolicy(cacheType, method, hop, reqHeaders, cacheByDefault)
+    if (hopPolicy === undefined || hopPolicy.staleAt <= now) {
+      return undefined
+    }
+
+    staleAt = Math.min(staleAt, hopPolicy.staleAt)
+    requiresRevalidation ||= hopPolicy.requiresRevalidation
+  }
+
+  return { staleAt, requiresRevalidation }
+}
+
+/**
+ * Calculate one redirect hop's absolute freshness deadline using the same age,
+ * date, expiration, invalid-directive, and heuristic rules as a final response.
+ *
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
+ * @param {string} method
+ * @param {RedirectHop} hop
+ * @param {import('../../types/header.d.ts').IncomingHttpHeaders} reqHeaders
+ * @param {number | undefined} cacheByDefault
+ * @returns {{ staleAt: number, requiresRevalidation: boolean } | undefined}
+ */
+function getRedirectHopPolicy (cacheType, method, hop, reqHeaders, cacheByDefault) {
+  if (!Number.isInteger(hop?.statusCode) || !Number.isFinite(hop?.receivedAt)) {
+    return undefined
+  }
+
+  const hopHeaders = hop.headers ?? {}
+  const cacheControlHeader = hopHeaders['cache-control']
+  const hopCacheControl = cacheControlHeader
+    ? parseCacheControlHeader(cacheControlHeader)
+    : {}
+
+  // https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5
+  if (hopHeaders.vary && hasVaryStar(hopHeaders.vary)) {
+    return undefined
+  }
+
+  if (hopCacheControl['no-store'] === true) {
+    return undefined
+  }
+
+  // A qualified `private` directive still marks the named fields as private.
+  // Folding a redirect that says `private="location"` would preserve the
+  // private target choice in the final body even though a shared cache is not
+  // allowed to store that Location field.
+  if (cacheType === 'shared' && Object.hasOwn(hopCacheControl, 'private')) {
+    return undefined
+  }
+
+  // These directives require the hop to be revalidated before reuse. Since the
+  // cache stores only the final response, it cannot independently validate the
+  // suppressed redirect response.
+  if (
+    Object.hasOwn(hopCacheControl, 'no-cache') ||
+    hopCacheControl['must-revalidate'] === true ||
+    (cacheType === 'shared' && hopCacheControl['proxy-revalidate'] === true)
+  ) {
+    return undefined
+  }
+
+  const heuristicallyCacheable =
+    hopHeaders['last-modified'] &&
+    arrayIncludes(HEURISTICALLY_CACHEABLE_STATUS_CODES, hop.statusCode)
+  if (
+    !cacheControlHeader &&
+    !hopHeaders.expires &&
+    !heuristicallyCacheable &&
+    !cacheByDefault
+  ) {
+    return undefined
+  }
+
+  if (!canCacheResponse(cacheType, method, hop.statusCode, hopHeaders, hopCacheControl, reqHeaders)) {
+    return undefined
+  }
+
+  const age = Object.hasOwn(hopHeaders, 'age') ? getAge(hopHeaders.age) : undefined
+  if (age !== undefined && age >= MAX_RESPONSE_AGE) {
+    return undefined
+  }
+
+  const responseDate = Object.hasOwn(hopHeaders, 'date') ? getDate(hopHeaders.date) : undefined
+  if (responseDate === null) {
+    return undefined
+  }
+
+  const apparentAge = responseDate ? Math.max(0, hop.receivedAt - responseDate.getTime()) : 0
+  const currentAge = Math.max(apparentAge, age ?? 0)
+  const hasValidator =
+    (typeof hopHeaders.etag === 'string' && isEtagUsable(hopHeaders.etag)) ||
+    typeof hopHeaders['last-modified'] === 'string'
+  const freshnessLifetime =
+    determineStaleAt(
+      cacheType,
+      hop.receivedAt,
+      age,
+      hopHeaders,
+      responseDate,
+      hopCacheControl,
+      hasValidator
+    ) ?? cacheByDefault
+
+  if (freshnessLifetime === undefined) {
+    return undefined
+  }
+
+  return {
+    staleAt: hop.receivedAt - currentAge + freshnessLifetime,
+    // RFC 9111 §5.2.2.10: s-maxage implies proxy-revalidate for shared caches.
+    requiresRevalidation: cacheType === 'shared' && Object.hasOwn(hopCacheControl, 's-maxage')
+  }
+}
+
 /**
  * @param {string | string[]} dateHeader
  * @returns {Date | null | undefined}
diff --git a/deps/undici/src/lib/handler/decorator-handler.js b/deps/undici/src/lib/handler/decorator-handler.js
index bc99893cda0..136685f91e8 100644
--- a/deps/undici/src/lib/handler/decorator-handler.js
+++ b/deps/undici/src/lib/handler/decorator-handler.js
@@ -32,9 +32,13 @@ module.exports = class DecoratorHandler {
   onResponseStart (...args) {
     assert(!this.#onCompleteCalled)
     assert(!this.#onErrorCalled)
-    assert(!this.#onResponseStartCalled)

-    this.#onResponseStartCalled = true
+    // Interim 1xx responses (e.g. 103 Early Hints) come before the final one.
+    const statusCode = args[1]
+    if (statusCode >= 200) {
+      assert(!this.#onResponseStartCalled)
+      this.#onResponseStartCalled = true
+    }

     return this.#handler.onResponseStart?.(...args)
   }
diff --git a/deps/undici/src/lib/handler/redirect-handler.js b/deps/undici/src/lib/handler/redirect-handler.js
index 604be127637..a1459cc32c1 100644
--- a/deps/undici/src/lib/handler/redirect-handler.js
+++ b/deps/undici/src/lib/handler/redirect-handler.js
@@ -38,11 +38,12 @@ class RedirectHandler {
     this.maxRedirections = maxRedirections
     this.handler = handler
     this.history = []
-    // `cache-control` of each intermediate (suppressed) redirect hop. An outer
-    // cache uses it to preserve a hop's storage policy (e.g. `no-store`) that
-    // would otherwise be lost because the hop's response is not forwarded. It
-    // travels as a non-enumerable property of the history array so it reaches
-    // the cache without becoming part of the public request/response context.
+    // Per-hop metadata for each intermediate (suppressed) redirect hop: the
+    // hop's status code and response headers. An outer cache uses it to honor a
+    // hop's storage policy (e.g. `no-store`, `no-cache`, `Vary: *`) that would
+    // otherwise be lost because the hop's response is not forwarded. It travels
+    // as a non-enumerable property of the history array so it reaches the cache
+    // without becoming part of the public request/response context.
     Object.defineProperty(this.history, kRedirectCacheControl, {
       value: [],
       enumerable: false
@@ -124,9 +125,13 @@ class RedirectHandler {
     }

     // This hop's response is suppressed (only its Location is used). Record its
-    // cache-control so an outer cache can honor a restrictive policy that would
-    // otherwise be lost when the final response is stored.
-    this.history[kRedirectCacheControl].push(headers['cache-control'])
+    // status and headers so an outer cache can honor a restrictive policy that
+    // would otherwise be lost when the final response is stored.
+    this.history[kRedirectCacheControl].push({
+      statusCode,
+      headers: { ...headers },
+      receivedAt: Date.now()
+    })

     const baseUrl = requestOrigin
       ? new URL(this.opts.path, requestOrigin)
@@ -241,9 +246,25 @@ function normalizeStripHeaders (headers, optionName) {
 function cleanRequestHeaders (headers, removeContent, unknownOrigin, stripHeaders, stripHeadersOnCrossOrigin) {
   const ret = []
   if (Array.isArray(headers)) {
-    for (let i = 0; i < headers.length; i += 2) {
-      if (!shouldRemoveHeader(headers[i], removeContent, unknownOrigin, stripHeaders, stripHeadersOnCrossOrigin)) {
-        ret.push(headers[i], headers[i + 1])
+    // Headers may be either a flat alternating [name, value, name, value, ...]
+    // list or an iterable of [name, value] pairs. A plain JavaScript array of
+    // tuples (e.g. `Object.entries(headers)`) satisfies the iterable-of-pairs
+    // public type, so we must distinguish the two representations before
+    // iterating. Treating every Array as a flat list misreads each tuple as a
+    // header name/value pair and defeats the cross-origin credential stripping.
+    if (headers.length > 0 && Array.isArray(headers[0])) {
+      // Array of [name, value] pairs.
+      for (const [key, value] of headers) {
+        if (!shouldRemoveHeader(key, removeContent, unknownOrigin, stripHeaders, stripHeadersOnCrossOrigin)) {
+          ret.push(key, value)
+        }
+      }
+    } else {
+      // Flat alternating [name, value, name, value, ...] list.
+      for (let i = 0; i < headers.length; i += 2) {
+        if (!shouldRemoveHeader(headers[i], removeContent, unknownOrigin, stripHeaders, stripHeadersOnCrossOrigin)) {
+          ret.push(headers[i], headers[i + 1])
+        }
       }
     }
   } else if (headers && typeof headers === 'object') {
diff --git a/deps/undici/src/lib/interceptor/cache.js b/deps/undici/src/lib/interceptor/cache.js
index b16c8994338..60d2522d1ff 100644
--- a/deps/undici/src/lib/interceptor/cache.js
+++ b/deps/undici/src/lib/interceptor/cache.js
@@ -14,7 +14,8 @@ const {
   normalizeHeaders,
   parseCacheControlHeader,
   isInvalidOrWildcardVaryHeader,
-  parseVaryHeader
+  parseVaryHeader,
+  isEtagUsable
 } = require('../util/cache.js')
 const { AbortError } = require('../core/errors.js')
 const { parseHttpDate } = require('../util/date.js')
@@ -202,6 +203,47 @@ function makeRevalidationHeaders (opts, result) {
   return headers
 }

+function revalidationTargetMatchesCacheKey (cacheKey, context) {
+  const history = context?.history
+  if (!Array.isArray(history) || history.length === 0) {
+    return true
+  }
+
+  try {
+    const cachedUrl = new URL(cacheKey.path, cacheKey.origin)
+    const responseUrl = history[history.length - 1]
+    return responseUrl.origin === cachedUrl.origin &&
+      responseUrl.pathname === cachedUrl.pathname &&
+      responseUrl.search === cachedUrl.search
+  } catch {
+    return false
+  }
+}
+
+function revalidationEtagMatchesCachedValue (result, headers) {
+  if (!Object.hasOwn(headers, 'etag')) {
+    return true
+  }
+
+  const responseEtag = headers.etag
+  const cachedEtag = result.etag
+  if (
+    typeof responseEtag !== 'string' ||
+    typeof cachedEtag !== 'string' ||
+    !isEtagUsable(responseEtag) ||
+    !isEtagUsable(cachedEtag)
+  ) {
+    return false
+  }
+
+  if (!responseEtag.startsWith('W/')) {
+    return responseEtag === cachedEtag
+  }
+
+  const cachedOpaqueTag = cachedEtag.startsWith('W/') ? cachedEtag.slice(2) : cachedEtag
+  return responseEtag.slice(2) === cachedOpaqueTag
+}
+
 /**
  * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives
@@ -366,21 +408,35 @@ function sendCachedValue (handler, opts, result, age, context, isStale) {

   stream
     .on('error', function (err) {
+      // Never rethrow: this runs from a stream event on a later tick, where a
+      // throw would crash the process. Like core's Request.onResponseError,
+      // the error is dropped if the handler has no onResponseError.
       if (!this.readableEnded) {
-        if (typeof handler.onResponseError === 'function') {
-          handler.onResponseError(controller, err)
-        } else {
-          throw err
-        }
+        handler.onResponseError?.(controller, err)
       }
     })
     .on('close', function () {
       if (!this.errored) {
-        handler.onResponseEnd?.(controller, {})
+        try {
+          handler.onResponseEnd?.(controller, {})
+        } catch (err) {
+          // The stream is already closed, so the error can't be routed
+          // through it; report it to the handler directly instead.
+          handler.onResponseError?.(controller, err)
+        }
       }
     })

-  handler.onRequestStart?.(controller, context)
+  // Downstream handler calls below run outside the dispatcher's own error
+  // handling (on later ticks for the body, or inside a promise callback for
+  // async stores), so a throw must be converged to a per-request error
+  // instead of escaping as an uncaught exception.
+  try {
+    handler.onRequestStart?.(controller, context)
+  } catch (err) {
+    stream.destroy(err)
+    return
+  }

   if (stream.destroyed) {
     return
@@ -398,13 +454,22 @@ function sendCachedValue (handler, opts, result, age, context, isStale) {

   controller.rawHeaders = util.toRawHeaders(headers)

-  handler.onResponseStart?.(controller, result.statusCode, headers, result.statusMessage)
+  try {
+    handler.onResponseStart?.(controller, result.statusCode, headers, result.statusMessage)
+  } catch (err) {
+    stream.destroy(err)
+    return
+  }

   if (opts.method === 'HEAD') {
     stream.destroy()
   } else {
     stream.on('data', function (chunk) {
-      handler.onResponseData?.(controller, chunk)
+      try {
+        handler.onResponseData?.(controller, chunk)
+      } catch (err) {
+        stream.destroy(err)
+      }
     })
   }
 }
@@ -502,6 +567,14 @@ function handleResult (
     }

     const headers = makeRevalidationHeaders(opts, result)
+    const refetch = () => {
+      if (util.isStream(result.body)) {
+        result.body.on('error', nop).destroy()
+      }
+
+      deleteCachedValue(globalOpts.store, cacheKey)
+      return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
+    }

     // We need to revalidate the response
     return dispatch(
@@ -513,24 +586,18 @@ function handleResult (
         (success, context, statusCode, headers) => {
           if (success) {
             if (statusCode === 304) {
-              if (revalidationResponseDisallowsCachedReuse(globalOpts.type, headers)) {
-                if (util.isStream(result.body)) {
-                  result.body.on('error', nop).destroy()
-                }
-
-                deleteCachedValue(globalOpts.store, cacheKey)
-                return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
+              if (
+                !revalidationTargetMatchesCacheKey(cacheKey, context) ||
+                !revalidationEtagMatchesCachedValue(result, headers) ||
+                revalidationResponseDisallowsCachedReuse(globalOpts.type, headers)
+              ) {
+                return refetch()
               }

               if (revalidationResponseUpdatesCacheControl(headers)) {
                 deleteCachedValue(globalOpts.store, cacheKey)
               } else if (revalidationResponseAddsVary(result, headers.vary ? parseVaryHeader(headers.vary, opts.headers) : undefined)) {
-                if (util.isStream(result.body)) {
-                  result.body.on('error', nop).destroy()
-                }
-
-                deleteCachedValue(globalOpts.store, cacheKey)
-                return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
+                return refetch()
               }
             }

@@ -666,6 +733,28 @@ module.exports = (opts = {}) => {
             reqCacheControl,
             result
           ))
+          .catch((err) => {
+            // Any throw escaping handleResult on the async-store path (e.g. a
+            // rejection from store.get() itself, or a throw in a code path not
+            // converged by sendCachedValue) would otherwise surface as an
+            // unhandled promise rejection and crash the process. Route it to
+            // the handler instead, matching the synchronous store path where
+            // the error propagates to the request() call site.
+            if (typeof handler.onResponseError === 'function') {
+              handler.onResponseError({
+                rawHeaders: [],
+                rawTrailers: [],
+                pause () {},
+                resume () {},
+                get paused () { return false },
+                get aborted () { return false },
+                get reason () { return null },
+                abort () {}
+              }, err)
+            } else {
+              throw err
+            }
+          })
       } else {
         return handleResult(
           dispatch,
diff --git a/deps/undici/src/lib/mock/mock-utils.js b/deps/undici/src/lib/mock/mock-utils.js
index 00c1a0bbe13..cf53e3ad5b0 100644
--- a/deps/undici/src/lib/mock/mock-utils.js
+++ b/deps/undici/src/lib/mock/mock-utils.js
@@ -240,6 +240,13 @@ function removeTrailingSlash (path) {
     return path
   }

+  // Only strip slashes from the pathname, not from a query string after it
+  const queryIndex = path.indexOf('?')
+  const search = queryIndex === -1 ? '' : path.slice(queryIndex)
+  if (queryIndex !== -1) {
+    path = path.slice(0, queryIndex)
+  }
+
   while (path.endsWith('/')) {
     path = path.slice(0, -1)
   }
@@ -248,7 +255,7 @@ function removeTrailingSlash (path) {
     path = '/'
   }

-  return path
+  return path + search
 }

 function buildKey (opts) {
@@ -379,6 +386,9 @@ function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler, re
     paused: false,
     rawHeaders: null,
     rawTrailers: null,
+    get aborted () {
+      return aborted
+    },
     pause () {
       this.paused = true
     },
diff --git a/deps/undici/src/lib/web/fetch/body.js b/deps/undici/src/lib/web/fetch/body.js
index 28124d018c0..b4ea3cbfe41 100644
--- a/deps/undici/src/lib/web/fetch/body.js
+++ b/deps/undici/src/lib/web/fetch/body.js
@@ -229,16 +229,36 @@ function extractBody (object, keepalive = false) {
       )
     }

-    stream = webidl.is.ReadableStream(object)
-      ? object
-      : ReadableStream.from(object).pipeThrough(new TransformStream({
-        transform (chunk, controller) {
-          const bytes = isUint8Array(chunk) ? chunk : Buffer.from(chunk)
-          if (bytes.byteLength) {
-            controller.enqueue(bytes)
+    if (webidl.is.ReadableStream(object)) {
+      stream = object
+    } else {
+      const reader = ReadableStream.from(object).getReader()
+      let cancelled = false
+      // Do not pull from the iterable until the body is read.
+      stream = new ReadableStream({
+        async pull (controller) {
+          while (true) {
+            const { done, value } = await reader.read()
+            if (cancelled) {
+              return
+            }
+            if (done) {
+              controller.close()
+              return
+            }
+            const bytes = isUint8Array(value) ? value : Buffer.from(value)
+            if (bytes.byteLength) {
+              controller.enqueue(bytes)
+              return
+            }
           }
+        },
+        cancel (reason) {
+          cancelled = true
+          return reader.cancel(reason)
         }
-      }))
+      }, { highWaterMark: 0 })
+    }
   }

   // 18. If source is a byte sequence, then set action to a
diff --git a/deps/undici/src/lib/web/fetch/index.js b/deps/undici/src/lib/web/fetch/index.js
index a9382ca325b..b90f6c8d866 100644
--- a/deps/undici/src/lib/web/fetch/index.js
+++ b/deps/undici/src/lib/web/fetch/index.js
@@ -290,10 +290,7 @@ function fetch (input, init = undefined) {
       cleanupAbortListeners()
     },
     processResponse,
-    dispatcher: getRequestDispatcher(requestObject), // undici
-    // Keep requestObject alive to prevent its AbortController from being GC'd
-    // See https://github.com/nodejs/undici/issues/4627
-    requestObject
+    dispatcher: getRequestDispatcher(requestObject) // undici
   })

   // 14. Return p.
@@ -412,8 +409,7 @@ function fetching ({
   processResponseEndOfBody,
   processResponseConsumeBody,
   useParallelQueue = false,
-  dispatcher = getGlobalDispatcher(), // undici
-  requestObject = null // Keep alive to prevent AbortController GC, see #4627
+  dispatcher = getGlobalDispatcher() // undici
 }) {
   // Ensure that the dispatcher is set accordingly
   assert(dispatcher)
@@ -467,9 +463,7 @@ function fetching ({
     processResponseConsumeBody,
     processResponseEndOfBody,
     taskDestination,
-    crossOriginIsolatedCapability,
-    // Keep requestObject alive to prevent its AbortController from being GC'd
-    requestObject
+    crossOriginIsolatedCapability
   }

   // 7. If request’s body is a byte sequence, then set request’s body to
diff --git a/deps/undici/src/lib/web/fetch/request.js b/deps/undici/src/lib/web/fetch/request.js
index 66f8150cb82..cce8b8e323e 100644
--- a/deps/undici/src/lib/web/fetch/request.js
+++ b/deps/undici/src/lib/web/fetch/request.js
@@ -26,60 +26,98 @@ const { webidl } = require('../webidl')
 const { URLSerializer } = require('./data-url')
 const { kConstruct } = require('../../core/symbols')
 const assert = require('node:assert')
-const { getMaxListeners, setMaxListeners, defaultMaxListeners } = require('node:events')

-const kAbortController = Symbol('abortController')
-
-const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
-  signal.removeEventListener('abort', abort)
+const requestFinalizer = new FinalizationRegistry(({ signal, reference }) => {
+  removeSignalFollower(signal, reference)
 })

-const dependentControllerMap = new WeakMap()
+/**
+ * @typedef {object} RequestSignalState
+ * @property {AbortController} controller
+ * @property {AbortSignal} source
+ * @property {WeakRef<AbortController>} reference
+ */
+
+/** @type {WeakMap<AbortSignal, RequestSignalState>} */
+const requestSignals = new WeakMap()
+
+/** @type {WeakMap<AbortSignal, WeakRef<AbortController> | Set<WeakRef<AbortController>>>} */
+const signalFollowers = new WeakMap()

-let abortSignalHasEventHandlerLeakWarning
+/**
+ * @param {AbortSignal} signal
+ * @param {WeakRef<AbortController>} reference
+ */
+function removeSignalFollower (signal, reference) {
+  const followers = signalFollowers.get(signal)
+  if (followers instanceof Set) {
+    followers.delete(reference)
+    if (followers.size !== 0) return
+  }

-try {
-  abortSignalHasEventHandlerLeakWarning = getMaxListeners(new AbortController().signal) > 0
-} catch {
-  abortSignalHasEventHandlerLeakWarning = false
+  signal.removeEventListener('abort', abortSignalFollowers)
+  signalFollowers.delete(signal)
 }

-function buildAbort (acRef) {
-  return abort
-
-  function abort () {
-    const ac = acRef.deref()
-    if (ac !== undefined) {
-      // Currently, there is a problem with FinalizationRegistry.
-      // https://github.com/nodejs/node/issues/49344
-      // https://github.com/nodejs/node/issues/47748
-      // In the case of abort, the first step is to unregister from it.
-      // If the controller can refer to it, it is still registered.
-      // It will be removed in the future.
-      requestFinalizer.unregister(abort)
-
-      // Unsubscribe a listener.
-      // FinalizationRegistry will no longer be called, so this must be done.
-      this.removeEventListener('abort', abort)
-
-      ac.abort(this.reason)
-
-      const controllerList = dependentControllerMap.get(ac.signal)
-
-      if (controllerList !== undefined) {
-        if (controllerList.size !== 0) {
-          for (const ref of controllerList) {
-            const ctrl = ref.deref()
-            if (ctrl !== undefined) {
-              ctrl.abort(this.reason)
-            }
-          }
-          controllerList.clear()
-        }
-        dependentControllerMap.delete(ac.signal)
-      }
+/**
+ * @param {WeakRef<AbortController>} reference
+ * @param {unknown} reason
+ */
+function abortSignalFollower (reference, reason) {
+  requestFinalizer.unregister(reference)
+  const ac = reference.deref()
+  if (ac !== undefined) {
+    requestSignals.delete(ac.signal)
+    ac.abort(reason)
+  }
+}
+
+/** @this {AbortSignal} */
+function abortSignalFollowers () {
+  this.removeEventListener('abort', abortSignalFollowers)
+  const followers = signalFollowers.get(this)
+  if (followers instanceof Set) {
+    for (const reference of followers) {
+      abortSignalFollower(reference, this.reason)
     }
+    followers.clear()
+  } else {
+    abortSignalFollower(followers, this.reason)
+  }
+  signalFollowers.delete(this)
+}
+
+/**
+ * @param {AbortSignal | null | undefined} signal
+ */
+function createDependentSignal (signal) {
+  const ac = new AbortController()
+  if (signal == null) return ac.signal
+
+  if (signal.aborted) {
+    ac.abort(signal.reason)
+    return ac.signal
+  }
+
+  const reference = new WeakRef(ac)
+  const followers = signalFollowers.get(signal)
+  if (followers === undefined) {
+    util.addAbortListener(signal, abortSignalFollowers)
+    signalFollowers.set(signal, reference)
+  } else if (followers instanceof WeakRef) {
+    const controllers = new Set()
+    controllers.add(followers)
+    controllers.add(reference)
+    signalFollowers.set(signal, controllers)
+  } else {
+    followers.add(reference)
   }
+
+  requestFinalizer.register(ac, { signal, reference }, reference)
+
+  // The signal must keep its controller alive even after its Request is collected.
+  requestSignals.set(ac.signal, { controller: ac, source: signal, reference })
+  return ac.signal
 }

 let patchMethodWarning = false
@@ -98,13 +136,6 @@ class Request {

   #state

-  /**
-   * Removes the `abort` listener that makes this request's signal follow the
-   * passed signal. `null` when no such listener was registered.
-   * @type {(() => void) | null}
-   */
-  #abortCleanup = null
-
   // https://fetch.spec.whatwg.org/#dom-request
   constructor (input, init = undefined) {
     webidl.util.markAsUncloneable(this)
@@ -418,51 +449,8 @@ class Request {
     // 27. Set this’s request to request.
     this.#state = request

-    // 28. Set this’s signal to a new AbortSignal object with this’s relevant
-    // Realm.
-    // TODO: could this be simplified with AbortSignal.any
-    // (https://dom.spec.whatwg.org/#dom-abortsignal-any)
-    const ac = new AbortController()
-    this.#signal = ac.signal
-
-    // 29. If signal is not null, then make this’s signal follow signal.
-    if (signal != null) {
-      if (signal.aborted) {
-        ac.abort(signal.reason)
-      } else {
-        // Keep a strong ref to ac while request object
-        // is alive. This is needed to prevent AbortController
-        // from being prematurely garbage collected.
-        // See, https://github.com/nodejs/undici/issues/1926.
-        this[kAbortController] = ac
-
-        const acRef = new WeakRef(ac)
-        const abort = buildAbort(acRef)
-
-        // If the max amount of listeners is equal to the default, increase it
-        if (abortSignalHasEventHandlerLeakWarning && getMaxListeners(signal) === defaultMaxListeners) {
-          setMaxListeners(1500, signal)
-        }
-
-        const removeAbortListener = util.addAbortListener(signal, abort)
-        // The third argument must be a registry key to be unregistered.
-        // Without it, you cannot unregister.
-        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
-        // abort is used as the unregister key. (because it is unique)
-        requestFinalizer.register(ac, { signal, abort }, abort)
-
-        // Allow the listener to be removed deterministically once the fetch
-        // that owns this request has settled, instead of relying solely on the
-        // FinalizationRegistry (i.e. garbage collection). Reusing a single
-        // signal across many requests would otherwise leak listeners.
-        // See https://github.com/nodejs/undici/issues/5285
-        this.#abortCleanup = () => {
-          requestFinalizer.unregister(abort)
-          removeAbortListener()
-          this.#abortCleanup = null
-        }
-      }
-    }
+    // 28-29. Create a signal that follows the supplied signal, if any.
+    this.#signal = createDependentSignal(signal)

     // 30. Set this’s headers to a new Headers object with this’s relevant
     // Realm, whose header list is request’s header list and guard is
@@ -792,25 +780,10 @@ class Request {
     // 3. Let clonedRequestObject be the result of creating a Request object,
     // given clonedRequest, this’s headers’s guard, and this’s relevant Realm.
     // 4. Make clonedRequestObject’s signal follow this’s signal.
-    const ac = new AbortController()
-    if (this.signal.aborted) {
-      ac.abort(this.signal.reason)
-    } else {
-      let list = dependentControllerMap.get(this.signal)
-      if (list === undefined) {
-        list = new Set()
-        dependentControllerMap.set(this.signal, list)
-      }
-      const acRef = new WeakRef(ac)
-      list.add(acRef)
-      util.addAbortListener(
-        ac.signal,
-        buildAbort(acRef)
-      )
-    }
+    const signal = createDependentSignal(this.#signal)

     // 4. Return clonedRequestObject.
-    return fromInnerRequest(clonedRequest, this.#dispatcher, ac.signal, getHeadersGuard(this.#headers))
+    return fromInnerRequest(clonedRequest, this.#dispatcher, signal, getHeadersGuard(this.#headers))
   }

   [nodeUtil.inspect.custom] (depth, options) {
@@ -894,7 +867,12 @@ class Request {
      * @param {Request} request
      */
     removeRequestAbortListener = (request) => {
-      request.#abortCleanup?.()
+      const state = requestSignals.get(request.#signal)
+      if (state !== undefined) {
+        requestSignals.delete(request.#signal)
+        requestFinalizer.unregister(state.reference)
+        removeSignalFollower(state.source, state.reference)
+      }
     }

     webidl.is.Request = (arg) => {
diff --git a/deps/undici/src/lib/web/subresource-integrity/subresource-integrity.js b/deps/undici/src/lib/web/subresource-integrity/subresource-integrity.js
index f7d165a9534..a10e08ecc75 100644
--- a/deps/undici/src/lib/web/subresource-integrity/subresource-integrity.js
+++ b/deps/undici/src/lib/web/subresource-integrity/subresource-integrity.js
@@ -197,8 +197,8 @@ function parseMetadata (metadata) {
   /** @type {MetadataList} */
   const result = []

-  // 2. For each item returned by splitting metadata on spaces:
-  for (const item of metadata.split(' ')) {
+  // 2. For each item returned by splitting metadata on ASCII whitespace:
+  for (const item of metadata.split(/[\t\n\f\r ]+/)) {
     // 1. Let expression-and-options be the result of splitting item on U+003F (?).
     const expressionAndOptions = item.split('?', 1)

diff --git a/deps/undici/src/package-lock.json b/deps/undici/src/package-lock.json
index c7745ffcba5..63b2cd1746a 100644
--- a/deps/undici/src/package-lock.json
+++ b/deps/undici/src/package-lock.json
@@ -1,15 +1,15 @@
 {
   "name": "undici",
-  "version": "8.12.3",
+  "version": "8.12.4",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "undici",
-      "version": "8.12.3",
+      "version": "8.12.4",
       "license": "MIT",
       "devDependencies": {
-        "@fastify/busboy": "3.2.3",
+        "@fastify/busboy": "3.2.4",
         "@matteo.collina/tspl": "^0.2.0",
         "@metcoder95/https-pem": "^1.0.0",
         "@sinonjs/fake-timers": "^12.0.0",
@@ -1196,9 +1196,9 @@
       }
     },
     "node_modules/@fastify/busboy": {
-      "version": "3.2.3",
-      "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.3.tgz",
-      "integrity": "sha512-fmo/IKgkenPzz3qTKG+sIUXdF/Kv83AkvvOXJHPTHJeKJbat8SsL1ihUaFaDotqoEl4UZ1Lf8Eb/3vK6eaWX2A==",
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.4.tgz",
+      "integrity": "sha512-lQS8xcXT54rdJb+wlG/Q7SMSdtkZEpLPHooSDUZyyZm9LJD7X1kONPhVoPaHq+dsPtWUGGLIz9gBNhs9qKbylw==",
       "dev": true,
       "license": "MIT"
     },
@@ -4557,9 +4557,9 @@
       }
     },
     "node_modules/eslint": {
-      "version": "10.12.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.12.0.tgz",
-      "integrity": "sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==",
+      "version": "10.13.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.13.0.tgz",
+      "integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==",
       "dev": true,
       "license": "MIT",
       "workspaces": [
@@ -6733,9 +6733,9 @@
       }
     },
     "node_modules/jsondiffpatch": {
-      "version": "0.7.6",
-      "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.7.6.tgz",
-      "integrity": "sha512-zE9+AXFq+MkTolDor2Cw1nJzLC0aleqPkYf52Kb4Kn4mJcka/gFHpGI2JBVEJCfWOvBl0OoxZS+wuLdislQcqg==",
+      "version": "0.7.7",
+      "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.7.7.tgz",
+      "integrity": "sha512-C3375q+xluwQYS6Je613YqXpZhW9uSi2YFRR+8QTJVYhdk0GZiDr7HD9bXNrdV9PJCpmwsCgJM9ZXXESSZPCzA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -7103,9 +7103,9 @@
       "license": "MIT"
     },
     "node_modules/neostandard": {
-      "version": "0.14.0",
-      "resolved": "https://registry.npmjs.org/neostandard/-/neostandard-0.14.0.tgz",
-      "integrity": "sha512-11ZZgQdQrzdMqAo494FJzrSh5/lNtUyrx1mz7BVXcA9EpXCEBOz4WhVz5sG79VvoBG/4L5lZ8xjfHxng95CtFQ==",
+      "version": "0.14.1",
+      "resolved": "https://registry.npmjs.org/neostandard/-/neostandard-0.14.1.tgz",
+      "integrity": "sha512-cEGi7ADMWJRTcdsZeDXCyA1ZeJLQPBq6HP9z9CE1ysu+CJOKYt59ADG0BT6ETonuF6xNXZbMiNc8S8prYys4aQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -7195,9 +7195,9 @@
       "license": "MIT"
     },
     "node_modules/node-forge": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz",
-      "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.2.tgz",
+      "integrity": "sha512-rRtAR5XLJ8gCKd9du4qrKAsgCG1ObGP4gLeNJMa4vYSeq4bmBMOJk5IHkrviBV0q7uoAJlVd8HEQYQSVUb4FGQ==",
       "dev": true,
       "license": "(BSD-3-Clause OR GPL-2.0)",
       "engines": {
@@ -8762,9 +8762,9 @@
       }
     },
     "node_modules/undici": {
-      "version": "6.29.0",
-      "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz",
-      "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==",
+      "version": "6.29.1",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz",
+      "integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==",
       "dev": true,
       "license": "MIT",
       "engines": {
diff --git a/deps/undici/src/package.json b/deps/undici/src/package.json
index 0f32b36afb4..d05d075a005 100644
--- a/deps/undici/src/package.json
+++ b/deps/undici/src/package.json
@@ -1,6 +1,6 @@
 {
   "name": "undici",
-  "version": "8.12.3",
+  "version": "8.12.4",
   "description": "An HTTP/1.1 client, written from scratch for Node.js",
   "homepage": "https://undici.nodejs.org",
   "bugs": {
@@ -111,7 +111,7 @@
     "prepare": "husky && node ./scripts/platform-shell.js"
   },
   "devDependencies": {
-    "@fastify/busboy": "3.2.3",
+    "@fastify/busboy": "3.2.4",
     "@matteo.collina/tspl": "^0.2.0",
     "@metcoder95/https-pem": "^1.0.0",
     "@sinonjs/fake-timers": "^12.0.0",
diff --git a/deps/undici/src/types/dispatcher.d.ts b/deps/undici/src/types/dispatcher.d.ts
index 1c2e3357c0b..86f51f409cb 100644
--- a/deps/undici/src/types/dispatcher.d.ts
+++ b/deps/undici/src/types/dispatcher.d.ts
@@ -231,7 +231,7 @@ declare namespace Dispatcher {
     onRequestSent?(): void;
   }
   export type PipelineHandler<TOpaque = null> = (data: PipelineHandlerData<TOpaque>) => Readable
-  export type HttpMethod = Autocomplete<'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH'>
+  export type HttpMethod = Autocomplete<'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH' | 'QUERY'>

   /**
    * @link https://fetch.spec.whatwg.org/#body-mixin
diff --git a/deps/undici/src/types/formdata.d.ts b/deps/undici/src/types/formdata.d.ts
index b9819a7e725..67d4532b2a1 100644
--- a/deps/undici/src/types/formdata.d.ts
+++ b/deps/undici/src/types/formdata.d.ts
@@ -76,33 +76,33 @@ export declare class FormData {
   /**
    * Executes given callback function for each field of the FormData instance
    */
-  forEach: (
+  forEach (
     callbackfn: (value: FormDataEntryValue, key: string, iterable: FormData) => void,
     thisArg?: unknown
-  ) => void
+  ): void

   /**
    * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through all keys contained in this `FormData` object.
    * Each key is a `string`.
    */
-  keys: () => SpecIterableIterator<string>
+  keys (): SpecIterableIterator<string>

   /**
    * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through all values contained in this object `FormData` object.
    * Each value is a [`FormDataValue`](https://developer.mozilla.org/en-US/docs/Web/API/FormDataEntryValue).
    */
-  values: () => SpecIterableIterator<FormDataEntryValue>
+  values (): SpecIterableIterator<FormDataEntryValue>

   /**
    * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through the `FormData` key/value pairs.
    * The key of each pair is a string; the value is a [`FormDataValue`](https://developer.mozilla.org/en-US/docs/Web/API/FormDataEntryValue).
    */
-  entries: () => SpecIterableIterator<[string, FormDataEntryValue]>
+  entries (): SpecIterableIterator<[string, FormDataEntryValue]>

   /**
    * An alias for FormData#entries()
    */
-  [Symbol.iterator]: () => SpecIterableIterator<[string, FormDataEntryValue]>
+  [Symbol.iterator] (): SpecIterableIterator<[string, FormDataEntryValue]>

   readonly [Symbol.toStringTag]: string
 }
diff --git a/deps/undici/src/types/retry-handler.d.ts b/deps/undici/src/types/retry-handler.d.ts
index 3bc484b2d07..180cde92166 100644
--- a/deps/undici/src/types/retry-handler.d.ts
+++ b/deps/undici/src/types/retry-handler.d.ts
@@ -37,7 +37,7 @@ declare namespace RetryHandler {
   export interface RetryOptions {
     /**
      * If true, the retry handler will throw an error if the request fails,
-     * this will prevent the folling handlers from being called, and will destroy the socket.
+     * this will prevent the following handlers from being called, and will destroy the socket.
      *
      * @type {boolean}
      * @memberof RetryOptions
@@ -98,14 +98,14 @@ declare namespace RetryHandler {
      *
      * @type {Dispatcher.HttpMethod[]}
      * @memberof RetryOptions
-     * @default ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'],
+     * @default ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE', 'QUERY'],
      */
     methods?: Dispatcher.HttpMethod[];
     /**
      * Error codes to be retried. e.g. `ECONNRESET`, `ENOTFOUND`, `ETIMEDOUT`, `ECONNREFUSED`, etc.
      *
      * @type {string[]}
-     * @default ['ECONNRESET','ECONNREFUSED','ENOTFOUND','ENETDOWN','ENETUNREACH','EHOSTDOWN','EHOSTUNREACH','EPIPE']
+     * @default ['ECONNRESET','ECONNREFUSED','ENOTFOUND','ENETDOWN','ENETUNREACH','EHOSTDOWN','EHOSTUNREACH','EPIPE','UND_ERR_SOCKET']
      */
     errorCodes?: string[];
     /**
diff --git a/deps/undici/undici.js b/deps/undici/undici.js
index 52529820e91..552dafce800 100644
--- a/deps/undici/undici.js
+++ b/deps/undici/undici.js
@@ -7045,14 +7045,35 @@ Content-Type: ${value.type || "application/octet-stream"}\r
             "Response body object should not be disturbed or locked"
           );
         }
-        stream = webidl.is.ReadableStream(object) ? object : ReadableStream.from(object).pipeThrough(new TransformStream({
-          transform(chunk, controller) {
-            const bytes = isUint8Array(chunk) ? chunk : Buffer.from(chunk);
-            if (bytes.byteLength) {
-              controller.enqueue(bytes);
+        if (webidl.is.ReadableStream(object)) {
+          stream = object;
+        } else {
+          const reader = ReadableStream.from(object).getReader();
+          let cancelled = false;
+          stream = new ReadableStream({
+            async pull(controller) {
+              while (true) {
+                const { done, value } = await reader.read();
+                if (cancelled) {
+                  return;
+                }
+                if (done) {
+                  controller.close();
+                  return;
+                }
+                const bytes = isUint8Array(value) ? value : Buffer.from(value);
+                if (bytes.byteLength) {
+                  controller.enqueue(bytes);
+                  return;
+                }
+              }
+            },
+            cancel(reason) {
+              cancelled = true;
+              return reader.cancel(reason);
             }
-          }
-        }));
+          }, { highWaterMark: 0 });
+        }
       }
       if (typeof source === "string" || isUint8Array(source)) {
         action = /* @__PURE__ */ __name(() => source, "action");
@@ -7292,7 +7313,9 @@ var require_client_h1 = __commonJS({
       const llhttpWasmData = process.env.JEST_WORKER_ID ? require_llhttp_wasm() : void 0;
       let mod;
       let useWasmSIMD = true;
-      if (process.arch === "ppc64") {
+      if (process.arch === "riscv64") {
+        useWasmSIMD = false;
+      } else if (process.arch === "ppc64") {
         const [major, minor] = process.versions.node.split(".").map((n) => parseInt(n, 10));
         if (major < 24 || major === 24 && minor < 12) {
           useWasmSIMD = false;
@@ -8638,7 +8661,6 @@ var require_client_h2 = __commonJS({
     } = require_symbols();
     var { channels } = require_diagnostics();
     var kOpenStreams = /* @__PURE__ */ Symbol("open streams");
-    var kRequestStreamId = /* @__PURE__ */ Symbol("request stream id");
     var kRequestStream = /* @__PURE__ */ Symbol("request stream");
     var kRequestStreamCleanup = /* @__PURE__ */ Symbol("request stream cleanup");
     var kRequestStreamState = /* @__PURE__ */ Symbol("request stream state");
@@ -8646,6 +8668,7 @@ var require_client_h2 = __commonJS({
     var kGoAwayReplayAttempts = /* @__PURE__ */ Symbol("goaway replay attempts");
     var kRefusedStreamRetry = /* @__PURE__ */ Symbol("refused stream retry");
     var MAX_GOAWAY_REPLAY_ATTEMPTS = 1;
+    var HTTP2_FRAME_TYPE_HEADERS = 1;
     var MIN_PROBE_TIMEOUT = 1e3;
     var extractBody;
     var http2;
@@ -8699,7 +8722,8 @@ var require_client_h2 = __commonJS({
         if (request == null) {
           continue;
         }
-        if (typeof request[kRequestStreamId] !== "number" || request[kRequestStreamId] > maxAcceptedStreamID) {
+        const streamId = request[kRequestStream]?.id;
+        if (typeof streamId !== "number" || streamId > maxAcceptedStreamID) {
           return i;
         }
       }
@@ -8707,7 +8731,6 @@ var require_client_h2 = __commonJS({
     }
     __name(getGoAwayPendingIdx, "getGoAwayPendingIdx");
     function detachRequestFromStream(request) {
-      request[kRequestStreamId] = null;
       request[kRequestStream] = null;
       request[kRequestStreamCleanup] = null;
     }
@@ -8717,7 +8740,6 @@ var require_client_h2 = __commonJS({
       const previousStream = request[kRequestStream];
       detachRequestFromStream(request);
       previousCleanup?.(previousStream);
-      request[kRequestStreamId] = stream.id;
       request[kRequestStream] = stream;
       request[kRequestStreamCleanup] = cleanup;
     }
@@ -8795,7 +8817,8 @@ var require_client_h2 = __commonJS({
         closeStreamSession(stream);
       }
       if (!stream.destroyed && !stream.closed) {
-        stream.once("error", noop);
+        stream.off("error", noop);
+        stream.on("error", noop);
       }
     }
     __name(severRequestStream, "severRequestStream");
@@ -9151,14 +9174,12 @@ var require_client_h2 = __commonJS({
           }
         }
       }
-      for (let i = 0; i < streamsToClose.length; i++) {
-        closeStream(streamsToClose[i]);
-      }
       if (pendingIdx !== previousPendingIdx) {
         const remainingPendingRequests = client[kQueue].slice(previousPendingIdx);
         client[kQueue].length = pendingIdx;
         client[kQueue].push(...retriableRequests, ...remainingPendingRequests);
       }
+      client[kPendingIdx] = pendingIdx;
       if (client[kHTTP2Session] === this) {
         client[kSocket] = null;
         client[kHTTPContext] = null;
@@ -9170,10 +9191,12 @@ var require_client_h2 = __commonJS({
       clearHttp2IdleTimeout(this);
       clearNoStreamsTimeout(this);
       clearHttp2ProbeTimeout(this);
+      for (let i = 0; i < streamsToClose.length; i++) {
+        closeStream(streamsToClose[i]);
+      }
       if (!this.closed && !this.destroyed) {
         this.close();
       }
-      client[kPendingIdx] = pendingIdx;
       client.emit("disconnect", client[kUrl], [client], err);
       client[kResume]();
     }
@@ -9257,7 +9280,6 @@ var require_client_h2 = __commonJS({
     }
     __name(closeStreamSession, "closeStreamSession");
     function onUpgradeStreamClose() {
-      this.off("error", noop);
       const state = this[kRequestStreamState];
       this[kRequestStreamState] = null;
       failUpgradeStream(state, new InformationalError("HTTP/2: stream closed before response headers"));
@@ -9335,7 +9357,7 @@ var require_client_h2 = __commonJS({
       }
       removeUpgradeStreamListeners(stream);
       if (!stream.destroyed && !stream.closed) {
-        stream.once("error", noop);
+        stream.on("error", noop);
       }
     }
     __name(releaseUpgradeStream, "releaseUpgradeStream");
@@ -9640,7 +9662,7 @@ var require_client_h2 = __commonJS({
       }
       if (!stream.destroyed && !stream.closed) {
         removeRequestStreamListeners(stream);
-        stream.once("error", noop);
+        stream.on("error", noop);
       }
     }
     __name(releaseRequestStream, "releaseRequestStream");
@@ -9760,15 +9782,29 @@ var require_client_h2 = __commonJS({
         return false;
       }
       request[kRefusedStreamRetry] = true;
+      requeueStreamRequest(client, state);
+      return true;
+    }
+    __name(retryRefusedStream, "retryRefusedStream");
+    function retryUnsentStream(state) {
+      const { client, request, session } = state;
+      if (!session[kReceivedGoAway] || state.responseReceived || request.aborted || request.completed || !canReplayRequest(request) || !registerGoAwayRefusal(request)) {
+        return false;
+      }
+      requeueStreamRequest(client, state);
+      return true;
+    }
+    __name(retryUnsentStream, "retryUnsentStream");
+    function requeueStreamRequest(client, state) {
+      const { request } = state;
       detachRequestStreamForClose(request);
       state.stream = null;
       state.requestFinalized = true;
       completeRequest(client, request);
       client[kQueue].splice(client[kPendingIdx], 0, request);
       client[kResume]();
-      return true;
     }
-    __name(retryRefusedStream, "retryRefusedStream");
+    __name(requeueStreamRequest, "requeueStreamRequest");
     function onError(err) {
       const stream = this;
       const state = stream[kRequestStreamState];
@@ -9776,6 +9812,7 @@ var require_client_h2 = __commonJS({
         return;
       }
       stream.off("error", onError);
+      stream.on("error", noop);
       if (state.responseContentLength != null && state.bytesRead !== state.responseContentLength) {
         err = new ResponseContentLengthMismatchError();
       }
@@ -9795,6 +9832,9 @@ var require_client_h2 = __commonJS({
         return;
       }
       stream.off("frameError", onFrameError);
+      if (type === HTTP2_FRAME_TYPE_HEADERS && retryUnsentStream(state)) {
+        return;
+      }
       state.abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`));
     }
     __name(onFrameError, "onFrameError");
@@ -13311,44 +13351,69 @@ var require_request2 = __commonJS({
     var { URLSerializer } = require_data_url();
     var { kConstruct } = require_symbols();
     var assert = require("node:assert");
-    var { getMaxListeners, setMaxListeners, defaultMaxListeners } = require("node:events");
-    var kAbortController = /* @__PURE__ */ Symbol("abortController");
-    var requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
-      signal.removeEventListener("abort", abort);
+    var requestFinalizer = new FinalizationRegistry(({ signal, reference }) => {
+      removeSignalFollower(signal, reference);
     });
-    var dependentControllerMap = /* @__PURE__ */ new WeakMap();
-    var abortSignalHasEventHandlerLeakWarning;
-    try {
-      abortSignalHasEventHandlerLeakWarning = getMaxListeners(new AbortController().signal) > 0;
-    } catch {
-      abortSignalHasEventHandlerLeakWarning = false;
-    }
-    function buildAbort(acRef) {
-      return abort;
-      function abort() {
-        const ac = acRef.deref();
-        if (ac !== void 0) {
-          requestFinalizer.unregister(abort);
-          this.removeEventListener("abort", abort);
-          ac.abort(this.reason);
-          const controllerList = dependentControllerMap.get(ac.signal);
-          if (controllerList !== void 0) {
-            if (controllerList.size !== 0) {
-              for (const ref of controllerList) {
-                const ctrl = ref.deref();
-                if (ctrl !== void 0) {
-                  ctrl.abort(this.reason);
-                }
-              }
-              controllerList.clear();
-            }
-            dependentControllerMap.delete(ac.signal);
-          }
-        }
+    var requestSignals = /* @__PURE__ */ new WeakMap();
+    var signalFollowers = /* @__PURE__ */ new WeakMap();
+    function removeSignalFollower(signal, reference) {
+      const followers = signalFollowers.get(signal);
+      if (followers instanceof Set) {
+        followers.delete(reference);
+        if (followers.size !== 0) return;
+      }
+      signal.removeEventListener("abort", abortSignalFollowers);
+      signalFollowers.delete(signal);
+    }
+    __name(removeSignalFollower, "removeSignalFollower");
+    function abortSignalFollower(reference, reason) {
+      requestFinalizer.unregister(reference);
+      const ac = reference.deref();
+      if (ac !== void 0) {
+        requestSignals.delete(ac.signal);
+        ac.abort(reason);
+      }
+    }
+    __name(abortSignalFollower, "abortSignalFollower");
+    function abortSignalFollowers() {
+      this.removeEventListener("abort", abortSignalFollowers);
+      const followers = signalFollowers.get(this);
+      if (followers instanceof Set) {
+        for (const reference of followers) {
+          abortSignalFollower(reference, this.reason);
+        }
+        followers.clear();
+      } else {
+        abortSignalFollower(followers, this.reason);
       }
-      __name(abort, "abort");
+      signalFollowers.delete(this);
     }
-    __name(buildAbort, "buildAbort");
+    __name(abortSignalFollowers, "abortSignalFollowers");
+    function createDependentSignal(signal) {
+      const ac = new AbortController();
+      if (signal == null) return ac.signal;
+      if (signal.aborted) {
+        ac.abort(signal.reason);
+        return ac.signal;
+      }
+      const reference = new WeakRef(ac);
+      const followers = signalFollowers.get(signal);
+      if (followers === void 0) {
+        util.addAbortListener(signal, abortSignalFollowers);
+        signalFollowers.set(signal, reference);
+      } else if (followers instanceof WeakRef) {
+        const controllers = /* @__PURE__ */ new Set();
+        controllers.add(followers);
+        controllers.add(reference);
+        signalFollowers.set(signal, controllers);
+      } else {
+        followers.add(reference);
+      }
+      requestFinalizer.register(ac, { signal, reference }, reference);
+      requestSignals.set(ac.signal, { controller: ac, source: signal, reference });
+      return ac.signal;
+    }
+    __name(createDependentSignal, "createDependentSignal");
     var patchMethodWarning = false;
     var setRequestSignal;
     var getRequestDispatcher;
@@ -13368,12 +13433,6 @@ var require_request2 = __commonJS({
       /** @type {Headers} */
       #headers;
       #state;
-      /**
-       * Removes the `abort` listener that makes this request's signal follow the
-       * passed signal. `null` when no such listener was registered.
-       * @type {(() => void) | null}
-       */
-      #abortCleanup = null;
       // https://fetch.spec.whatwg.org/#dom-request
       constructor(input, init = void 0) {
         webidl.util.markAsUncloneable(this);
@@ -13559,27 +13618,7 @@ var require_request2 = __commonJS({
           signal = init.signal;
         }
         this.#state = request;
-        const ac = new AbortController();
-        this.#signal = ac.signal;
-        if (signal != null) {
-          if (signal.aborted) {
-            ac.abort(signal.reason);
-          } else {
-            this[kAbortController] = ac;
-            const acRef = new WeakRef(ac);
-            const abort = buildAbort(acRef);
-            if (abortSignalHasEventHandlerLeakWarning && getMaxListeners(signal) === defaultMaxListeners) {
-              setMaxListeners(1500, signal);
-            }
-            const removeAbortListener = util.addAbortListener(signal, abort);
-            requestFinalizer.register(ac, { signal, abort }, abort);
-            this.#abortCleanup = () => {
-              requestFinalizer.unregister(abort);
-              removeAbortListener();
-              this.#abortCleanup = null;
-            };
-          }
-        }
+        this.#signal = createDependentSignal(signal);
         this.#headers = new Headers(kConstruct);
         setHeadersList(this.#headers, request.headersList);
         setHeadersGuard(this.#headers, "request");
@@ -13773,23 +13812,8 @@ var require_request2 = __commonJS({
           throw new TypeError("unusable");
         }
         const clonedRequest = cloneRequest(this.#state);
-        const ac = new AbortController();
-        if (this.signal.aborted) {
-          ac.abort(this.signal.reason);
-        } else {
-          let list = dependentControllerMap.get(this.signal);
-          if (list === void 0) {
-            list = /* @__PURE__ */ new Set();
-            dependentControllerMap.set(this.signal, list);
-          }
-          const acRef = new WeakRef(ac);
-          list.add(acRef);
-          util.addAbortListener(
-            ac.signal,
-            buildAbort(acRef)
-          );
-        }
-        return fromInnerRequest(clonedRequest, this.#dispatcher, ac.signal, getHeadersGuard(this.#headers));
+        const signal = createDependentSignal(this.#signal);
+        return fromInnerRequest(clonedRequest, this.#dispatcher, signal, getHeadersGuard(this.#headers));
       }
       [nodeUtil.inspect.custom](depth, options) {
         if (options.depth === null) {
@@ -13835,7 +13859,12 @@ var require_request2 = __commonJS({
           request.#state = newState;
         }, "setRequestState");
         removeRequestAbortListener = /* @__PURE__ */ __name((request) => {
-          request.#abortCleanup?.();
+          const state = requestSignals.get(request.#signal);
+          if (state !== void 0) {
+            requestSignals.delete(request.#signal);
+            requestFinalizer.unregister(state.reference);
+            removeSignalFollower(state.source, state.reference);
+          }
         }, "removeRequestAbortListener");
         webidl.is.Request = (arg) => {
           return arg != null && typeof arg === "object" && #state in arg;
@@ -14121,7 +14150,7 @@ var require_subresource_integrity = __commonJS({
     __name(getStrongestMetadata, "getStrongestMetadata");
     function parseMetadata(metadata) {
       const result = [];
-      for (const item of metadata.split(" ")) {
+      for (const item of metadata.split(/[\t\n\f\r ]+/)) {
         const expressionAndOptions = item.split("?", 1);
         const algorithmExpression = expressionAndOptions[0];
         let base64Value = "";
@@ -14379,11 +14408,8 @@ var require_fetch = __commonJS({
           cleanupAbortListeners();
         }, "processResponseEndOfBody"),
         processResponse,
-        dispatcher: getRequestDispatcher(requestObject),
+        dispatcher: getRequestDispatcher(requestObject)
         // undici
-        // Keep requestObject alive to prevent its AbortController from being GC'd
-        // See https://github.com/nodejs/undici/issues/4627
-        requestObject
       });
       return p.promise;
     }
@@ -14454,10 +14480,8 @@ var require_fetch = __commonJS({
       processResponseEndOfBody,
       processResponseConsumeBody,
       useParallelQueue = false,
-      dispatcher = getGlobalDispatcher2(),
+      dispatcher = getGlobalDispatcher2()
       // undici
-      requestObject = null
-      // Keep alive to prevent AbortController GC, see #4627
     }) {
       assert(dispatcher);
       let taskDestination = null;
@@ -14480,9 +14504,7 @@ var require_fetch = __commonJS({
         processResponseConsumeBody,
         processResponseEndOfBody,
         taskDestination,
-        crossOriginIsolatedCapability,
-        // Keep requestObject alive to prevent its AbortController from being GC'd
-        requestObject
+        crossOriginIsolatedCapability
       };
       assert(!request.body || request.body.stream);
       if (request.window === "client") {
diff --git a/src/undici_version.h b/src/undici_version.h
index f16582bcb61..e0a4578df6c 100644
--- a/src/undici_version.h
+++ b/src/undici_version.h
@@ -2,5 +2,5 @@
 // Refer to tools/dep_updaters/update-undici.sh
 #ifndef SRC_UNDICI_VERSION_H_
 #define SRC_UNDICI_VERSION_H_
-#define UNDICI_VERSION "8.12.3"
+#define UNDICI_VERSION "8.12.4"
 #endif  // SRC_UNDICI_VERSION_H_