Skip to content

[7.x] fix: support Node 26 and legacy global dispatcher#5319

Open
mcollina wants to merge 8 commits into
v7.xfrom
feat/node-26-global-dispatcher-compat
Open

[7.x] fix: support Node 26 and legacy global dispatcher#5319
mcollina wants to merge 8 commits into
v7.xfrom
feat/node-26-global-dispatcher-compat

Conversation

@mcollina
Copy link
Copy Markdown
Member

@mcollina mcollina commented May 23, 2026

Summary

Backport Node 26 coverage to the v7.x branch and keep setGlobalDispatcher() compatible with Node.js' built-in fetch().

The package now stores its dispatcher under Symbol.for('undici.globalDispatcher.2'), while Node's bundled fetch() still reads Symbol.for('undici.globalDispatcher.1'). setGlobalDispatcher() now writes the same dispatcher to both symbols so package APIs and Node's built-in fetch() observe the same global dispatcher.

Changes

  • add Node 26 to the CI matrix for the regular and WASM-SIMD-disabled test jobs
  • run the shared-builtin-undici workflow against Node 24 and Node 26
  • build Node's FFI tests in the shared-builtin-undici workflow when the checked-out Node tree exposes build-ffi-tests
  • update lib/global.js to use the v2 dispatcher symbol while mirroring the configured dispatcher to the legacy v1 symbol
  • add node-test coverage proving:
    • setGlobalDispatcher(new Agent()) does not break Node.js global fetch()
    • Node.js global fetch() uses the dispatcher mirrored under the v1 symbol
    • the same dispatcher is present under both v1 and v2 symbols
  • align the client-errors.js reconnect tests with main so the stream/async-iterator POST error cases remain stable on Node 26

Testing

  • node --test --test-reporter=spec --test-timeout=180000 test/node-test/client-errors.js
  • node --test --test-reporter=spec --test-timeout=180000 test/content-length.js
  • npx borp --timeout 180000 -p "test/client.js"
  • npm run test:node-test
  • npm run lint via the pre-commit hook

Signed-off-by: Matteo Collina <hello@matteocollina.com>
@mcollina mcollina marked this pull request as draft May 23, 2026 08:00
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.89%. Comparing base (2b3cd22) to head (3d93b73).
⚠️ Report is 1 commits behind head on v7.x.

Additional details and impacted files
@@            Coverage Diff             @@
##             v7.x    #5319      +/-   ##
==========================================
- Coverage   92.89%   92.89%   -0.01%     
==========================================
  Files         112      112              
  Lines       35828    35837       +9     
==========================================
+ Hits        33283    33291       +8     
- Misses       2545     2546       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@aduh95 aduh95 changed the title fix: support Node 26 and legacy global dispatcher [7.x] fix: support Node 26 and legacy global dispatcher May 25, 2026
@mcollina mcollina marked this pull request as ready for review May 28, 2026 16:57
@mcollina
Copy link
Copy Markdown
Member Author

cc @domenic

@domenic
Copy link
Copy Markdown
Contributor

domenic commented May 29, 2026

Thanks. My plan is to move entirely off of Undici v7 for jsdom though (jsdom/jsdom#4170) so hopefully I just won't need to worry about this :)

Copy link
Copy Markdown
Member

@metcoder95 metcoder95 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, just tests are not happy

mcollina added a commit to platformatic/platformatic that referenced this pull request May 29, 2026
Node.js >= 26 bundles undici >= 8, whose built-in fetch() reads the global
dispatcher from Symbol.for('undici.globalDispatcher.2'). Our bundled undici
(v7) setGlobalDispatcher() only writes Symbol.for('undici.globalDispatcher.1'),
so the application's global fetch() bypassed the runtime mesh interceptor and
internal *.plt.local calls failed with ENOTFOUND (composer/gateway returned 500
in development mode for nest/vite/remix/tanstack/react-router).

Add mirrorGlobalDispatcherForBuiltinFetch() to @platformatic/foundation and
invoke it wherever the mesh dispatcher is installed so both undici versions
observe the same dispatcher. Can be dropped once undici aligns the symbols
across versions: nodejs/undici#5319

Assisted-by: Claude Code:claude-opus-4-8[1m]
Signed-off-by: Matteo Collina <hello@matteocollina.com>
mcollina added a commit to platformatic/platformatic that referenced this pull request May 29, 2026
* ci: test packages on Node.js 26

Assisted-by: OpenAI:gpt-5
Signed-off-by: Matteo Collina <hello@matteocollina.com>

* fix(sql-mapper): update sqlite-pool for Node.js 26

Assisted-by: OpenAI:gpt-5
Signed-off-by: Matteo Collina <hello@matteocollina.com>

* fix(runtime): mirror global dispatcher for Node 26 built-in fetch

Node.js >= 26 bundles undici >= 8, whose built-in fetch() reads the global
dispatcher from Symbol.for('undici.globalDispatcher.2'). Our bundled undici
(v7) setGlobalDispatcher() only writes Symbol.for('undici.globalDispatcher.1'),
so the application's global fetch() bypassed the runtime mesh interceptor and
internal *.plt.local calls failed with ENOTFOUND (composer/gateway returned 500
in development mode for nest/vite/remix/tanstack/react-router).

Add mirrorGlobalDispatcherForBuiltinFetch() to @platformatic/foundation and
invoke it wherever the mesh dispatcher is installed so both undici versions
observe the same dispatcher. Can be dropped once undici aligns the symbols
across versions: nodejs/undici#5319

Assisted-by: Claude Code:claude-opus-4-8[1m]
Signed-off-by: Matteo Collina <hello@matteocollina.com>

* test(wattpm): mirror MockAgent dispatcher for Node 26 built-in fetch

The update-dependencies test fixtures mock the npm registry via undici
MockAgent + setGlobalDispatcher, which writes Symbol.for('undici.globalDispatcher.1').
On Node.js >= 26 (bundled undici >= 8) the built-in fetch() used by the
`update` command reads Symbol.for('undici.globalDispatcher.2'), so the mock
was bypassed and the command hit the real registry, breaking the
wattpm-utils dependency update tests.

Mirror the mock onto the symbol fetch() reads so the registry mock is
honored on Node 26.

Assisted-by: Claude Code:claude-opus-4-8[1m]
Signed-off-by: Matteo Collina <hello@matteocollina.com>

* fix(runtime): grant --allow-net under the permission model on Node 25+

Node.js 25 added --allow-net to the Permission Model, gating dns.lookup(),
server.listen(), outbound connections and fetch() behind it. When an
application enables fs permissions, its worker runs with --permission, so on
Node >= 25 it could no longer bind its HTTP server (getaddrinfo
ERR_ACCESS_DENIED localhost) nor reach other applications through the mesh.

Add a features.node.permission.network capability flag (detected via
process.allowedNodeEnvironmentFlags so the flag is never passed on older
versions where it does not exist) and append --allow-net to the worker
permission flags when supported.

Assisted-by: Claude Code:claude-opus-4-8[1m]
Signed-off-by: Matteo Collina <hello@matteocollina.com>

---------

Signed-off-by: Matteo Collina <hello@matteocollina.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.

5 participants