Repository navigation
Conversation
FetchError reported its name as "Error", left errno and erroredSysCall unset and always defined code, and AbortError had no type. node-fetch names each error after its class, copies code, errno and erroredSysCall from the system error, and gives AbortError the type "aborted". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: oven-sh/bun/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review. WalkthroughUpdates Changesnode-fetch error properties
Suggested reviewers: Priority: ⬇️ Low Merge Risk: ⚪ Minimal · up to The change updates node-fetch error metadata without changing native fetch abort behavior. No actionable user-facing risk remains, so it is ready for normal checks. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change may affect applications that classify exported errors, but no new security capability or change to the fetch cancellation path was identified. Compatibility effects in external applications remain uncertain. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
Hotragn
left a comment
There was a problem hiding this comment.
I checked the part this rests on: FetchBaseError's name getter returns this.constructor.name, so it only works if release builds keep class names. They do. src/codegen/bundle-modules.ts:217 bundles builtins with --minify-syntax --keep-names and doesn't minify identifiers. On 1.4.2, FetchError.name and AbortError.name already come out right. So the getter gives the same answer in debug and release.
I also compared against real node-fetch 3.3.2 on Node 24, and found a gap next to this PR that I think matters more. This PR fixes the errors you build with new FetchError(...). But most code never builds one. It catches the error fetch() rejects with, and fetch() in the shim does await nativeFetch.$call(undefined, url, init) and passes native errors through unchanged:
fetch() fails because… |
node-fetch 3.3.2 on Node 24 | Bun 1.4.2 |
|---|---|---|
| the signal aborted | AbortError, type: "aborted", instanceof AbortError true |
DOMException AbortError, no type, instanceof AbortError false |
| connection refused | FetchError, type: "system", code/errno: "ECONNREFUSED", erroredSysCall: "connect" |
TypeError, code: "ConnectionRefused", instanceof FetchError false |
| DNS lookup failed | FetchError, code: "ENOTFOUND", erroredSysCall: "getaddrinfo" |
TypeError, code: "ENOTFOUND", instanceof FetchError false |
The usual node-fetch retry and timeout code (if (err instanceof AbortError), if (err instanceof FetchError && err.code === "ECONNREFUSED"), err.type === "aborted") never matches on Bun. That's true before this PR and after it. This PR doesn't cause it. I'm raising it here because this PR's classes are what a fix would build on: catch around the nativeFetch call and rethrow new AbortError("The operation was aborted.") or new FetchError(\request to ${url} failed, reason: ${e.message}`, "system", { code, syscall })`.
Bun's ConnectionRefused would need mapping to ECONNREFUSED. And some code may already check instanceof DOMException on Bun, so the rethrow is a behavior change a maintainer should sign off on. That's why I'd keep it out of this PR. I've opened it as its own issue so it doesn't get lost.
Script to reproduce: install node-fetch@3.3.2, then run it with node and with bun:
import fetch, { AbortError, FetchError } from "node-fetch";
import http from "node:http";
const srv = http.createServer((q, s) => setTimeout(() => s.end("x"), 2000)).listen(0);
await new Promise(r => srv.once("listening", r));
const ac = new AbortController();
const p = fetch(`http://127.0.0.1:${srv.address().port}/`, { signal: ac.signal });
setTimeout(() => ac.abort(), 20);
try { await p; } catch (e) { console.log(e.constructor.name, e instanceof AbortError, e.type); }
try { await fetch("http://127.0.0.1:1/"); } catch (e) { console.log(e.constructor.name, e instanceof FetchError, e.code); }
srv.closeAllConnections(); srv.close();The changes in the diff look right to me. code/errno/erroredSysCall are set only when there's a system error, like errors/fetch-error.js. The aborted fetch() path can't regress: it rejected with a native DOMException before and still does, so instanceof AbortError was already false.
What does this PR do?
The builtin
node-fetchshim's error classes do not match node-fetch's:new FetchError(...)hasname"Error", soString(err)is"Error: ..."and thetoStringTagis[object Error].errnoanderroredSysCallare never set.code: undefinedis always defined, even with no system error.new AbortError(...)has notype.node-fetch names each error after its class (
errors/base.js). It copiescode,errnoanderroredSysCallfrom the system error only when one is given (errors/fetch-error.js). ItsAbortErroris aFetchBaseErrorwith type"aborted"(errors/abort-error.js). Libraries and tests that build these errors and checkerr.name === "FetchError",err.errnoorerr.type === "aborted"behaved differently under Bun.Changes to
src/js/thirdparty/node-fetch.ts:FetchBaseErrorgetsnameandSymbol.toStringTaggetters that returnthis.constructor.name. Release builtins are bundled with--keep-names, so this givesFetchErrorandAbortError.FetchErrorsetscode/errno/erroredSysCallonly whensystemErroris given. The fields aredeclared, so no ownundefinedproperties are created.AbortErrornow extendsFetchBaseError, notDOMException, withtype = "aborted"by default, as node-fetch does. Abortedfetch()calls still reject with the nativeDOMExceptionAbortError. This class is only the exported constructor.How did you verify your code works?
New test in
test/js/node/http/node-fetch.test.js: "node-fetch FetchError and AbortError match node-fetch's error classes". It asserts the name, string form,toStringTag,type,code,errnoanderroredSysCall. It also checks thatcodeis absent without a system error, and theAbortErrorname and type.Red on Bun 1.4.2:
I did not build Bun locally (no
bun bdon this machine), so the green half was checked by loading the patched module into Bun 1.4.2:USE_SYSTEM_BUN=1 bun test test/js/node/http/node-fetch.test.js -t "<new test>"with Bun 1.4.2 (744846f), whose builtinnode-fetchis the same as main at 29d9638 for these lines: fails with the output below.src/js/thirdparty/node-fetch.tsinto a user module and changes only the builtin-only syntax ($cpp(...)→ the globalResponse/Request/Blob/Headers/FormData/File,.$call→.call,$isJSArray→Array.isArray,new $Buffer→Buffer.from,Promise.$resolve→Promise.resolve, and aReadable.fromWebstand-in for_ReadableFromWeb). A--preloadthenmock.module("node-fetch", ...)s it, and the same test file runs against it. The new test fails on the transformed main source and passes on the transformed patched source. The rest of the file gives the same results before and after the patch. The 12 body-stream tests that fail under the stand-in fail the same way in both runs, because the stand-in is not_ReadableFromWeb.prettier --checkandoxlint --config=oxlint.jsonpass on the changed files.tsc --noEmit -p src/jsgives no new errors innode-fetch.ts.🤖 Generated with Claude Code