Skip to content

TextDecoder: throw ERR_STRING_TOO_LONG instead of returning an empty string - #37216

Open
robobun wants to merge 3 commits into
mainfrom
farm/625ff9f2/textdecoder-string-too-long
Open

TextDecoder: throw ERR_STRING_TOO_LONG instead of returning an empty string#37216
robobun wants to merge 3 commits into
mainfrom
farm/625ff9f2/textdecoder-string-too-long

Conversation

@robobun

@robobun robobun commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Repro

for (const n of [2 ** 31 - 1, 2 ** 31, 2 ** 31 + 5, 2 ** 32 - 1]) {
  try { console.log(n, '->', new TextDecoder().decode(new Uint8Array(n)).length); }
  catch (e) { console.log(n, '-> threw', String(e).slice(0, 80)); }
}

On 1.4.0-canary this prints 2147483647 -> 2147483647, then -> 0 for every larger size, with exit code 0: any decode whose output is 2^31 .. 2^32-1 characters silently returns the empty string. Node throws ERR_STRING_TOO_LONG for sizes over its limit.

Cause

The all-ASCII decode path lands in ZigString__toValueGC (ZigString.toJS), where Zig::toStringCopy returns a null WTF::String when the length exceeds WTF::StringImpl::MaxLength (2^31-1) or allocation fails, and jsString() turns that null string into the empty string. No error was raised anywhere.

Fix

  • Zig::toJSStringGC (and with it ZigString__toValueGC, which now delegates to it) throws ERR_STRING_TOO_LONG when the input is over the length limit, and an out-of-memory error when the length was fine but allocation failed, instead of returning "". Its other caller, Bun::toJS's ZigString arm, already returns nullptr-with-pending-exception for dead strings, so the contract is unchanged.
  • The non-UTF-8 branches of the single-argument Zig::toStringCopy now check the synthetic allocation limit, matching the other helpers in helpers.h.
  • JSC__JSValue__fromEntries materializes the value and checks for the exception before putDirect (a nullptr JSString* would have put an empty JSValue).

decode(new Uint8Array(2 ** 31 - 1)) still returns a 2^31-1-length string.

This PR is scoped to the silent-empty-string path. The sibling bug on the external-string paths (lengths in 2^31 .. 2^32-1 aborting the process, e.g. fs.readFileSync of a >2 GiB file, #26323) is fixed separately in #37215, which caps String::max_length() at WTF::StringImpl::MaxLength; the two PRs no longer share any hunks and merge independently in either order.

Verification

New tests in test/js/web/encoding/text-decoder.test.js: a 2^31-byte decode must throw ERR_STRING_TOO_LONG (the zeroed input is never written, so the 2 GiB stay untouched zero pages and the test runs in under a second), plus a cheap variant via BUN_FEATURE_FLAG_SYNTHETIC_MEMORY_LIMIT that covers the same path with a 160 MB input. Both fail on current main (returned:0 / returned:167772160) and pass with this change. The full text-decoder.test.js (127 tests) and blob-oom.test.ts pass with the debug build.


[review] gate passed · iteration 2 · 3 files touched

fails on main (without fix)
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/encoding/text-decoder.test.js
bun test v1.4.0 (bbf4670d7)

test/js/web/encoding/text-decoder.test.js:
(pass) TextDecoder > should not crash on empty text [2463.32ms]
(pass) TextDecoder > should decode ascii text [143.32ms]
(pass) TextDecoder > should decode unicode text [2795.90ms]
(pass) TextDecoder > typedArrays > should decode Uint8Array [3.06ms]
(pass) TextDecoder > typedArrays > should decode Uint16Array [0.46ms]
(pass) TextDecoder > typedArrays > should decode Uint32Array [0.38ms]
(pass) TextDecoder > typedArrays > should decode Int8Array [0.29ms]
(pass) TextDecoder > typedArrays > should decode Int16Array [0.35ms]
(pass) TextDecoder > typedArrays > should decode Int32Array [0.37ms]
(pass) TextDecoder > typedArrays > should decode Float16Array [0.34ms]
(pass) TextDecoder > typedArrays > should decode Float32Array [0.43ms]
(pass) TextDecoder > typedArrays > should decode Float64Array [0.30ms]
(pass) TextDecoder > typedArrays > should decode DataView [0.35ms]
(pass) TextDecoder > typedArrays > should decode BigInt64Arra
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (b70c1fc55)

test/js/web/encoding/text-decoder.test.js:
(pass) TextDecoder > should not crash on empty text [6.33ms]
(pass) TextDecoder > should decode ascii text [7.68ms]
(pass) TextDecoder > should decode unicode text [97.49ms]
(pass) TextDecoder > typedArrays > should decode Uint8Array [0.09ms]
(pass) TextDecoder > typedArrays > should decode Uint16Array
(pass) TextDecoder > typedArrays > should decode Uint32Array
(pass) TextDecoder > typedArrays > should decode Int8Array
(pass) TextDecoder > typedArrays > should decode Int16Array
(pass) TextDecoder > typedArrays > should decode Int32Array
(pass) TextDecoder > typedArrays > should decode Float16Array
(pass) TextDecoder > typedArrays > should decode Float32Array
(pass) TextDecoder > typedArrays > should decode Float64Array
(pass) TextDecoder > typedArrays > should decode DataView
(pass) TextDecoder > typedArrays > should decode BigInt64Array
(pass) TextDecoder > typedArrays > should decode BigUint64Array
(pass) TextDecoder > typedArrays > DOMJIT call [25.86ms]
(pass) TextDecoder > should decode unicode text with multiple consecutive emoji [10.97ms]
(pass) TextDecoder > coerces the fatal fl
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/encoding/text-decoder.test.js
bun test v1.4.0 (bbf4670d7)

test/js/web/encoding/text-decoder.test.js:
(pass) TextDecoder > should not crash on empty text [2480.51ms]
(pass) TextDecoder > should decode ascii text [143.63ms]
(pass) TextDecoder > should decode unicode text [2809.08ms]
(pass) TextDecoder > typedArrays > should decode Uint8Array [2.66ms]
(pass) TextDecoder > typedArrays > should decode Uint16Array [0.48ms]
(pass) TextDecoder > typedArrays > should decode Uint32Array [0.35ms]
(pass) TextDecoder > typedArrays > should decode Int8Array [0.31ms]
(pass) TextDecoder > typedArrays > should decode Int16Array [0.32ms]
(pass) TextDecoder > typedArrays > should decode Int32Array [0.37ms]
(pass) TextDecoder > typedArrays > should decode Float16Array [0.32ms]
(pass) TextDecoder > typedArrays > should decode Float32Array [0.40ms]
(pass) TextDecoder > typedArrays > should decode Float64Array [0.32ms]
(pass) TextDecoder > typedArrays > should decode DataView [0.32ms]
(pass) TextDecoder > typedArrays > should decode BigInt64Arra
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 636ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/124] gen cpp.rs (cppbind)
[1/124] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m bun_brotli v0.0.0 (/workspace/bun/src/brotli)
�[1m�[92m   Compiling�[0m bun_output v0.0.0 (/workspace/bun/src/output)
�[1m
... (truncated)
diff hotspot
src/jsc/bindings/bindings.cpp             | 10 ++++---
 src/jsc/bindings/helpers.h                | 34 +++++++++++++++++++++++-
 test/js/web/encoding/text-decoder.test.js | 43 +++++++++++++++++++++++++++++++
 3 files changed, 83 insertions(+), 4 deletions(-)

gate history · 3 passed · 0 rejected · iteration 2

evidence per changed file
file                                       reads  edits  tests
src/jsc/bindings/bindings.cpp                  4      7      0
src/jsc/bindings/helpers.h                     4      8      0
test/js/web/encoding/text-decoder.test.js      2      5      0

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR adds JavaScript string-length validation, throws ERR_STRING_TOO_LONG for oversized conversions, propagates conversion exceptions in bindings, and adds TextDecoder.decode regression tests for physical and synthetic oversized inputs.

Changes

String length safety

Layer / File(s) Summary
String conversion guards and regression tests
src/jsc/bindings/helpers.h, test/js/web/encoding/text-decoder.test.js
toJSStringGC checks oversized strings and allocation failures. It throws the corresponding errors and returns nullptr. TextDecoder tests cover large inputs and synthetic memory limits.
Binding exception propagation
src/jsc/bindings/bindings.cpp
fromEntries checks conversion exceptions before property insertion. ZigString__toValueGC uses toJSStringGC for GC-managed conversion.

Possibly related PRs

  • oven-sh/bun#37025: Adds related exception-safe handling for JavaScript value and string conversions in bindings.cpp.
  • oven-sh/bun#37215: Covers related string-length error handling in bindings.cpp and toJSStringGC.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly and concisely describes the primary change: TextDecoder now throws ERR_STRING_TOO_LONG for oversized output.
Description check ✅ Passed The description explains the problem, cause, fix, scope, and verification results, covering the template requirements despite different section headings.

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

@github-actions github-actions Bot added the claude label Aug 8, 2026
@robobun

robobun commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 10:20 PM PT - Aug 8th, 2026

@robobun, your commit bbf4670 has 1 failures in Build #90770 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 37216

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

bun-37216 --bun

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. fs.readFileSync crashes with SIGTRAP on macOS when file exceeds JS string limit (~2GB) #26323 - fs.readFileSync(path, "utf-8") on a ~2.5 GB ASCII file lands in the same unguarded 2^31..2^32-1 length window: it reaches create_external_globally_allocated_latin1, whose String::max_length() check was backed by STRING_ALLOCATION_LIMIT (u32::MAX), so the length passed into WTF and trapped instead of raising a catchable error — this PR clamps max_length() to WTF_STRING_MAX_LENGTH (i32::MAX). Note: the error surfaced there is a sys ENOMEM rather than Node's ERR_STRING_TOO_LONG, so this may fix the crash without fully matching Node's error code.

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

Fixes #26323

🤖 Generated with Claude Code

@robobun

robobun commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

Verified the #26323 connection on Linux x64 with a 2147483649-byte ASCII file: current main dumps core in fs.readFileSync(path, "utf-8"), and with this change it throws a catchable ENOMEM error instead, because the oversized conversion now returns a dead string before the length reaches WTF. Node throws ERR_STRING_TOO_LONG for the same case, so the fs error code still differs from Node; that alignment is a small follow-up in the fs string conversion, separate from the crash fix. Added Fixes #26323 to the description.

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

Beyond the inline nit, I also checked: the remaining 2^32-1 characters assertions in blob-oom.test.ts are for the JSON-parse message (bindings.cpp:2534), not the create-string message, so they're unaffected. ZigString__toValueGC newly throwing is consistent with to_js's other branch (to_external_value), which already threw + returned .ZERO, so callers already handle that shape.

Extended reasoning...

Checked that the error-message change didn't miss any test assertions (grep for 2^32-1 characters in test/ — remaining hits are the untouched JSON-parse path), and that making ZigString__toValueGC throwing doesn't introduce a new caller contract (the sibling to_external_value arm of ZigStringJsc::to_js already had throw-and-return-ZERO semantics). Not approving because the change touches core string→JS conversion and String::max_length() semantics used across many paths; the inline nit plus a human look at CI is appropriate.

Comment thread src/jsc/bindings/helpers.h
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Throw ERR_STRING_TOO_LONG instead of aborting for 2 GiB to 4 GiB strings #37215 - Same root-cause fix for 2 GiB–4 GiB strings: identical String::max_length() clamp to a new WTF_STRING_MAX_LENGTH const, identical >=> flips in the three create_external* guards, and the same "2^32-1" → "2147483647" error message change across the same files.

🤖 Generated with Claude Code

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@robobun close or rebase

…string

Decoding 2^31 .. 2^32-1 bytes of ASCII with TextDecoder.decode() returned
the empty string with no error: Zig::toStringCopy maps a failed string
creation (length over WTF::StringImpl::MaxLength, or allocation failure)
to a null WTF::String, and jsString() turns that into the empty string.

Zig::toJSStringGC (and with it ZigString__toValueGC, i.e. ZigString.toJS)
now throws ERR_STRING_TOO_LONG when the input is over the length limit
and an out-of-memory error when allocation fails, instead of silently
returning "". The non-UTF-8 branches of the single-argument
Zig::toStringCopy now check the synthetic allocation limit like the
other helpers, and JSC__JSValue__fromEntries checks for the exception
before putDirect.

The sibling guards on the external-string paths (2 GiB to 4 GiB aborts)
are fixed separately in #37215.
@robobun
robobun force-pushed the farm/625ff9f2/textdecoder-string-too-long branch from dfdcd88 to e926765 Compare August 9, 2026 04:20
Comment thread src/jsc/bindings/helpers.h Outdated
Comment thread src/jsc/bindings/helpers.h
Comment thread src/jsc/bindings/helpers.h Outdated
@robobun

robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased and narrowed. This PR and #37215 were opened minutes apart for sibling bugs with overlapping fixes; this one now contains only the parts #37215 does not cover: the silent-empty-string path (Zig::toJSStringGC / ZigString__toValueGC now throw on failed string creation, plus the missing synthetic-limit check in the single-argument Zig::toStringCopy and an exception check in fromEntries) and the TextDecoder tests. The String::max_length() cap, the >= to > guard changes, the error message updates, and the readFileSync abort fix (#26323) stay in #37215. The two diffs no longer share any hunks and merge independently in either order.

@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/bindings/helpers.h`:
- Around line 261-271: Update the toStringCopy failure handling to distinguish
decoded UTF-16 length from the input UTF-8 byte length before selecting the
exception. In the wtfString null branch, use the conversion status or retained
decoded-length result so inputs whose decoded output is within the limit report
out-of-memory, while genuinely over-limit strings still use
Bun::ERR::STRING_TOO_LONG.

In `@test/js/web/encoding/text-decoder.test.js`:
- Around line 965-973: Remove the explicit 30-second timeout from the test
containing the 2**31-byte ASCII input. Measure its runtime in debug and ASAN
builds; if it exceeds the default test budget, reduce or conditionally gate the
physical-input case while preserving the synthetic regression coverage.
- Around line 966-972: Update both subprocess tests at
test/js/web/encoding/text-decoder.test.js:966-972 and :978-984 to configure
Bun.spawn with stderr: "pipe" and concurrently await proc.stderr.text()
alongside proc.stdout.text() and proc.exited. Keep assertions based on stdout
and exitCode, without requiring stderr to be empty.
🪄 Autofix

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: 31586b07-cd8b-4293-8da2-52499210359c

📥 Commits

Reviewing files that changed from the base of the PR and between 0c35ccc and e926765.

📒 Files selected for processing (3)
  • src/jsc/bindings/bindings.cpp
  • src/jsc/bindings/helpers.h
  • test/js/web/encoding/text-decoder.test.js

Comment thread src/jsc/bindings/helpers.h
Comment thread test/js/web/encoding/text-decoder.test.js Outdated
Comment thread test/js/web/encoding/text-decoder.test.js
…eout

For UTF-8-tagged input the byte length can exceed the limit while the
decoded UTF-16 length is within it; recompute the decoded length in the
failure path so a genuine allocation failure reports out-of-memory
rather than ERR_STRING_TOO_LONG. Pipe and drain stderr in the new
subprocess tests and rely on the default test timeout (the 2 GiB case
runs in about a second).
Comment thread src/jsc/bindings/helpers.h
Comment thread test/js/web/encoding/text-decoder.test.js Outdated
Comment thread test/js/web/encoding/text-decoder.test.js
The Uint8Array reservation itself can fail there, which would report an
allocation error instead of exercising the decode path. Same gate as
blob-oom.test.ts and fs-oom.test.ts; the synthetic-limit variant keeps
the coverage on small machines.

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

All prior review feedback (mine and CodeRabbit's) is addressed in b70c1fc and bbf4670, and this pass found no new issues. Deferring to a human because the fix changes the contract of Zig::toJSStringGC / ZigString__toValueGC — a core string-conversion helper reached from ZigString::to_js across the Rust codebase — from never-throws to throws-and-returns-nullptr.

What was reviewed:

  • toJSStringGC's three call sites (ZigString__toValueGC, JSC__JSValue__fromEntries, Bun::toJS's ZigString arm) — each now sees the exception before using the result.
  • The forward declaration of Bun::ERR::STRING_TOO_LONG matches ErrorCode.h:112 exactly.
  • The 2 GiB test now gates on it.skipIf(os.totalmem() < 10 * 1024 ** 3) and both subprocess tests pipe/drain stderr.
Extended reasoning...

Overview

The PR fixes TextDecoder.decode() silently returning "" when the decoded string would be 2^31..2^32-1 characters. It touches src/jsc/bindings/helpers.h (adds a length check to toStringCopy, rewrites toJSStringGC to throw ERR_STRING_TOO_LONG/OOM on a null WTF::String instead of passing it to jsString(), and forward-declares Bun::ERR::STRING_TOO_LONG to avoid an include cycle), src/jsc/bindings/bindings.cpp (ZigString__toValueGC delegates to toJSStringGC; JSC__JSValue__fromEntries checks the exception before putDirect), and adds two subprocess tests to text-decoder.test.js.

Security risks

None identified. The change tightens behavior (fail loud instead of silent empty string) on a boundary condition. No new user-controlled data reaches parsing or allocation paths that weren't already reachable.

Level of scrutiny

Medium-high. helpers.h is included transitively by essentially every C++ binding file, and ZigString__toValueGC backs ZigString::to_js which is called throughout the Rust runtime (src/jsc/lib.rs:1633, src/runtime/node/node_os.rs:103, etc.). The contract change from "always returns a valid JSValue (possibly empty string)" to "may throw and return encoded-nullptr" is correct — it matches what Bun::toJS's Dead arm already does at BunString.cpp:176 — but it means any Rust caller that previously relied on an infallible return now sees JSValue(0) with a pending exception on oversize input. In practice this only fires above 2^31 characters (or the synthetic limit), so existing callers passing normal-sized strings are unaffected, and the old behavior (silent "") was the bug being fixed. Still, a maintainer should confirm they're comfortable with this contract change landing without auditing every to_js call site.

Other factors

This is my third look at the PR. My earlier findings (missing skipIf memory gate on the 2 GiB test; stderr not piped) and CodeRabbit's (UTF-8 byte-length vs. decoded-length classification; per-test timeout; stderr draining) are all resolved in the current diff. The comment-cop flags on long comments were also addressed. The forward declaration in helpers.h:32 matches the real declaration in ErrorCode.h:112. The gate evidence in the PR description shows the test file failing on main and passing with the fix under both debug+ASAN and release. The diff no longer overlaps with #37215. Nothing new surfaced this run; the deferral is purely because of the breadth of the helper being changed, not any specific concern.

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

All prior feedback (mine, CodeRabbit's, and comment-cop's) has been addressed in b70c1fc and bbf4670 — the toJSStringGC sibling now carries the fix, the 2 GiB test is gated on os.totalmem(), stderr is piped and drained, the per-test timeout is gone, and the UTF-8 too-long/OOM classification uses the decoded length. No new issues found this pass. Deferring to a maintainer only because this changes the contract of Zig::toJSStringGC (now throws and returns nullptr) in a header transitively included across the JSC bindings, and adds a forward declaration of Bun::ERR::STRING_TOO_LONG into helpers.h to avoid a circular include — both look correct, but worth a maintainer glance.

Extended reasoning...

Overview

The PR fixes TextDecoder.decode() silently returning "" for outputs whose length falls in [2^31, 2^32-1]. It touches three files: src/jsc/bindings/helpers.h (~34 lines: toJSStringGC now throws ERR_STRING_TOO_LONG/OOM on a null WTF::String and returns nullptr; single-arg toStringCopy gains the synthetic-limit check its siblings already have; a forward decl of Bun::ERR::STRING_TOO_LONG avoids a circular include), src/jsc/bindings/bindings.cpp (~10 lines: ZigString__toValueGC delegates to toJSStringGC; JSC__JSValue__fromEntries materializes the value and checks the exception before putDirect), and two new subprocess tests in text-decoder.test.js.

Security risks

None. This tightens a silent-failure path into a thrown error; no auth, crypto, or untrusted-input parsing is touched.

Level of scrutiny

Medium-high. helpers.h is included transitively by essentially every JSC binding TU, and toJSStringGC is called from Bun::toJS's ZigString arm (BunString.cpp:194) and ZigString__toValueGC, which back bun_core::String::to_js and ZigString::to_js on the Rust side. Changing it from "returns empty JSString" to "throws and returns nullptr" is the correct JSC convention (matching the existing Dead-tag arm at BunString.cpp:176-178), and the PR already updated the one caller (fromEntries) that would have putDirect'd an empty JSValue — but a maintainer should confirm they're comfortable with the forward-declaration-in-helpers.h approach and the broader contract change.

Other factors

  • All eight prior review threads (my two, CodeRabbit's three, comment-cop's three) are resolved and verified against the current diff.
  • The forward declaration at helpers.h:32 matches ErrorCode.h:112 exactly.
  • Tests are properly gated (it.skipIf(os.totalmem() < 10 * 1024 ** 3)), pipe/drain stderr concurrently, use bunEnv, and have a cheap synthetic-limit variant so coverage survives on small runners.
  • PR evidence shows fail-without-fix / pass-with-fix on both debug+ASAN and release.
  • Jarred engaged ("close or rebase") and the author rebased/narrowed to remove overlap with #37215; no maintainer approval yet.

@robobun

robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

CI on bbf4670 finished 195/196 green. The one failed job is "step failed outside runner" on the darwin 26 aarch64 lane, a pre-existing infra failure (the machine is currently unreachable); it also occurs on main and is unrelated to this change. The new TextDecoder tests passed on every lane that ran them. All review feedback is addressed and resolved; this is ready for a maintainer.

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.

2 participants