Skip to content

Resolve bare undici to the installed package, keep the shim as fallback - #36102

Open
robobun wants to merge 2 commits into
mainfrom
farm/6fc27d0f/undici-prefer-installed
Open

robobun wants to merge 2 commits into
mainfrom
farm/6fc27d0f/undici-prefer-installed

Conversation

@robobun

@robobun robobun commented Jul 27, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Bun resolves the bare undici specifier to its builtin shim even when the real package is installed. require.resolve("undici") returns the string undici. The shim's Dispatcher, Agent and RetryAgent are empty classes, so new RetryAgent(new Agent()).compose(...) throws agent.compose is not a function.
  • The alias table in src/resolve_builtins/HardcodedModule.rs maps undici to the shim, and the shim's registry key is also undici, the npm name.

Fixes #17799. Fixes #36098. Fixes #42084.

Fix

  • The shim's registry key is now internal:undici (one special case in src/codegen/bundle-modules.ts). The bare undici alias is removed, so undici is an ordinary package at every lookup site. next/dist/compiled/undici still maps to the shim.
  • Alias::fallback(name) names the builtin that stands in for a package. The resolver consults it after a node_modules probe with auto-install disabled. Only NotFound falls back. Bun never auto-installs undici.
  • A key that is not the npm name cannot re-resolve into node_modules. The module loader resolves a dynamic import() result a second time with no referrer, so import("next/dist/compiled/undici") now gives the shim like the static import does.
  • undici is no longer reported by isBuiltin, builtinModules or process.getBuiltinModule. bun build --target=bun bundles an installed undici and prints internal:undici when none is installed.
  • Self-reviewed, two rounds. Round one found the dynamic import blocker in the old flag-based shape. Round two asked that bundles keep the text undici. Not taken: the compile builtin set and the bake resolver would then need the fallback too, and a bundle that pins what it saw at build time is deterministic.
  • Verified: test/js/first_party/undici/undici-prefer-installed.test.ts (12 tests, 10 fail on main). Other suites in Notes.

Background

  • The hardcoded alias table (HardcodedModule::Alias) maps specifiers such as node:fs and ws to builtin module keys. The key is what the module loader fetches.
  • GlobalCache::disable tells the resolver to look in node_modules only. The other modes can read the global package cache or install from the registry.
  • The transpiler cache version is bumped, because cached output written by older versions has next/dist/compiled/undici already rewritten to undici.
Notes

no test proof · iteration 6 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/module/node-module-module.test.js, test/js/bun/util/fuzzy-wuzzy.test.ts, test/bundler/bundler_bytecode_portable.test.ts

@github-actions

Copy link
Copy Markdown
Contributor

Found 7 issues this PR may fix:

  1. Remove the undici polyfill #17799 - Requests removing the undici polyfill so the real npm package can be used; this PR makes installed undici take priority over the builtin shim
  2. SyntaxError: Export named 'cacheStores' not found in module 'undici'. #27783 - cacheStores export not found in builtin undici shim; installing real undici now resolves correctly
  3. Undici native module is outdated #19748 - Builtin undici module is outdated with no way to use the installed version; this PR enables exactly that
  4. @elastic/elasticsearch@8.11.0 not working due to missing undici.Pool.request #7920 - undici.Pool.request throws "Not implemented in bun"; with real undici preferred, Pool.request works
  5. Undici Client request is undefined #21944 - undici.Client.request is undefined in the builtin shim; real undici provides it
  6. undici.Agent is missing async close() method #14498 - undici.Agent missing close() method in builtin shim; real undici has it
  7. Support undici ProxyAgent #4474 - undici.ProxyAgent is non-functional in the builtin shim; real undici's ProxyAgent works

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #17799
Fixes #27783
Fixes #19748
Fixes #7920
Fixes #21944
Fixes #14498
Fixes #4474

🤖 Generated with Claude Code

@robobun

robobun commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Of the listed issues, this PR directly fixes #19748: installing undici is now sufficient to get the real package for the bare specifier.

