Skip to content

node: expose internal/* under --expose-internals, implement internal/errors helpers [1v1cy7]#29823

Open
robobun wants to merge 10 commits into
mainfrom
farm/6e2211c1/expose-internal-errors
Open

node: expose internal/* under --expose-internals, implement internal/errors helpers [1v1cy7]#29823
robobun wants to merge 10 commits into
mainfrom
farm/6e2211c1/expose-internal-errors

Conversation

@robobun

@robobun robobun commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

What

Node tests flagged // Flags: --expose-internals do require('internal/errors') (and friends) directly. In Bun, src/js/internal/*.ts modules were only reachable from other builtins via the build-time requireTransformer → numeric InternalModuleRegistry IDs; userland require('internal/errors') failed with Cannot find module.

Module loader

  • ModuleLoader.zig: add ExposedInternals — a comptime-derived string map of every internal:* tag in ResolvedSource.Tag keyed as internal/<name>. When is_allowed_to_use_internal_testing_apis is set (same gate as bun:internal-for-testing; always on in debug builds), fetchBuiltinModule serves those specifiers straight from the InternalModuleRegistry.
  • VirtualMachine.zig: let the same specifiers through both _resolve and resolveMaybeNeedsTrailingSlash under the same gate.

internal/errors

Fills in src/js/internal/errors.ts with the surface area the Node tests exercise — previously it only exported aggregateTwoErrors:

  • E / codes / getMessage / makeNodeErrorWithCode with arg-count assertion (ERR_INTERNAL_ASSERTION)
  • SystemError (with info/errno/syscall/path/dest accessors, kIsNodeError, custom inspect/toString)
  • hideStackFrames (+ .withoutStackTrace), HideStackFramesError
  • AbortError, genericNodeError, isErrorStackTraceLimitWritable
  • determineSpecificType, formatList
  • DNSException, UVException, UVExceptionWithHostPort, uvErrmapGet (via process.binding('uv'))
  • kIsNodeError, captureLargerStackTrace, useOriginalName
  • aggregateTwoErrors now re-captures the caller's stack like Node does
  • codes.ERR_INVALID_ARG_TYPE / codes.ERR_ACCESS_DENIED registered so new codes.ERR_INVALID_ARG_TYPE(...) works

Other

  • New internal/test/binding — thin process.binding proxy (Node's internalBinding for tests).
  • New internal/error_serdes — error serializer used by worker message passing tests.

Tests (14/18 from group 1v1cy7)

test-dns-memory-error.js
test-error-aggregateTwoErrors.js
test-error-format-list.js
test-error-value-type-detection.mjs
test-errors-aborterror.js
test-errors-systemerror-frozen-intrinsics.js
test-errors-systemerror-stackTraceLimit-custom-setter.js
test-errors-systemerror-stackTraceLimit-deleted-and-Error-sealed.js
test-errors-systemerror-stackTraceLimit-deleted.js
test-errors-systemerror-stackTraceLimit-has-only-a-getter.js
test-errors-systemerror-stackTraceLimit-not-writable.js
test-errors-systemerror.js
test-internal-error-original-names.js
test-uv-unmapped-exception.js

Regression test: test/js/node/module/expose-internals.test.ts.

Three tests had superficial edits for engine-specific wording only (never error.code, never assertion intent):

  • test-errors-systemerror.js: V8's "Cannot read properties of undefined (reading 'syscall')" / "…read only property 'Symbol(kIsNodeError)'" → regexes that also match JSC's phrasing.
  • test-error-aggregateTwoErrors.js, test-dns-memory-error.js: top-of-module frame label at Object (Node CJS wrapper) → also accept at <anonymous>. Still asserts the internal frame was stripped and the caller's frame is first.

Deferred (each is an unrelated Bun gap)

  • test-error-serdes.jsutil.isDeepStrictEqual rejects Object.create(Error.prototype, …) vs a real ErrorInstance (Bun__deepEquals ErrorInstanceType branch in bindings.cpp).
  • test-errors-hide-stack-frames.js — native validateInteger shows up in caught-error .stack; Node wraps its JS validator in hideStackFrames.
  • test-internal-errors.jsconsole.log doesn't route through process.stdout.write, so hijackStdout captures nothing.
  • test-webstream-readablestream-pipeto.jspipeTo rejects with the raw abort reason instead of a DOMException('AbortError') wrapper.

Dropped from this PR

  • The process.config.variables.v8_enable_i8n_supportv8_enable_i18n_support typo fix was reverted (afc51b2): fixing it un-skips 11 unrelated hasIntl-gated tests (test-icu-*, test-url-format*, test-whatwg-*) that each need their own work. That belongs in a separate PR.

Verify

bun bd
node ./scripts/runner.node.mjs --quiet --exec-path=./build/debug/bun-debug --node-tests \
  test-dns-memory-error test-error-aggregateTwoErrors test-error-format-list \
  test-error-value-type-detection test-errors-aborterror test-errors-systemerror \
  test-errors-systemerror-frozen-intrinsics test-errors-systemerror-stackTraceLimit-custom-setter \
  test-errors-systemerror-stackTraceLimit-deleted test-errors-systemerror-stackTraceLimit-deleted-and-Error-sealed \
  test-errors-systemerror-stackTraceLimit-has-only-a-getter test-errors-systemerror-stackTraceLimit-not-writable \
  test-internal-error-original-names test-uv-unmapped-exception
bun bd test test/js/node/module/expose-internals.test.ts

@robobun

robobun commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 12:19 PM PT - May 4th, 2026

@robobun, your commit e05878a has some failures in Build #51206 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 29823

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

bun-29823 --bun

@robobun

robobun commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator Author

Status

14/18 tests from node-compat group 1v1cy7 now pass.

before after
require('internal/errors') ❌ Cannot find module ✅ resolves (gated by BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING, same as bun:internal-for-testing)

Deferred (separate Bun gaps, not internal/errors):

  • test-error-serdesdeepStrictEqual(ErrorInstance, Object.create(Error.prototype)) type mismatch in Bun__deepEquals
  • test-errors-hide-stack-frames → native validators visible in caught .stack
  • test-internal-errorsconsole.log bypasses process.stdout.write
  • test-webstream-readablestream-pipetopipeTo abort reason not wrapped in DOMException

Regression test: test/js/node/module/expose-internals.test.ts. Lint green, review threads resolved.


CI (#48546): 280 passed / 6 failed

None of this PR's tests appear in any failure. The 6 failed lanes are:

  • agent death (exit -1) on darwin-14-x64 / debian-13-x64-asan
  • 4× Windows/macOS lanes hitting the usual fleet-wide flakes

All 25 flagged tests (bake/dev-and-prod, serve-http3, bake/stress, hot.test, bun-install-registry, shell-blocking-pipe, serve-protocols, wpt-h2, s3, etc.) appear at the same or higher frequency in unrelated PR builds #48507/#48509/#48511/#48520/#48524. The 11-test hasIntl regression from the previous build was fixed in afc51b2.

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@robobun has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 25 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 62129402-f304-4be8-8f30-859ccb8e9e52

📥 Commits

Reviewing files that changed from the base of the PR and between 4a5b596 and e3eada9.

📒 Files selected for processing (1)
  • test/js/node/module/expose-internals.test.ts

Walkthrough

Adds a compile-time map and runtime fast-path to expose Node-style internal/* module specifiers when debug/testing flags are enabled, implements a Node-style error subsystem and error (de)serialization, fixes a process.config typo, and adds numerous tests exercising errors and internal exposure.

Changes

Cohort / File(s) Summary
Internal Module Resolution
src/bun.js/ModuleLoader.zig, src/bun.js/VirtualMachine.zig
Adds ExposedInternals compile-time map and runtime lookup; intercepts internal/* specifiers with fast-paths in resolution flows when debug or internal-testing flags permit.
Error Handling & Serialization
src/js/internal/errors.ts, src/js/internal/error_serdes.ts
Implements Node-style error-code registry, multiple error classes (SystemError, UV/DNS exceptions, AbortError, etc.), stack-trace utilities, formatting helpers, and an error_serdes module to serialize/deserialize errors (handles symbols, causes, circular refs, custom inspect).
Process Config Fix
src/bun.js/bindings/BunProcess.cpp
Corrects typo in process.config.variables key: v8_enable_i8n_supportv8_enable_i18n_support.
Test Helpers
src/js/internal/test/binding.ts
Adds internalBinding(name: string) wrapper exported for tests to access Node-style internal bindings.
Parallel & Unit Tests (errors, formatting, types)
test/js/node/test/parallel/test-error-*.js, test/js/node/test/parallel/test-errors-*.js, test/js/node/test/parallel/test-uv-*.js, test/js/node/test/parallel/test-dns-memory-error.js, test/js/node/test/parallel/test-internal-error-original-names.js
Adds many parallel tests validating aggregateTwoErrors, formatList, determineSpecificType, AbortError, SystemError and UV/DNS exception behavior across stackTraceLimit edge cases, useOriginalName semantics, and libuv memory-error propagation.
Module Exposure Tests
test/js/node/module/expose-internals.test.ts
Adds a Bun integration test that spawns CJS/ESM children to verify internal/<name> specifiers (e.g., internal/errors) resolve under the expose-internals/testing flag and asserts presence/behavior of exported error utilities and process.config.variables.v8_enable_i18n_support.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: exposing internal Node modules under --expose-internals and implementing the internal/errors module with its helpers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering the objectives, implementation details, test coverage, and deferred work.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Review rate limit: 0/5 reviews remaining, refill in 2 minutes and 25 seconds.

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

@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: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/js/internal/error_serdes.ts`:
- Around line 137-139: The custom-inspect branch should only run when the hook
is callable: change the guard in the block that returns
Buffer.from(StringFromCharCode(kCustomInspectedObject) + inspect(error), "utf8")
so it requires $isCallable(error[customInspectSymbol]) in addition to error !=
null and customInspectSymbol in error; i.e., check that the property exists and
is callable (using the JSC intrinsic $isCallable) before invoking inspect and
emitting the kCustomInspectedObject marker.
- Around line 181-184: The kInspectedSymbol branch uses buf.length (UTF-8 byte
length) to slice the JS string, causing broken non-ASCII symbols; change to
convert buffer to string first and use the string's length for slicing. In the
kInspectedSymbol case, call fromBuffer(error) -> const buf = fromBuffer(error);
const s = buf.toString("utf8"); then use StringPrototypeSubstring.$call(s,
kSymbolStringLength, s.length - 1) (and pass that result into SymbolFor) instead
of using buf.length - 1 so non-ASCII symbol descriptions deserialize correctly.

In `@src/js/internal/errors.ts`:
- Around line 71-76: The code temporarily mutates Error.stackTraceLimit (in the
block guarded by isErrorStackTraceLimitWritable()) but doesn't ensure
restoration on exception; wrap the mutation and creation of the AggregateError
(the code that sets limit, constructs err via new AggregateError(new
SafeArrayIterator([outerError, innerError]), outerError.message), calls
ErrorCaptureStackTrace(err, aggregateTwoErrors), etc.) in a try/finally so
Error.stackTraceLimit is always restored to its previous value; apply the same
try/finally pattern to the other similar blocks that mutate
Error.stackTraceLimit (the blocks that also use AggregateError,
SafeArrayIterator and ErrorCaptureStackTrace) to avoid leaking process-wide
state.
- Line 339: Replace the lint-blocking short-circuit expressions with explicit
conditional statements: instead of relying on the side-effect expression
"Error.stackTraceLimit && ErrorCaptureStackTrace(error, wrappedFn)" use a clear
if (Error.stackTraceLimit) { ErrorCaptureStackTrace(error, wrappedFn); } (and do
the analogous change at the other occurrence around line that uses the same
pattern). This targets the invocation using Error.stackTraceLimit and
ErrorCaptureStackTrace with the variables error and wrappedFn so the linter
no-unused-expressions rule is satisfied.

In `@src/js/internal/test/binding.ts`:
- Around line 6-8: internalBinding currently forwards any input to
process.binding without validation; add argument validation using the internal
validators (import from internal/validators) to ensure name is a non-empty
string and throw the appropriate Node error code (e.g. $ERR_INVALID_ARG_TYPE)
for invalid types/values before calling process.binding; update the function
internalBinding to call the validator (e.g., validateString or
validators.string/validateString with the argument name 'name') and only invoke
process.binding(name) after validation.
🪄 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: 0e3fcaf8-592b-4d5b-bb06-996a5d844eb0

📥 Commits

Reviewing files that changed from the base of the PR and between ca9e089 and c15de06.

📒 Files selected for processing (20)
  • src/bun.js/ModuleLoader.zig
  • src/bun.js/VirtualMachine.zig
  • src/bun.js/bindings/BunProcess.cpp
  • src/js/internal/error_serdes.ts
  • src/js/internal/errors.ts
  • src/js/internal/test/binding.ts
  • test/js/node/test/parallel/test-dns-memory-error.js
  • test/js/node/test/parallel/test-error-aggregateTwoErrors.js
  • test/js/node/test/parallel/test-error-format-list.js
  • test/js/node/test/parallel/test-error-value-type-detection.mjs
  • test/js/node/test/parallel/test-errors-aborterror.js
  • test/js/node/test/parallel/test-errors-systemerror-frozen-intrinsics.js
  • test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-custom-setter.js
  • test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-deleted-and-Error-sealed.js
  • test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-deleted.js
  • test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-has-only-a-getter.js
  • test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-not-writable.js
  • test/js/node/test/parallel/test-errors-systemerror.js
  • test/js/node/test/parallel/test-internal-error-original-names.js
  • test/js/node/test/parallel/test-uv-unmapped-exception.js

Comment thread src/js/internal/error_serdes.ts
Comment thread src/js/internal/error_serdes.ts
Comment thread src/js/internal/errors.ts
Comment thread src/js/internal/errors.ts Outdated
Comment thread src/js/internal/test/binding.ts

@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.

♻️ Duplicate comments (2)
src/js/internal/error_serdes.ts (2)

141-143: ⚠️ Potential issue | 🟡 Minor

Gate custom-inspect serialization on a callable hook.

Line 141 currently branches on property presence only. If error[customInspectSymbol] is non-callable, this path should not be used.

Suggested fix
-    if (error != null && customInspectSymbol in error) {
-      return Buffer.from(StringFromCharCode(kCustomInspectedObject) + inspect(error), "utf8");
-    }
+    if (error != null) {
+      const customInspect = error[customInspectSymbol];
+      if ($isCallable(customInspect)) {
+        return Buffer.from(StringFromCharCode(kCustomInspectedObject) + inspect(error), "utf8");
+      }
+    }

As per coding guidelines, "Use JSC intrinsics (prefixed with $) such as $Array.from(), $isCallable(), and $newArrayWithSize() for performance-critical operations".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/internal/error_serdes.ts` around lines 141 - 143, The branch using
customInspectSymbol should only run when error[customInspectSymbol] is callable;
change the condition in the block that currently checks "error != null &&
customInspectSymbol in error" to also verify callability (e.g., using
$isCallable(error[customInspectSymbol])). Keep the current behavior of producing
Buffer.from(StringFromCharCode(kCustomInspectedObject) + inspect(error), "utf8")
and ensure you use the suggested JSC intrinsics (e.g., $isCallable and other
$-prefixed intrinsics where appropriate) when validating the hook before calling
inspect.

186-188: ⚠️ Potential issue | 🟠 Major

Slice inspected Symbol text using string length, not buffer byte length.

Line 187 uses buf.length - 1 while slicing a JS string. buf.length is UTF-8 bytes, so non-ASCII symbol descriptions deserialize incorrectly.

Suggested fix
     case kInspectedSymbol: {
-      const buf = fromBuffer(error);
-      return SymbolFor(StringPrototypeSubstring.$call(buf.toString("utf8"), kSymbolStringLength, buf.length - 1));
+      const inspected = fromBuffer(error).toString("utf8");
+      return SymbolFor(
+        StringPrototypeSubstring.$call(inspected, kSymbolStringLength, inspected.length - 1),
+      );
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/internal/error_serdes.ts` around lines 186 - 188, The code is slicing
the JS string using buf.length (UTF-8 byte count) which corrupts non-ASCII
symbol text; change to decode the buffer first (use fromBuffer ->
buf.toString("utf8") into a local str), compute the end index from str.length
(e.g. const end = str.length - 1), and call StringPrototypeSubstring.$call(str,
kSymbolStringLength, end) before passing to SymbolFor so substring uses
character length not byte length.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/js/internal/error_serdes.ts`:
- Around line 141-143: The branch using customInspectSymbol should only run when
error[customInspectSymbol] is callable; change the condition in the block that
currently checks "error != null && customInspectSymbol in error" to also verify
callability (e.g., using $isCallable(error[customInspectSymbol])). Keep the
current behavior of producing
Buffer.from(StringFromCharCode(kCustomInspectedObject) + inspect(error), "utf8")
and ensure you use the suggested JSC intrinsics (e.g., $isCallable and other
$-prefixed intrinsics where appropriate) when validating the hook before calling
inspect.
- Around line 186-188: The code is slicing the JS string using buf.length (UTF-8
byte count) which corrupts non-ASCII symbol text; change to decode the buffer
first (use fromBuffer -> buf.toString("utf8") into a local str), compute the end
index from str.length (e.g. const end = str.length - 1), and call
StringPrototypeSubstring.$call(str, kSymbolStringLength, end) before passing to
SymbolFor so substring uses character length not byte length.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: de8b4d23-6ff1-490c-ac9a-4ac0cf884045

📥 Commits

Reviewing files that changed from the base of the PR and between c15de06 and 89252dd.

📒 Files selected for processing (2)
  • src/js/internal/error_serdes.ts
  • test/js/node/module/expose-internals.test.ts

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/js/internal/errors.ts`:
- Around line 644-650: Replace direct array method calls on the local variables
with the tamper-proof prototype wrappers: instead of types.indexOf("object") and
types.splice(pos, 1) use ArrayPrototypeIndexOf.$call(types, "object") and
ArrayPrototypeSplice.$call(types, pos, 1) respectively; also ensure the file
declares the prototype references (e.g., ArrayPrototypeIndexOf and
ArrayPrototypeSplice) alongside the other prototype bindings at the top so the
new .$call usages match the project's tamper-proof pattern and reference the
same symbols as other functions in this file.
🪄 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: 16149c19-a995-4473-adf5-57df6cbb0307

📥 Commits

Reviewing files that changed from the base of the PR and between 89252dd and 4a5b596.

📒 Files selected for processing (1)
  • src/js/internal/errors.ts

Comment thread src/js/internal/errors.ts
Comment thread test/js/node/module/expose-internals.test.ts Outdated

@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.

Thanks for fixing the ASAN stderr assertion. I didn't find anything else blocking, but given this touches the Zig module loader/resolver and adds ~700 lines of internal/errors infrastructure (including a behavior change to aggregateTwoErrors used by existing builtins), it's worth a human pass.

Extended reasoning...

Overview

This PR makes internal/* specifiers resolvable from userland when is_allowed_to_use_internal_testing_apis is set (or in debug builds), so that Node.js --expose-internals tests can run. It does this via a comptime-derived ExposedInternals string map in ModuleLoader.zig plus fast-path checks in two resolve paths in VirtualMachine.zig. On the JS side it expands src/js/internal/errors.ts from a single aggregateTwoErrors export to a ~700-line port of Node's lib/internal/errors.js (E/codes/SystemError/hideStackFrames/UVException/DNSException/determineSpecificType/formatList/etc.), adds a verbatim port of internal/error_serdes, and a thin internal/test/binding shim. Fourteen upstream Node parallel tests plus one Bun regression test are added.

Security risks

The new internal/* resolution surface is gated behind the same flag as bun:internal-for-testing in release builds, so it doesn't expose internals to ordinary userland. The Environment.isDebug or ... disjunction means debug builds now always resolve every internal:* registry entry from userland — that's a deliberate widening but worth a maintainer's eye since it exposes the entire internal module registry, not just internal/errors. No injection, auth, or data-exposure vectors otherwise; the JS additions are pure error-formatting/serialization helpers.

Level of scrutiny

Moderate-to-high. The Zig changes sit on the module resolution hot path (the gate check runs on every internal/-prefixed specifier), and src/js/internal/errors.ts is loaded by other builtins — notably aggregateTwoErrors now mutates Error.stackTraceLimit and re-captures the caller's stack, which is a behavior change for every existing internal caller (e.g. streams). The error_serdes and test/binding modules are inert unless explicitly required under the flag.

Other factors

All CodeRabbit threads are resolved (mostly upstream-parity rationale). My one prior finding (empty-stderr assertions failing on the x64-asan lane) was fixed in e3eada9. CI on the latest build shows only fleet-wide flakes per the status comment. The PR is well-scoped and well-tested, but at ~1500 added lines spanning the loader, resolver, and a core internal module, it exceeds the threshold for bot auto-approval.

robobun and others added 6 commits May 4, 2026 10:29
…internal/errors

Node tests flagged '// Flags: --expose-internals' do
require('internal/errors') etc. Bun's src/js/internal/* modules were
only reachable from other builtins via the build-time require
transformer — userland require('internal/errors') failed with 'Cannot
find module'.

- ModuleLoader: derive a comptime map of all internal:* ResolvedSource
  tags and, when is_allowed_to_use_internal_testing_apis is set (same
  gate as bun:internal-for-testing; always on in debug), resolve and
  fetch 'internal/<name>' from the InternalModuleRegistry.
- VirtualMachine: let those specifiers through both resolve paths.
- internal/errors.ts: implement the surface area these tests use —
  E/codes, SystemError, getMessage, hideStackFrames, AbortError,
  determineSpecificType, formatList, DNSException, UVException[WithHostPort],
  kIsNodeError, isErrorStackTraceLimitWritable, aggregateTwoErrors
  (now captures the caller's stack like Node).
- Add internal/test/binding (thin process.binding proxy) and
  internal/error_serdes.
- BunProcess: fix typo v8_enable_i8n_support -> v8_enable_i18n_support
  so common.hasIntl is truthy.

Checks in 14 Node parallel tests from the internal/errors group.
Verifies require/import of internal/errors (and siblings) resolves under
the testing flag and that the new helper surface is present; also covers
the v8_enable_i18n_support typo fix in process.config.
oxlint no-unused-expressions flagged two '&&'-as-if patterns ported from
upstream Node; rewrite as explicit 'if' statements.
Correcting the key name flips common.hasIntl from false to true, which
un-skips 11 pre-existing node parallel tests (test-icu-*, test-url-*,
test-whatwg-*) that each fail for unrelated reasons (missing
process.binding('icu'), different error message wording, URL formatting
differences). Fixing those is out of scope for the internal/errors work;
the typo fix belongs in its own PR where each un-skipped test can be
addressed.

Also drops the corresponding assertion from expose-internals.test.ts.
On the x64-asan lane every JS-executing bunExe() subprocess writes
'WARNING: ASAN interferes with JSC signal handlers...' to stderr, which
bunEnv does not suppress. The asan lane died with exit -1 on the last
CI run so this never surfaced. Switch to a combined toEqual that keeps
stderr in the failure diff (for diagnostics) but accepts any string;
stdout == 'ok' + exitCode == 0 remain the actual regression guard.
@Jarred-Sumner Jarred-Sumner force-pushed the farm/6e2211c1/expose-internal-errors branch from e3eada9 to 53774a4 Compare May 4, 2026 10:29
Comment thread src/jsc/VirtualMachine.zig
resolveMaybeNeedsTrailingSlash's contract is that callers deref
res.result.value on success in addition to deref'ing their own borrowed
specifier (ZigGlobalObject.cpp moduleLoaderResolve/moduleLoaderImportModule
both do keyZ.deref() + res.result.value.deref()). Storing the borrowed
specifier directly was +1 in / -2 out on the same StringImpl for every
ESM import of an internal/* module. Match the sibling HardcodedModule
branch and return specifier.dupeRef().
Comment thread src/js/internal/errors.ts Outdated
This module is required by internal/streams/{destroy,pipeline,readable}
via the build-time requireTransformer, so it evaluates on first
node:stream load — not only under --expose-internals. Reword the
comment so future E() registrations don't assume they're test-only.

@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.

No blockers from my side — the three issues I raised earlier (ASAN stderr assertion, missing dupeRef() in resolveMaybeNeedsTrailingSlash, misleading load-scope comment) are all fixed. Deferring to a human because this touches the module loader/resolver in Zig with manual refcounting and adds ~700 lines to internal/errors on the node:stream cold-start path.

Extended reasoning...

Overview

This PR enables require('internal/*') from userland under the BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING gate (and unconditionally in debug builds) so that Node's --expose-internals test corpus can run. It does so by:

  • Adding a comptime ExposedInternals string map in src/jsc/ModuleLoader.zig and a fast-path in fetchBuiltinModule.
  • Adding matching short-circuits in VirtualMachine.zig _resolve and resolveMaybeNeedsTrailingSlash (the latter with a specifier.dupeRef() to honour the caller-derefs contract).
  • Expanding src/js/internal/errors.ts from ~22 lines to ~708 lines with a near-port of Node's lib/internal/errors.js surface (E, codes, SystemError, hideStackFrames, UVException/DNSException, AbortError, determineSpecificType, formatList, etc.).
  • Adding internal/error_serdes (~200 lines, verbatim Node port) and internal/test/binding.
  • Importing 14 upstream Node parallel tests plus one new Bun regression test.

Security risks

The new resolver path is gated by Environment.isDebug or is_allowed_to_use_internal_testing_apis — the same gate as the existing bun:internal-for-testing module — so it does not expose internals to release-build userland by default. The exposed modules are already reachable from other builtins; this only adds a userland entry point under a testing flag. I don't see injection, auth, or data-exposure concerns. The one memory-safety issue (refcount underflow on the echoed specifier) was caught and fixed in fe5ef10.

Level of scrutiny

I'd put this at moderate-to-high. Although most of the line count is upstream Node test files and a fairly mechanical port of lib/internal/errors.js, the parts that warrant a human eye are:

  • The Zig module-loader/resolver changes sit on a manual-refcounting boundary (ErrorableString, bun.String); one real bug was already found here during review.
  • internal/errors.ts is required at module-eval time by internal/streams/{destroy,pipeline,readable}, so the ~30 prototype captures, six class declarations, and two eager E() registrations now run on first node:stream load in every process. The cost is small, but it's a hot-path change worth a second opinion.
  • Three upstream test files were edited for JSC vs V8 message-text differences — the edits look surgical (regex widening, frame-label alternation) but a maintainer should confirm the policy on diverging from upstream test text.

Other factors

  • All CodeRabbit threads and all three of my prior inline comments are resolved with commits.
  • The only remaining bug-hunter finding is a non-blocking test.concurrent nit on the new regression test.
  • robobun's CI summary reports the 14 target tests passing and attributes the failed lanes to fleet-wide flakes/agent deaths, but the most recent build banner still shows ❌ on fe5ef10 — worth confirming the fbcd73d build is green before merge.
  • The v8_enable_i18n_support typo fix was correctly reverted out of scope (it un-skipped 11 unrelated tests).

Comment thread test/js/node/module/expose-internals.test.ts Outdated
robobun added 2 commits May 4, 2026 12:05
Both tests spawn independent bunExe() children with no shared state;
per test/CLAUDE.md convention run them concurrently to halve this
file's wall-clock on debug/ASAN lanes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant