Skip to content

chore(deps): update undici to v8 - #3438

Draft
VaguelySerious wants to merge 4 commits into
mainfrom
peter/undici-v8
Draft

chore(deps): update undici to v8#3438
VaguelySerious wants to merge 4 commits into
mainfrom
peter/undici-v8

Conversation

@VaguelySerious

@VaguelySerious VaguelySerious commented Aug 10, 2026

Copy link
Copy Markdown
Member

Bumps the workspace catalog from undici@7.29.0 to undici@8.10.0 and adapts the two packages that depend on it, @workflow/world-vercel and @workflow/world-local.

undici 8 is a breaking release for anyone who constructs a dispatcher and hands it to the global fetch, which is exactly what both packages do. Most of the diff is that adaptation. A secondary chunk removes workarounds that v8 made unnecessary, and one change gives up a property v8 removed.

Relevant changes between undici 7 and 8

1. Node.js floor moved to 22.19

undici@8 declares engines: {"node": ">=22.19.0"}; undici@7.29.0 declared >=20.18.1.

Nothing under packages/ declares engines, so this PR does not change what npm/pnpm will let a consumer install. It does mean @workflow/world-vercel and @workflow/world-local no longer support Node 18 or 20 in practice. The root package.json still says ^18.0.0 || ^20.0.0 || ^22.0.0 || ^24.0.0. Whether to narrow that, and whether to start declaring engines on the published packages, is a policy call I left for review rather than making here. CI runs node-version: 22.x, which resolves above the floor.