The others (#27783, #7920, #21944, #14498, #4474) are reports about the builtin shim itself, which is unchanged here and still applies when undici is not installed. After this lands, bun add undici works around all of them, but the shim-side gaps remain, so I left them open. #17799 asks for removing the shim entirely, which this PR deliberately does not do (the shim stays as the fallback and for the next/dist/compiled/undici alias), so that one is a maintainer call.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 577539c1-1eaf-45a4-a66e-973009d2c777

📥 Commits

Reviewing files that changed from the base of the PR and between eb4a990 and 1aa4b64.

📒 Files selected for processing (1)
  • src/jsc/ModuleLoader.rs

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.


Walkthrough

Changes

Bare undici aliases can prefer an installed package and fall back to Bun’s shim. Runtime resolution, bundling, and transpilation preserve deferred specifiers. Tests cover installed, fallback, builtin classification, and compiled-shim resolution.

Undici resolution

Layer / File(s) Summary
Alias preference and deferred-resolution contract
src/resolve_builtins/HardcodedModule.rs
Adds prefer_installed, defaults existing aliases to false, enables it for undici, and adds deferred-resolution detection.
Installed-package probing and shim fallback
src/jsc/VirtualMachine.rs, src/runtime/jsc_hooks.rs, src/resolver/resolver.rs, src/jsc/ModuleLoader.rs, src/jsc/bindings/..., src/jsc/modules/..., src/jsc/RuntimeTranspilerCache.rs, src/js/internal/repl/...
Preferred aliases probe installed packages before falling back to hardcoded paths. undici is removed from builtin classification, and the transpiler cache version is incremented.
Deferred alias handling in compilation
src/bundler/..., src/jsc/RuntimeTranspilerStore.rs
Bundler, linker, and transpiler paths preserve user-written specifiers when resolution is deferred.
Undici behavior coverage
test/js/first_party/undici/*, test/js/bun/util/fuzzy-wuzzy.test.ts, test/js/node/module/node-module-module.test.js
Tests cover installed-package precedence, shim fallback, explicit compiled-shim aliases, and updated builtin counts.

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 1aa4b

Bare undici imports now prefer an installed package while retaining the builtin shim as fallback. No concrete merge-blocking risk is established for the current change.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy both linked issues [#36098] [#42084] by preferring an installed "undici" package and retaining the builtin shim as a fallback when no package is installed.
Out of Scope Changes check ✅ Passed The code, cache, builtin-reporting, resolver, transpiler, and test changes all support the stated "undici" resolution behavior and linked issue objectives. No unrelated code changes are evident.
Title check ✅ Passed The title clearly summarizes the main change: bare undici resolves to an installed package when available and keeps the shim as a fallback.
Description check ✅ Passed The description explains the problem, implementation, compatibility behavior, scope, and verification results. It does not use the exact template headings, but it provides the required information in …

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/jsc/VirtualMachine.rs`:
- Around line 4084-4103: The alias resolution probes in both runtime resolvers
incorrectly treat resolver failures as missing packages. In
src/jsc/VirtualMachine.rs lines 4084-4103 and src/runtime/jsc_hooks.rs lines
4977-4996, split the ResultUnion match so Failure(e) immediately returns
Err(e.into()), while only Pending and NotFound use the existing alias.path
fallback.

In `@test/js/first_party/undici/undici-prefer-installed.test.ts`:
- Around line 65-84: Add an ESM fallback test alongside the existing CJS test in
the “without an installed undici” case, using a main.mjs entry point and an
import-based probe. Assert that the builtin shim resolves and exposes the
expected request and MockAgent APIs, with the same empty-stderr and
successful-exit expectations as the existing CJS path.
- Around line 9-25: Add a test in the undici resolution suite covering bun build
with --target=bun, verifying that a user-written "undici" specifier remains
mapped to the installed package rather than the builtin shim. Keep the existing
runtime require.resolve and Next.js compatibility alias assertions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dfe03831-02a4-401c-aff5-1b71fa670d87

📥 Commits

Reviewing files that changed from the base of the PR and between 4eb6f99 and ee0716c.

📒 Files selected for processing (10)
  • src/bundler/bundle_v2.rs
  • src/bundler/linker.rs
  • src/jsc/RuntimeTranspilerStore.rs
  • src/jsc/VirtualMachine.rs
  • src/resolve_builtins/HardcodedModule.rs
  • src/runtime/jsc_hooks.rs
  • test/js/bun/util/fuzzy-wuzzy.test.ts
  • test/js/first_party/undici/undici-prefer-installed.test.ts
  • test/js/first_party/undici/undici-primordials.test.ts
  • test/js/first_party/undici/undici.test.ts

Comment thread src/jsc/VirtualMachine.rs Resolved
Comment thread src/bundler/bundle_v2.rs Resolved
Comment thread src/bundler/linker.rs Resolved
Comment thread src/jsc/RuntimeTranspilerStore.rs Resolved
Comment thread src/jsc/VirtualMachine.rs Resolved
Comment thread src/jsc/VirtualMachine.rs Resolved
Comment thread src/jsc/VirtualMachine.rs Resolved
Comment thread src/jsc/VirtualMachine.rs Resolved
Comment thread src/jsc/VirtualMachine.rs Resolved
Comment thread src/resolve_builtins/HardcodedModule.rs Resolved
Comment thread src/resolve_builtins/HardcodedModule.rs Resolved
Comment thread src/resolve_builtins/HardcodedModule.rs Resolved
Comment thread src/runtime/jsc_hooks.rs Resolved
Comment thread src/runtime/jsc_hooks.rs Resolved
Comment thread src/runtime/jsc_hooks.rs Resolved
Comment thread src/runtime/jsc_hooks.rs Resolved
Comment thread src/runtime/jsc_hooks.rs Resolved
Comment thread src/runtime/jsc_hooks.rs Resolved
@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. undici: remove polyfill, resolve to real npm package #30561 - Also changes bare undici resolution to prefer the real npm package over the builtin shim, modifying the same HardcodedModule.rs code path

🤖 Generated with Claude Code

@robobun

robobun commented Jul 27, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:59 PM PT - Sep 8th, 2026

✅ @robobun, your commit 7b56d60ba487286f1137242b83fc39cf286f6493 passed in Build #113463! 🎉


🧪   To try this PR locally:

bunx bun-pr 36102

That installs a local version of the PR into your bun-36102 executable, so you can run:

bun-36102 --bun

Comment thread src/resolve_builtins/HardcodedModule.rs Resolved
@robobun

robobun commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

Not a duplicate, but related: #30561 removes the shim entirely, so a missing install becomes "Cannot find module" and the next/dist/compiled/undici alias goes away. This PR is the non-breaking variant: the installed package wins when present, the shim remains the fallback otherwise, and the Next.js compat alias keeps pointing at the shim. If maintainers prefer full removal, #30561 supersedes this; otherwise this one keeps existing programs working.

Comment thread src/jsc/RuntimeTranspilerStore.rs Resolved
Comment thread src/resolve_builtins/HardcodedModule.rs Resolved
Comment thread src/resolve_builtins/HardcodedModule.rs Resolved
Comment thread src/runtime/jsc_hooks.rs Resolved
Comment thread src/jsc/ModuleLoader.rs Resolved
Comment thread src/jsc/RuntimeTranspilerCache.rs Resolved
Comment thread src/sys/lib.rs Resolved
Comment thread src/url/lib.rs Resolved
Comment thread src/url/lib.rs Resolved
Comment thread src/uws/lib.rs Resolved
Comment thread src/uws_sys/App.rs Resolved
Comment thread src/uws_sys/ListenSocket.rs Resolved
The shim's registry key is now internal:undici, so a resolved key never
re-resolves into node_modules. Bare undici is an ordinary package at every
lookup site. The resolver probes node_modules without auto-install and
returns the shim key when nothing is installed.
@robobun
robobun force-pushed the farm/6fc27d0f/undici-prefer-installed branch from 903e055 to 50ac9d4 Compare September 9, 2026 05:22
…des the installed undici

@remix-run/node in the corpus requires undici. bun build --target=bun now
bundles the installed package instead of leaving the shim external, so the
payload grows on every platform alike (nine CI lanes agree on these values).

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pull request has been reviewed before and this review found new issues. Where they share a root cause, one fix may close them together.

Comment thread src/http/HTTPContext.rs Resolved
Comment thread src/jsc/bindings/ImportMetaObject.h Resolved
Comment thread src/runtime/webcore/blob/copy_file.rs Resolved
Comment thread src/runtime/webcore/FileReader.rs Resolved
@robobun

robobun commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

The four review findings above (HTTPContext keep-alive guard, ImportMetaObject destroy, FileReader pause, Bun.write byte count) were real, but they describe an intermediate push (903e055) whose squash accidentally reverted merged main commits. That head was replaced. The current head 7b56d60 changes only the 14 undici-related files against main, so those four commits (#41987, #42023, #42038, #42066) are intact. I replied on each thread and resolved it.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code review found no issues

No high-confidence issues detected in this change.

@robobun

robobun commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Two housekeeping notes.

Added "Fixes #17799" to the body. The maintainer note on #36098 (#36098 (comment)) names this PR as the fix for that request and tracks it there. My earlier comment above left that as a maintainer call, and the call has since been made.

Compatibility data for review: #30561 (closed in favor of this PR) carried an integration suite for the real package, written by @galenzo17. I ran that suite against undici 6.21.3 loaded from node_modules under Bun 1.4.3. 32 of 33 tests pass: request(), Client, Pool, Agent, Dispatcher.compose() with the redirect and retry interceptors, MockAgent (get/intercept/reply, disableNetConnect, assertNoPendingInterceptors), errors, and the cookie and MIME helpers. The one failure is isBuiltin("undici") === true, which this PR changes to false.

That suite is not folded in here. compose() and the interceptors need undici 6.x, and test/package.json pins undici at 5.20.0. Moving the pin also moves the undici/index.js and libraries.js hashes in test/bundler/bundler_bytecode_portable.test.ts. If reviewers want the suite in this PR, it can come with that pin bump and a Co-authored-by credit to @galenzo17.

alii pushed a commit that referenced this pull request Sep 14, 2026
…t-in npm package replacements (#42718)

### Problem

- `docs/guides/http/hot.mdx:7` says "When any module or file changes,
Bun re-runs the file." `bun --hot` watches only the entry file and the
files it imports. An edit to `.env`, `bunfig.toml`, or `tsconfig.json`
triggers no reload.
- `docs/runtime/watch-mode.mdx:124` shows `Reloaded 1 times` first. The
sample logs before it increments, so the first line is `Reloaded 0
times`.
- No docs page says that Bun resolves `undici`, `node-fetch`, `ws`, and
other npm specifiers to built-in modules, even when the package is
installed.

### Fix

- `hot.mdx`: say what `--hot` watches, name three files it does not
watch, and say to restart Bun for those.
- `watch-mode.mdx`: the output sample starts at `Reloaded 0 times`.
- `module-resolution.mdx`: new section "Built-in replacements for npm
packages". It lists every third-party specifier in the alias table
(`src/resolve_builtins/HardcodedModule.rs:746-799`), what each loads,
how subpaths and `require.resolve()` behave, and what `bun build` does
per target.
- Verified: I ran every claim on a release build of main
(`1.4.3-canary.1+09bb54630`). Output is in Notes. `prettier --check`
passes.

### Background

- `--hot` adds a file to the watcher when the module loader loads it,
and skips `node_modules`. Bun reads `.env` and `bunfig.toml` at startup,
outside the module loader.
- The alias table maps a specifier to a built-in module before the
resolver looks at `node_modules`. It applies when the target is `bun`:
the runtime and `bun build --target=bun`.
- #36102 (open) makes bare `undici` prefer the installed package. If it
lands, the `undici` row changes with it.

<details><summary>Notes</summary>

No hunk overlaps #30912 (`watch-mode.mdx` lines 91-99 and 146-153) or
#38408 (`module-resolution.mdx` line 309 and below).

**`--hot` triggers.** `server.ts` is the sample from
`watch-mode.mdx:104-116`, with `.env`, `tsconfig.json`, and
`bunfig.toml` in the same directory. A driver script edits one file,
waits, and counts the `Reloaded` lines.

```
[after start] lines=1
[after .env edit] lines=1
[after tsconfig edit] lines=1
[after bunfig edit] lines=1
[after server.ts edit 1] lines=2
[after server.ts edit 2] lines=3
--- out.log
Reloaded 0 times
Reloaded 1 times
Reloaded 2 times
```

**Resolution.** `node_modules/{undici,node-fetch,ws,isomorphic-fetch}`
each hold a stub package that exports `{ fromNodeModules: true }`.

```
$ bun b.cjs
undici             resolve: undici             fromNodeModules: false
node-fetch         resolve: node-fetch         fromNodeModules: false
ws                 resolve: ws                 fromNodeModules: false
isomorphic-fetch   resolve: isomorphic-fetch   fromNodeModules: false
undici/package.json -> /tmp/dv/undici-check/node_modules/undici/package.json
$ node b.cjs
undici             resolve: /tmp/dv/undici-check/node_modules/undici/index.js fromNodeModules: true
...
```

`require.resolve()` for every specifier in the table:

```
@vercel/fetch                    -> @vercel/fetch
isomorphic-fetch                 -> isomorphic-fetch
node-fetch                       -> node-fetch
undici                           -> undici
utf-8-validate                   -> utf-8-validate
ws                               -> ws
ws/lib/websocket                 -> ws
abort-controller                 -> abort-controller
abort-controller/polyfill        -> abort-controller
next/dist/compiled/ws            -> ws
next/dist/compiled/node-fetch    -> node-fetch
next/dist/compiled/undici        -> undici
undici/package.json              -> /tmp/dv/undici-check/node_modules/undici/package.json
ws/package.json                  -> /tmp/dv/undici-check/node_modules/ws/package.json
```

`import.meta.resolve("undici")` and `import.meta.resolve("node-fetch")`
return `undici` and `node-fetch`.

**What the built-in modules do.** A local `Bun.serve` answers
`direct:<path>`.

```
undici.Request/Response/Headers/FormData/File/WebSocket === the globals: true (each)
undici.fetch === Bun.fetch: true
undici.fetch with dead ProxyAgent -> 200 direct:/x
global fetch after setGlobalDispatcher(dead proxy) -> direct:/x
Agent has dispatch/close/destroy/compose: undefined,undefined,undefined,undefined
undici.request -> 200 body is Readable: true direct:/x
undici.stream() / pipeline() / connect() / upgrade() -> This function is not yet implemented in Bun
node-fetch res instanceof Response: true | body is Readable: true | buffer(): function | headers.raw(): function
abort-controller default === AbortController: true, .AbortSignal === AbortSignal: true
utf-8-validate: function, returns true for Buffer.from("hi")
```

`ws`: `WebSocket` wraps the global `WebSocket`
(`src/js/thirdparty/ws.js:312`). `WebSocketServer` uses `node:http` and
`server.upgrade()` (`ws.js:1373`, `ws.js:1566`). `utf-8-validate`
exports `jsBufferConstructorFunction_isUtf8`
(`src/jsc/modules/UTF8ValidateModule.h:16`), the function behind
`isUtf8` in `node:buffer`.

**`bun build`.** The entry imports `fetch` from `undici` and the default
export of `node-fetch`, with the stub packages installed.

```
--target=bun      keeps  import { fetch as f1 } from "undici"; import f2 from "node-fetch";
--target=node     bundles node_modules/undici/index.js and node_modules/node-fetch/index.js
--target=browser  bundles node_modules/undici/index.js and node_modules/node-fetch/index.js
```

The `bun test` rewrite of `@jest/globals` and `vitest` to `bun:test`
uses the same table. It is already in
`docs/guides/test/migrate-from-jest.mdx:19` and is not repeated here.

</details>
usrbinkat pushed a commit to usrbinkat/bun that referenced this pull request Sep 15, 2026
…t-in npm package replacements (oven-sh#42718)

### Problem

- `docs/guides/http/hot.mdx:7` says "When any module or file changes,
Bun re-runs the file." `bun --hot` watches only the entry file and the
files it imports. An edit to `.env`, `bunfig.toml`, or `tsconfig.json`
triggers no reload.
- `docs/runtime/watch-mode.mdx:124` shows `Reloaded 1 times` first. The
sample logs before it increments, so the first line is `Reloaded 0
times`.
- No docs page says that Bun resolves `undici`, `node-fetch`, `ws`, and
other npm specifiers to built-in modules, even when the package is
installed.

### Fix

- `hot.mdx`: say what `--hot` watches, name three files it does not
watch, and say to restart Bun for those.
- `watch-mode.mdx`: the output sample starts at `Reloaded 0 times`.
- `module-resolution.mdx`: new section "Built-in replacements for npm
packages". It lists every third-party specifier in the alias table
(`src/resolve_builtins/HardcodedModule.rs:746-799`), what each loads,
how subpaths and `require.resolve()` behave, and what `bun build` does
per target.
- Verified: I ran every claim on a release build of main
(`1.4.3-canary.1+09bb54630`). Output is in Notes. `prettier --check`
passes.

### Background

- `--hot` adds a file to the watcher when the module loader loads it,
and skips `node_modules`. Bun reads `.env` and `bunfig.toml` at startup,
outside the module loader.
- The alias table maps a specifier to a built-in module before the
resolver looks at `node_modules`. It applies when the target is `bun`:
the runtime and `bun build --target=bun`.
- oven-sh#36102 (open) makes bare `undici` prefer the installed package. If it
lands, the `undici` row changes with it.

<details><summary>Notes</summary>

No hunk overlaps oven-sh#30912 (`watch-mode.mdx` lines 91-99 and 146-153) or
oven-sh#38408 (`module-resolution.mdx` line 309 and below).

**`--hot` triggers.** `server.ts` is the sample from
`watch-mode.mdx:104-116`, with `.env`, `tsconfig.json`, and
`bunfig.toml` in the same directory. A driver script edits one file,
waits, and counts the `Reloaded` lines.

```
[after start] lines=1
[after .env edit] lines=1
[after tsconfig edit] lines=1
[after bunfig edit] lines=1
[after server.ts edit 1] lines=2
[after server.ts edit 2] lines=3
--- out.log
Reloaded 0 times
Reloaded 1 times
Reloaded 2 times
```

**Resolution.** `node_modules/{undici,node-fetch,ws,isomorphic-fetch}`
each hold a stub package that exports `{ fromNodeModules: true }`.

```
$ bun b.cjs
undici             resolve: undici             fromNodeModules: false
node-fetch         resolve: node-fetch         fromNodeModules: false
ws                 resolve: ws                 fromNodeModules: false
isomorphic-fetch   resolve: isomorphic-fetch   fromNodeModules: false
undici/package.json -> /tmp/dv/undici-check/node_modules/undici/package.json
$ node b.cjs
undici             resolve: /tmp/dv/undici-check/node_modules/undici/index.js fromNodeModules: true
...
```

`require.resolve()` for every specifier in the table:

```
@vercel/fetch                    -> @vercel/fetch
isomorphic-fetch                 -> isomorphic-fetch
node-fetch                       -> node-fetch
undici                           -> undici
utf-8-validate                   -> utf-8-validate
ws                               -> ws
ws/lib/websocket                 -> ws
abort-controller                 -> abort-controller
abort-controller/polyfill        -> abort-controller
next/dist/compiled/ws            -> ws
next/dist/compiled/node-fetch    -> node-fetch
next/dist/compiled/undici        -> undici
undici/package.json              -> /tmp/dv/undici-check/node_modules/undici/package.json
ws/package.json                  -> /tmp/dv/undici-check/node_modules/ws/package.json
```

`import.meta.resolve("undici")` and `import.meta.resolve("node-fetch")`
return `undici` and `node-fetch`.

**What the built-in modules do.** A local `Bun.serve` answers
`direct:<path>`.

```
undici.Request/Response/Headers/FormData/File/WebSocket === the globals: true (each)
undici.fetch === Bun.fetch: true
undici.fetch with dead ProxyAgent -> 200 direct:/x
global fetch after setGlobalDispatcher(dead proxy) -> direct:/x
Agent has dispatch/close/destroy/compose: undefined,undefined,undefined,undefined
undici.request -> 200 body is Readable: true direct:/x
undici.stream() / pipeline() / connect() / upgrade() -> This function is not yet implemented in Bun
node-fetch res instanceof Response: true | body is Readable: true | buffer(): function | headers.raw(): function
abort-controller default === AbortController: true, .AbortSignal === AbortSignal: true
utf-8-validate: function, returns true for Buffer.from("hi")
```

`ws`: `WebSocket` wraps the global `WebSocket`
(`src/js/thirdparty/ws.js:312`). `WebSocketServer` uses `node:http` and
`server.upgrade()` (`ws.js:1373`, `ws.js:1566`). `utf-8-validate`
exports `jsBufferConstructorFunction_isUtf8`
(`src/jsc/modules/UTF8ValidateModule.h:16`), the function behind
`isUtf8` in `node:buffer`.

**`bun build`.** The entry imports `fetch` from `undici` and the default
export of `node-fetch`, with the stub packages installed.

```
--target=bun      keeps  import { fetch as f1 } from "undici"; import f2 from "node-fetch";
--target=node     bundles node_modules/undici/index.js and node_modules/node-fetch/index.js
--target=browser  bundles node_modules/undici/index.js and node_modules/node-fetch/index.js
```

The `bun test` rewrite of `@jest/globals` and `vitest` to `bun:test`
uses the same table. It is already in
`docs/guides/test/migrate-from-jest.mdx:19` and is not repeated here.

</details>
@iki

iki commented Oct 16, 2026

Copy link
Copy Markdown

Additional version coverage from a Windows x64 downstream audit: Bun 1.4.2 (744846f), installed undici 8.12.1, Wrangler 4.151.0 and direct Miniflare 5.20261001.0-alpha still hit this. createRequire(import.meta.url).resolve('undici') returns undici; the bare module's Pool.prototype.dispatch and close are undefined, while an explicit import of the installed node_modules/undici/index.js has both functions.

Our Wrangler subprocess temporarily copies the installed implementation into the builtin module object; Worker/HTTPS smoke tests pass with that override. We support preferring the installed package as proposed here, and will remove that half of our preload after a released Bun passes the same startup/request/reconfiguration/disposal checks. The separate global-fetch override needs independent validation even after module resolution is fixed; see #39250 and #39247. This is a reproduction on released Bun, not a claim that we tested this PR's Bun build.

roshne added a commit to Rackbops/rackbops-discord-bot that referenced this pull request Oct 19, 2026
## Why

On 2026-10-15 (noon EDT) nodejs/undici published eight repository-level security advisories (`gh api "repos/nodejs/undici/security-advisories"`). They are fixed in 6.29.1 / 7.30.1 / 8.12.2, all published 2026-10-15. They are **not yet in GitHub's global advisory database** (`gh api "advisories?ecosystem=npm&affects=undici@7.29.0"` lists only the older 2026-09 batch), so Dependabot and Renovate do not flag them. This repo pins undici through `overrides.undici`, locked at 7.29.0, which is inside the vulnerable range of **all eight** (each is `< 7.30.1` on the 7.x line, with a lower bound between 7.0.0 and 7.18.0):

| Advisory | Severity | What |
|---|---|---|
| [GHSA-xjj6-f33q-929c](GHSA-xjj6-f33q-929c) | high | DoS via unhandled error on excessive content-encodings |
| [GHSA-4w7w-5xrc-9h26](GHSA-4w7w-5xrc-9h26) | high | cache poisoning and cross-user disclosure via cross-origin redirects |
| [GHSA-2m5v-pch6-74hx](GHSA-2m5v-pch6-74hx) | high | TLS certificate validation bypass via dropped tls options in Agent |
| [GHSA-qrxm-fqhp-jwvm](GHSA-qrxm-fqhp-jwvm) | high | DoS via TLS 1.2 certificate-changing renegotiation |
| [GHSA-4qv8-2p9j-2685](GHSA-4qv8-2p9j-2685) | medium | DoS via unbounded memory in the EventSource parser |
| [GHSA-f72v-6w45-q472](GHSA-f72v-6w45-q472) | medium | Subresource Integrity bypass via non-lowercase algorithm tokens |
| [GHSA-9fm3-97px-rjf6](GHSA-9fm3-97px-rjf6) | medium | WebSocket client DoS via unbounded automatic PONG buffering |
| [GHSA-55wc-3pg8-jfw6](GHSA-55wc-3pg8-jfw6) | medium | DoS via unhandled exception in the decompress interceptor |

7.29.0 was also still inside the **ten advisories fixed in 7.29.1** that GitHub's global database lists from 2026-09-28 and 2026-09-29 (`gh api "advisories?ecosystem=npm&affects=undici@7.29.0"`): two high (GHSA-rfgv-xxqx-mfg5, GHSA-w293-vg96-wgc3), five medium and three low. 7.30.1 contains those fixes too.

Approved by roshne on 2026-10-19 (US Eastern): one PR per repo, undici only. Not from the Dependency Dashboard; no checkbox ticked. Rackbops/Tooling#1362 is the same fix for Tooling's worker.

## What changed

| Item | Before | After |
|---|---|---|
| `package.json` `overrides.undici` | `^7.29.0` | `^7.30.1` |
| `bun.lock` undici | 7.29.0 | **7.30.1** (integrity matches the registry) |

- Still the 7.x line, as before. undici 8 is the closed, declined PR #148 and is not part of this change.
- The override forces this one copy onto every consumer: `discord.js` and `@discordjs/rest` declare `undici ^6.27.0`, and the `@discordjs/rest` copy nested under `@discordjs/ws` pins `6.24.1`. That pre-existing choice is unchanged; `bun.lock` still has a single `undici` entry.
- Diff: `package.json` (1 line) and `bun.lock` (the override line and the undici entry).
- **What this does and does not change at runtime (found in review, reproduced).** On Bun 1.4.3 a bare `undici` specifier does not necessarily load `node_modules/undici`: in this worktree `bun -e "console.log(require.resolve('undici'))"` prints `undici` (not a file path) even though `require('undici/package.json').version` is `7.30.1`, and the same holds when resolving from `node_modules/@discordjs/rest`. That points to Bun's built-in `undici` module taking precedence for a bare import (compare oven-sh/bun#17799 and the unmerged oven-sh/bun#36102). If so, the bot's `@discordjs/rest` calls (`request`, `Headers`, `FormData`) and `discord.js`'s `fetch` run on Bun's built-in module today, not on the package version, and this bump protects the lockfile, the typecheck/test tooling and any Node-based use, and takes effect on the bot as soon as Bun resolves the installed package. `docs/research/2026-09-03-ci-github-actions.md` (lines 366-377) says the override does govern those imports; this probe suggests that is not the case on 1.4.3, and I did not change that doc. The gateway uses `ws`, not undici, so the WebSocket advisory (GHSA-9fm3) does not reach the bot through discord.js either. The bot's own HTTP (`github.ts`, `update.ts`, `docker.ts`, `plugins/index.ts`) is Bun-native `fetch` and sets no undici agent.

## Local checks (CI `ci.yml`; Bun 1.4.3, the `packageManager` pin)

- `bun install --frozen-lockfile` at the root and in `ops/admin`: exit 0.
- `bun run check`, `bunx tsc --noEmit -p ops/tsconfig.json`, and `bun run check` in `ops/admin`: all exit 0.
- `bun test ./src ./test ./ops/admin ./ops/docker-compose.test.ts`: **3534 pass, 22 skip, 0 fail** (3556 tests, 53 files).

## Mutation

Setting `overrides.undici` back to `^7.29.0` without relocking makes `bun install --frozen-lockfile` fail: `error: lockfile had changes, but lockfile is frozen` / `note: overrides in package.json changed since bun.lock was saved`. Restoring the file makes the frozen install pass again.

## Unverified locally

- The unscoped `bun test` and `bun test --randomize` that CI runs (the unscoped run hangs locally on bash-spawning ops tests); tests that skip on Windows or without `docker compose` / `jq`.
- The `docker-build` job (Docker is not running here): CI only.
- Behaviour of the live bot against Discord on 7.30.1: not exercised; no bot was started.

## Review gate

Round 1 of the three-reviewer gate (author plus two read-only adversarial reviewers, both on the merged state of all four undici PRs). Reviewer A (upstream release notes vs this code): SOUND as a diff, one major finding on this body's claims. Reviewer B (claims vs diff): SOUND, minor wording findings. Dispositions:

1. A major / B minor, reproduced: the body said the override "governs what discord.js / @discordjs/rest resolve ... where the TLS, content-encoding and WebSocket advisories would bite". On Bun 1.4.3 a bare `undici` import likely resolves to Bun's built-in module (probe above), and the WebSocket advisory cannot bite because the gateway is `ws`. Fixed in this body (the diff is correct either way: harmless if the built-in wins, what is needed once Bun resolves the package).
2. B, minor, reproduced: GHSA-3wwx-pv8p-q78v in the ten-advisory batch was published 2026-09-28 (5:42 pm EDT), the other nine on 2026-09-29. Body corrected; the pushed commit message still says 2026-09-29 and is not amended.
3. A, note: 7.29.1/7.30.0/7.30.1 release notes show no breaking change for what this repo imports (`request`, `Headers`, `FormData`, `fetch`); 7.30.1 `connect.js` defaults `allowH2` to false, so the REST path stays HTTP/1.1. 7.30.x does change a decompress-interceptor default ("disable size limit by default"), which nothing here uses.

Both fixes were wording only, so no second round was run. Reviewers verified: one `undici@7.30.1` entry in `bun.lock` (the nested `@discordjs/ws/@discordjs/rest` pin of 6.24.1 collapses into it), integrity matches the registry, no bundled copy in `node_modules`, `ops/admin/bun.lock` has only `undici-types`, CI green on this PR (checks and docker-build).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

3 participants