2. The dispatcher handler ABI is v2 only (#4786)

v8 removed the legacy handler wrappers. A dispatcher is now driven with the v2 callbacks:

v1 v2
onConnect onRequestStart
onHeaders onResponseStart
onData onResponseData
onComplete onResponseEnd
onError onResponseError
onUpgrade onRequestUpgrade

Pause, resume and abort moved onto a DispatchController handed to the callbacks.

This matters because every request in world-vercel goes through the global fetch (makeRequest in http-core.ts, and fetchV4 in events-v4.ts, which does so deliberately to stay visible in Vercel's outgoing-requests view). That fetch belongs to the undici bundled into Node, not to the undici in our dependencies, and it drives a custom dispatcher with a v1 handler. Two failure modes, both observed:

  • a bare Agent: TypeError: fetch failed, cause InvalidArgumentError: invalid onRequestStart method;
  • a RetryAgent or a composed dispatcher: no callback the v1 handler implements is ever called, so fetch() never settles. The request hangs until the caller's own timeout.

The fix is a v1-to-v2 bridge applied as the outermost layer of every dispatcher this package hands to fetch (forGlobalFetch). It is applied once at construction, not per request, because DispatcherRecycler.note() identifies the dispatcher a request used by reference.

3. undici's own bridge drops every response header over HTTP/2

undici ships Dispatcher1Wrapper for exactly this, and it cannot be used on an H2 path. It forwards controller.rawHeaders to the v1 onHeaders. lib/core/request.js sets that field unconditionally, but the two protocols fill it with different shapes: the H1 parser supplies the raw Buffer[] name/value pair list a v1 handler expects, while client-h2.js supplies a parsed { name: value } object. That object has no .length, so the pair-wise loop in fetch's v1 handler reads zero headers. Status and body arrive intact, and Headers is empty.

undici masks this by forcing allowH2: false into every dispatch:

// lib/dispatcher/dispatcher1-wrapper.js
dispatch (opts, handler) {
  // Legacy (v1) consumers do not support HTTP/2, so force HTTP/1.1.
  if (opts.allowH2 !== false) {
    opts = { ...opts, allowH2: false }

The stated reason is that a v1 handler cannot carry a WebSocket upgrade over H2 (nodejs/undici#4989). Agent honours the flag per request by selecting a different pool:

// lib/dispatcher/agent.js
const allowH2 = opts.allowH2 ?? this[kOptions].allowH2
const key = allowH2 === false ? `${origin}#http1-only` : origin

So using undici's wrapper silently downgrades the events agent to HTTP/1.1: every option would still say H2 while ALPN negotiated http/1.1, undoing the multiplexing and flow-control work those agents exist for. It also breaks MockAgent, which registers its interceptors on the un-suffixed key, so the injected flag sends mocked requests to a pool holding no interceptors and they escape to the network.

An earlier revision of this PR used the wrapper and composed an interceptor under it to delete the injected flag. That produced the header bug above in production: every E2E Vercel Prod lane failed with v4 listEvents: expected application/vnd.workflow.v4-frames, got (none), because fetchV4 checks the response content-type and there were no response headers at all.

This PR therefore carries its own bridge (V1BridgeDispatcher in http-client.ts), which normalizes the header shape to the Buffer[] pair list a v1 handler parses, and leaves allowH2 untouched. That is safe for these dispatchers specifically: they carry plain request/response traffic only, and the WebSocket transport (events-v4-ws.ts) does not go through them. http-client.test.ts now asserts that response headers survive over h2, including a repeated header; the h2 harness previously replied with no headers at all, which is why nothing caught this.

4. Caller-supplied dispatchers now need bridging too, across three undici majors

APIConfig.dispatcher is documented as accepting a dispatcher from any undici version, and the four accessors used to return it untouched. A caller who installs undici today gets 8, and that dispatcher hangs under global fetch for the reason in (2). bridgeCallerDispatcher applies the same bridge, memoized in a WeakMap so the recycler's and the retry bookkeeping's by-reference comparisons still hold.

undici 6 complicates this: it speaks the v1 ABI only, validates the handler on dispatch, and rejects a pure v2 one with invalid onError method. Since a v6 dispatcher works fine unbridged today, bridging it naively would be a regression. The bridge therefore emits a handler carrying both ABIs. Measured against undici 6.28, 7.29 and 8.10 with a handler exposing both: 6 drives only the v1 callbacks, 7 and 8 drive only the v2 ones, and none of them drives both. All three complete a real h2 request and a MockAgent request through the bridge.

5. HTTP/2 is on by default (#4828)

v8 negotiates H2 whenever the origin offers it over ALPN. Any path that must stay on HTTP/1.1 has to say so. DEFAULT_AGENT_OPTIONS already did; world-local's queue agent now sets allowH2: false explicitly rather than inheriting the old default.

6. The H2 in-flight ceiling is protocol-aware, so pipelining is no longer a multiplexing knob (#5362, fixing #4143)

In v7, pipelining had to be set for H2 to multiplex at all: undici gated in-flight requests on getPipelining(client), and the Client constructor coerced pipelining to a number, so client-h2.js's defaultPipelining: Infinity was unreachable. Our events agent carried pipelining: 100 purely for that.

v8 picks the ceiling by protocol: on H2 it is the peer's SETTINGS_MAX_CONCURRENT_STREAMS, and pipelining only applies before a protocol is negotiated. pipelining is now removed from EVENTS_AGENT_OPTIONS. Measured against a loopback H2 origin, 16 concurrent requests run as 16 parallel streams on one connection with no pipelining set. Leaving it in would have raised H1 pipelining depth on an H2 fallback, which is what DEFAULT_AGENT_OPTIONS deliberately avoids.

7. busy() no longer serializes non-idempotent or stream-bodied requests (#5391, #5538)

v7's H2 busy() returned true for request.idempotent === false and for a stream/async-iterable body. v8 dispatches both concurrently, on the reasoning that H2 streams are independent.

This cuts both ways:

  • Events path (win). h2MultiplexInterceptor existed largely to relabel POSTs idempotent: true so they would not serialize. That relabelling is gone. What remains is the body re-buffering, so the interceptor is renamed bufferStreamedBodyInterceptor. That part is still needed for a different reason: fetch converts every body into an async iterable, an async iterable can be consumed once, and on a RetryAgent re-dispatch the exhausted body aborts the request with NGHTTP2_PROTOCOL_ERROR instead of resending.
  • Stream write/close agents (loss). Their append-isolation property depended on that busy() gate. Stream appends are not idempotent, and STREAM_RETRY_OPTIONS retries PUT on transient errorCodes, so multiplexing N appends onto one connection would let a single GOAWAY or socket reset fail all N and resend chunks the server may already have applied. Measured on v8: 16 concurrent appends went from 1 to 16 in flight on one connection. There is no client-side option that caps it back (see 8), so STREAM_AGENT_OPTIONS now sets allowH2: false. Those requests send a fully buffered body or none, so H2 was safe for them, but with multiplexing suppressed it was doing nothing the H1 agent does not.

8. h2 options were namespaced, and the replacement has two bugs (#5498, 8.10.0)

The flat maxConcurrentStreams, initialWindowSize and connectionWindowSize are marked @deprecated in favour of h2Options.*. The events agent keeps the flat spelling, because the documented replacement does not work:

  • h2Options.maxConcurrentStreams throws InvalidArgumentError. While validating maxConcurrentStreams, undici validates h2Options.connectionWindowSize (lib/dispatcher/client.js), so the option is unusable on its own.
  • h2Options.settings.initialWindowSize is validated and typed, and then ignored: undici reads h2Options.initialWindowSize. Measured against a loopback H2 origin, the flat option puts 4 MiB on the wire, while the documented spelling leaves the server seeing undici's 256 KiB default.

Separately confirmed (unchanged from v7, worth stating because it is easy to reach for): the maxConcurrentStreams option only seeds peerMaxConcurrentStreams at connect time and is overwritten by the server's SETTINGS, so it cannot be used to cap in-flight streams. That is why (7) is solved with allowH2: false rather than a stream cap.

9. MockAgent changes the shape of opts.body seen by reply callbacks

Test-only, but it accounts for a chunk of the diff. When the handler exposes body-sent hooks, and Dispatcher1Wrapper's LegacyHandlerWrapper always defines them, v8's mockDispatch defers the reply callback, drains the outgoing body, and substitutes a replayable async iterable. v7 passed whatever fetch dispatched (a Uint8Array) straight through. Reply callbacks that inspect the request body therefore have to drain it, which makes them async. v8 also accepts promise-returning reply callbacks, which is what makes that possible.

10. Unchanged, checked because a break here would be silent

  • Dispatcher.compose() still returns a Proxy that loses this. close() on a composed RetryAgent throws TypeError: Cannot read private member #agent. The existing withBoundLifecycle workaround stays.
  • undici still lazily calls require('node:http2') (lib/dispatcher/client-h2.js). The createRequire banner shims in packages/nitro, packages/sveltekit and packages/web are still load-bearing and are unchanged.

Testing

  • @workflow/world-vercel: 479 tests across 23 files pass.
  • @workflow/world-local: 524 tests across 14 files pass.
  • Typecheck and lint clean for both.

New coverage in http-client.test.ts:

  • Every accessor bridges a caller-supplied dispatcher instead of returning it by identity, forwards to the caller's own dispatch, passes the dispatch options through untouched, and is stable per input.
  • Response headers survive over h2, including a repeated header. This is the regression from (3), and it fails with an empty Headers if the bridge forwards the H2 header object through unnormalized.
  • The bridged handler still answers the v1 ABI, driven the way undici 6 drives it.
  • Headers and trailers stay distinct in both dispatcher shapes: the H1 shape, where both raw lists are on the controller, and the H2 shape, where neither is and both come from the parsed callback argument.
  • A caller-supplied undici@8 Agent completes a real request through global fetch, still negotiates h2, and sees its response headers. Unbridged, that request fails with invalid onRequestStart method.
  • The multiplexing harness now serves HTTP/1.1 and h2 from one origin, so the events agent (h2, expected to multiplex) and the stream agent (allowH2: false, expected not to) are measured against the same concurrency barrier.

Measurements referenced above were taken with throwaway probe scripts against a loopback node:http2 origin, not checked in.

Risk

The behavior change most likely to be felt in production is (7): stream write and close now speak HTTP/1.1 rather than h2-without-multiplexing. Wire-level shape per request is the same, one request per connection either way.

The bridge in (2), (3) and (4) sits on every outgoing request in world-vercel, so it is the highest-blast-radius part of the diff. It is also the part that already failed once in production shape, on the header path in (3), which the E2E Vercel Prod lanes caught and unit tests did not.

Bumps the workspace catalog from undici 7.29.0 to 8.10.0 and adapts
world-vercel and world-local to the v8 dispatcher API.

- Bridge every dispatcher handed to the global `fetch` through
  `Dispatcher1Wrapper`, including a caller-supplied `APIConfig.dispatcher`,
  and strip the `allowH2: false` that bridge injects.
- Drop the pipelining and idempotent-relabelling workarounds that used to be
  needed for H2 multiplexing; v8 multiplexes on its own.
- Turn H2 off for the stream write/close agents, which relied on v7's
  non-idempotent `busy()` gate for append isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c32d012

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
Name Type
@workflow/world-vercel Minor
@workflow/world-local Minor
@workflow/cli Patch
@workflow/core Patch
@workflow/web Patch
@workflow/vitest Patch
@workflow/world-postgres Patch
workflow Patch
@workflow/world-testing Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/web-shared Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview Aug 11, 2026 1:28am
example-nextjs-workflow-webpack Ready Ready Preview Aug 11, 2026 1:28am
example-workflow Ready Ready Preview Aug 11, 2026 1:28am
workbench-astro-workflow Ready Ready Preview Aug 11, 2026 1:28am
workbench-express-workflow Ready Ready Preview Aug 11, 2026 1:28am
workbench-fastify-workflow Ready Ready Preview Aug 11, 2026 1:28am
workbench-hono-workflow Ready Ready Preview Aug 11, 2026 1:28am
workbench-nestjs-workflow Ready Ready Preview Aug 11, 2026 1:28am
workbench-nitro-workflow Ready Ready Preview Aug 11, 2026 1:28am
workbench-nuxt-workflow Ready Ready Preview Aug 11, 2026 1:28am
workbench-python-workflow Error Error Aug 11, 2026 1:28am
workbench-sveltekit-workflow Ready Ready Preview Aug 11, 2026 1:28am
workbench-tanstack-start-workflow Ready Ready Preview Aug 11, 2026 1:28am
workbench-vite-workflow Ready Ready Preview Aug 11, 2026 1:28am
workflow-docs Ready Ready Preview, v0 Aug 11, 2026 1:28am
workflow-swc-playground Ready Ready Preview Aug 11, 2026 1:28am
workflow-tarballs Ready Ready Preview Aug 11, 2026 1:28am
workflow-web Building Building Preview Aug 11, 2026 1:28am

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit c32d012 · Tue, 11 Aug 2026 01:42:25 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 395 (-63%) 💚 1351 🔴 (+15%) 1371 🔴 (+14%) 1444 🔴 (-16%) 💚 30
TTFS stream 1275 (+22%) 🔻 1354 🔴 (+18%) 🔻 1370 🔴 (+17%) 🔻 1448 🔴 (+20%) 🔻 30
TTFS hook + stream 1544 (+24%) 🔻 1625 🔴 (+19%) 🔻 1665 🔴 (+19%) 🔻 1683 🔴 (+12%) 30
STSO 1020 steps (inline) 90 (-6.3%) 125 (-24%) 💚 142 (-30%) 💚 207 (-73%) 💚 1019
WO 1020 steps 124650 (-27%) 💚 124650 (-27%) 💚 124650 (-27%) 💚 124650 (-27%) 💚 1
SL stream latency 85 (-6.6%) 117 🔴 (-24%) 💚 125 🔴 (-27%) 💚 163 🔴 (-71%) 💚 30
SO stream overhead (text) 108 (-18%) 💚 155 (-44%) 💚 172 (-48%) 💚 212 (-73%) 💚 30
SO stream overhead (structured) 107 (-12%) 151 (-38%) 💚 158 (-49%) 💚 177 (-61%) 💚 30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 170410ms → this run 123292ms (Δ -47118ms, -28%)

   50-100 ms  ┃                         main   1  this  16   +15
  100-150 ms  ████████████████░░░░░░░┃  main 627  this 927  +300
  150-200 ms  █┃█████                   main 279  this  62  -217
  200-250 ms  ┃█                        main  59  this  10   -49
  250-300 ms  ┃                         main  17  this   4   -13
  300-350 ms  ┃                         main   8  this   0    -8
  350-400 ms  ┃                         main   3  this   0    -3
  400-450 ms  ┃                         main   2  this   0    -2
  450-500 ms  ┃                         main   1  this   0    -1
  500-550 ms  ┃                         main   2  this   0    -2
  600-650 ms  ┃                         main   2  this   0    -2
  650-700 ms  ┃                         main   4  this   0    -4
  700-750 ms  ┃                         main   3  this   0    -3
  750-800 ms  ┃                         main   2  this   0    -2
  800-850 ms  ┃                         main   3  this   0    -3
  850-900 ms  ┃                         main   1  this   0    -1
 950-1000 ms  ┃                         main   1  this   0    -1
1000-1050 ms  ┃                         main   1  this   0    -1
1250-1300 ms  ┃                         main   1  this   0    -1
1300-1350 ms  ┃                         main   1  this   0    -1
1650-1700 ms  ┃                         main   1  this   0    -1
📜 Previous results (1)

4889e13

Tue, 11 Aug 2026 00:52:18 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1296 (+54%) 🔻 1385 🔴 (+25%) 🔻 1430 🔴 (+25%) 🔻 1545 🔴 (+31%) 🔻 30
TTFS stream 1265 (+511%) 🔻 1329 🔴 (+21%) 🔻 1349 🔴 (+22%) 🔻 1465 🔴 (+27%) 🔻 30
TTFS hook + stream 1535 (+23%) 🔻 1632 🔴 (+18%) 🔻 1696 🔴 (+19%) 🔻 1722 🔴 (+12%) 30
STSO 1020 steps (inline) 84 (-17%) 💚 133 (-10%) 150 (-12%) 241 (-20%) 💚 1019
WO 1020 steps 131767 (-7.1%) 131767 (-7.1%) 131767 (-7.1%) 131767 (-7.1%) 1
SL stream latency 84 (-3.4%) 105 🔴 (-7.1%) 140 🔴 (+12%) 663 🔴 (+367%) 🔻 30
SO stream overhead (text) 99 (-10%) 159 (-15%) 💚 186 (-12%) 208 (-13%) 30
SO stream overhead (structured) 116 (+4.5%) 152 (-6.2%) 195 (+5.4%) 327 (+33%) 🔻 30
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the Best column shows the fastest (warm-start) sample for comparison.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
Passed Failed Skipped Total
✅ ▲ Vercel Production 3466 0 590 4056
✅ 💻 Local Development 3810 0 558 4368
✅ 📦 Local Production 3810 0 558 4368
✅ 🐘 Local Postgres 3810 0 558 4368
✅ 🪟 Windows 312 0 0 312
✅ vercel-multi-region 27 0 0 27
Total 15235 0 2264 17499
Details by Category

✅ ▲ Vercel Production

App Passed Failed Skipped
✅ astro-node 128 0 28
✅ astro-quickjs 128 0 28
✅ example-node 128 0 28
✅ example-quickjs 128 0 28
✅ express-node 128 0 28
✅ express-quickjs 128 0 28
✅ fastify-node 128 0 28
✅ fastify-quickjs 128 0 28
✅ hono-node 128 0 28
✅ hono-quickjs 128 0 28
✅ nest-node 128 0 28
✅ nest-quickjs 128 0 28
✅ nextjs-turbopack-node 153 0 3
✅ nextjs-turbopack-quickjs 153 0 3
✅ nextjs-webpack-node 153 0 3
✅ nextjs-webpack-quickjs 153 0 3
✅ nitro-node 128 0 28
✅ nitro-quickjs 128 0 28
✅ nuxt-node 128 0 28
✅ nuxt-quickjs 128 0 28
✅ sveltekit-node 147 0 9
✅ sveltekit-quickjs 147 0 9
✅ tanstack-start-node 128 0 28
✅ tanstack-start-quickjs 128 0 28
✅ vite-node 128 0 28
✅ vite-quickjs 128 0 28

✅ 💻 Local Development

App Passed Failed Skipped
✅ astro-stable-node 130 0 26
✅ astro-stable-quickjs 130 0 26
✅ express-stable-node 130 0 26
✅ express-stable-quickjs 130 0 26
✅ fastify-stable-node 130 0 26
✅ fastify-stable-quickjs 130 0 26
✅ hono-stable-node 130 0 26
✅ hono-stable-quickjs 130 0 26
✅ nest-stable-node 130 0 26
✅ nest-stable-quickjs 130 0 26
✅ nextjs-turbopack-canary-node 137 0 19
✅ nextjs-turbopack-canary-quickjs 137 0 19
✅ nextjs-turbopack-stable-node 156 0 0
✅ nextjs-turbopack-stable-quickjs 156 0 0
✅ nextjs-webpack-canary-node 137 0 19
✅ nextjs-webpack-canary-quickjs 137 0 19
✅ nextjs-webpack-stable-node 156 0 0
✅ nextjs-webpack-stable-quickjs 156 0 0
✅ nitro-stable-node 130 0 26
✅ nitro-stable-quickjs 130 0 26
✅ nuxt-stable-node 130 0 26
✅ nuxt-stable-quickjs 130 0 26
✅ sveltekit-stable-node 149 0 7
✅ sveltekit-stable-quickjs 149 0 7
✅ tanstack-start-node 130 0 26
✅ tanstack-start-quickjs 130 0 26
✅ vite-stable-node 130 0 26
✅ vite-stable-quickjs 130 0 26

✅ 📦 Local Production

App Passed Failed Skipped
✅ astro-stable-node 130 0 26
✅ astro-stable-quickjs 130 0 26
✅ express-stable-node 130 0 26
✅ express-stable-quickjs 130 0 26
✅ fastify-stable-node 130 0 26
✅ fastify-stable-quickjs 130 0 26
✅ hono-stable-node 130 0 26
✅ hono-stable-quickjs 130 0 26
✅ nest-stable-node 130 0 26
✅ nest-stable-quickjs 130 0 26
✅ nextjs-turbopack-canary-node 137 0 19
✅ nextjs-turbopack-canary-quickjs 137 0 19
✅ nextjs-turbopack-stable-node 156 0 0
✅ nextjs-turbopack-stable-quickjs 156 0 0
✅ nextjs-webpack-canary-node 137 0 19
✅ nextjs-webpack-canary-quickjs 137 0 19
✅ nextjs-webpack-stable-node 156 0 0
✅ nextjs-webpack-stable-quickjs 156 0 0
✅ nitro-stable-node 130 0 26
✅ nitro-stable-quickjs 130 0 26
✅ nuxt-stable-node 130 0 26
✅ nuxt-stable-quickjs 130 0 26
✅ sveltekit-stable-node 149 0 7
✅ sveltekit-stable-quickjs 149 0 7
✅ tanstack-start-node 130 0 26
✅ tanstack-start-quickjs 130 0 26
✅ vite-stable-node 130 0 26
✅ vite-stable-quickjs 130 0 26

✅ 🐘 Local Postgres

App Passed Failed Skipped
✅ astro-stable-node 130 0 26
✅ astro-stable-quickjs 130 0 26
✅ express-stable-node 130 0 26
✅ express-stable-quickjs 130 0 26
✅ fastify-stable-node 130 0 26
✅ fastify-stable-quickjs 130 0 26
✅ hono-stable-node 130 0 26
✅ hono-stable-quickjs 130 0 26
✅ nest-stable-node 130 0 26
✅ nest-stable-quickjs 130 0 26
✅ nextjs-turbopack-canary-node 137 0 19
✅ nextjs-turbopack-canary-quickjs 137 0 19
✅ nextjs-turbopack-stable-node 156 0 0
✅ nextjs-turbopack-stable-quickjs 156 0 0
✅ nextjs-webpack-canary-node 137 0 19
✅ nextjs-webpack-canary-quickjs 137 0 19
✅ nextjs-webpack-stable-node 156 0 0
✅ nextjs-webpack-stable-quickjs 156 0 0
✅ nitro-stable-node 130 0 26
✅ nitro-stable-quickjs 130 0 26
✅ nuxt-stable-node 130 0 26
✅ nuxt-stable-quickjs 130 0 26
✅ sveltekit-stable-node 149 0 7
✅ sveltekit-stable-quickjs 149 0 7
✅ tanstack-start-node 130 0 26
✅ tanstack-start-quickjs 130 0 26
✅ vite-stable-node 130 0 26
✅ vite-stable-quickjs 130 0 26

✅ 🪟 Windows

App Passed Failed Skipped
✅ nextjs-turbopack-node 156 0 0
✅ nextjs-turbopack-quickjs 156 0 0

✅ vercel-multi-region

App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

📋 View full workflow run

@socket-security

socket-security Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​undici@​8.10.09310010098100

View full report

undici's own v1/v2 bridge drops every response header over HTTP/2:
`controller.rawHeaders` is a Buffer[] pair list on H1 but a plain
{ name: value } object on H2, and the v1 handler's pair-wise loop reads
nothing out of the object. undici hides this by forcing allowH2: false on
every dispatch; restoring H2 under it exposed it, and the v4 events path
rejected every reply for a missing content-type.

The bridge here normalizes the header shape instead, so allowH2 is left
alone and the events agent keeps H2. It also keeps the v1 callbacks on the
handler it emits, because undici 6 validates and drives the v1 ABI only
and APIConfig.dispatcher accepts any undici version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two comments still credited Dispatcher1Wrapper for work this package now
does itself, and the world-local queue said nothing about why undici's
wrapper is fine there (that agent is HTTP/1.1 only, so the header shape
the wrapper mishandles never arises).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant