Conversation
Node's getOptions() asserts the options encoding before getValidatedPath() checks the path, so the encoding error wins when both are invalid. The native fs argument parsers read the path first and threw its error. The parsers still read the path first, because that captures a resizable buffer before an encoding getter can shrink it. When the path is invalid, they now assert the encoding before they report the path error. fs.watch does the same around its URL conversion in JS.
|
Warning Review paused — included plan limit reachedKeep your review moving with free on-demand reviews.
On-demand reviews are free for one more day.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Promotion and pricing detailsOn-demand reviews are free for one more day. After that, they cost $0.25 per reviewed file. Review limit detailsOr wait 3 minutes for your next included review. Limit details: You’ve used all 10 included reviews currently available. Review configuration: ⚙️ Run configurationConfiguration used: Repository: oven-sh/bun/.coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (4)
Comment |
|
Status
|
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Additional findings (outside the current diff — GitHub can't attach inline comments there):
-
🟣
src/runtime/node/node_fs_watcher.rs— fs.watch callers with a valid path, a bogusencodingand a non-booleanpersistent(orverbose) still get the persistent error after merging, while Node reports ERR_INVALID_ARG_VALUE for the encoding. The PR only asserts the encoding on the invalid-path branch (node_fs_watcher.rs:662); the valid-path branch at node_fs_watcher.rs:675-698 validates persistent and verbose before reading encoding. Fix: read and assertencodingbeforepersistent/verbose/recursivein Arguments::from_js so Node's getOptions-first order holds for every invalid option, not only an invalid path.Extended reasoning...
Node's fs.watch runs getOptions(options) first (assertEncoding), then kFSWatchStart validates the path, then validateBoolean(persistent) and validateBoolean(recursive). Bun: watch.ts:168 calls native fs.watch(path, options, listener). Arguments::from_js at node_fs_watcher.rs:655 parses the path; on success or_encoding_error at :662 is a no-op. Line 675 reads persistent and throws 'persistent must be a boolean' at :679 before encoding is read at :695. So fs.watch(dir, { encoding: 'bogus', persistent: 'yes' }) throws the persistent error in Bun and the encoding error in Node. The same applies to verbose at :686. The PR's stated goal is that an invalid encoding wins as in Node, and REVIEW.md requires the whole class (every check that getOptions runs before) be fixed in the same PR. The dismissing finder confirmed the lines are unchanged from base but did not weigh that this is the same class the PR claims to close and the new test matrix only covers invalid paths for watch. Population: any watch caller with two bad options; the new tests would not detect the gap. Remedy: hoist the encoding…
Verification: pre-existing. Trigger:
fs.watch(validDir, { encoding: "bogus", persistent: 1 })(or any truthy non-booleanpersistent/verbose). Mechanism verified in /home/claude/bun/src/runtime/node/node_fs_watcher.rs:or_encoding_errorat :662 only asserts the encoding when the path parse failed — in /home/claude/bun/src/runtime/node/node_fs.rs:3220-3222 `let Err(bun_jsc::JsError::Thrown) = path…
Node's getOptions() runs first in fs.watch, so an invalid encoding wins over an invalid path, an invalid URL, and an invalid persistent, recursive, signal or ignore option. The JS wrapper now asserts the encoding before it does anything else. The native watcher parser no longer needs a change.
|
Updated 6:04 PM PT - Sep 19th, 2026
✅ @robobun, your commit 9237368a09e5ce66f07e8abbd8e0069e4e505e89 passed in 🧪 To try this PR locally: bunx bun-pr 43563That installs a local version of the PR into your bun-43563 --bun |
There was a problem hiding this comment.
I reviewed the latest push (the fs.watch rework that moves the encoding assertion into the JS wrapper and drops the native watcher change) and found no bugs; since this manipulates pending-exception state in the native fs argument parsers, a human look is still worthwhile.
What was reviewed:
or_encoding_errorin src/runtime/node/node_fs.rs: the termination guard runs beforetake_exception, a throwingencodinggetter propagates via?(path error dropped), andthrow_valuere-throws only when nothing is pending — consistent withJSGlobalObject::throw_value/take_exceptionsemantics.peek_atoffsets: 1 for path/options parsers and 2 for writeFile/appendFile (path, data, options); slices start at argument 0 in node_fs_binding.rs, and a callback in the options slot parses as a plain object with noencoding, so the default is used.- watch.ts:
assertEncoding(options?.encoding)after the string/function normalization but before URL conversion; top-level$newRustFunctionmatches the dgram.ts pattern, and the net diff no longer touches node_fs_watcher.rs.
Extended reasoning...
Overview
The PR changes the error-precedence contract of node:fs argument parsing so that an invalid encoding option is reported before an invalid path, matching Node's getOptions()-before-getValidatedPath() order. It touches src/runtime/node/node_fs.rs (six from_js parsers plus the new or_encoding_error helper and a signature change to parse_encoding_arg), adds ArgumentsSlice::peek_at in src/jsc/CallFrame.rs, moves the fs.watch encoding assertion into src/js/internal/fs/watch.ts, and adds a table-driven describe block to test/js/node/fs/fs.test.ts covering sync, callback, and promise forms. The second push removed the earlier node_fs_watcher.rs change and the try/catch around fileURLToPath, so the net diff is now four files.
Security risks
None identified. The change only affects which of two argument-validation errors is surfaced first; it does not add new input processing, allocation, or I/O before validation. The path is still captured before the encoding getter can run, which preserves the resizable-ArrayBuffer capture behavior pinned by the existing test from #40641. The JS-side assertEncoding call goes through a native intrinsic rather than user-overridable machinery.
Level of scrutiny
Moderate. The interesting part is or_encoding_error, which takes a pending exception off the VM, runs user-reachable JS (an encoding getter), and re-throws. I checked that has_pending_termination_exception is consulted before take_exception, that a getter error propagates cleanly via ? while the held path_error JSValue is simply dropped, and that throw_value will not clobber an exception that is already pending. The peek_at index for WriteFile/AppendFile (2) matches the (path, data, options) layout, and slices are initialized from argument 0 in node_fs_binding.rs. This is subtle enough that I would not claim a human never needs to look, but it is small and well-contained.
Other factors
The prior inline note about writeFile/appendFile reporting the path error before an invalid data argument (Node checks data first) was flagged as pre-existing and non-blocking; the latest push does not change that behavior, and it is not restated here. The new tests assert code, name, and that the message names the encoding, across 8 invalid path kinds x 2 option shapes for 26 entry points, and the fs.watch test covers a second invalid option with both a valid and an invalid path. The bug-hunting run ended on dry_streak with no findings. No CODEOWNERS entry covers the changed paths.
Still open from earlier reviews (1):
- Unresolved: 1 minor or pre-existing.
|
On the
|
Problem
fs.readFileSync(123n, "bogus")has an invalid path and an invalid encoding. Bun throwsTypeError [ERR_INVALID_ARG_TYPE]: path must be a string or a file descriptor. Node v26.3.0 throwsERR_INVALID_ARG_VALUEfor the encoding.writeFile,appendFile,readdir,readlink,realpath,mkdtempandwatch, in every form, for every kind of invalid path.src/runtime/node/node_fs.rsthrows the path error at once. Node runsgetOptions(options), which asserts the encoding, beforegetValidatedPath(path).Fix
or_encoding_error). An invalid encoding wins, as in Node.ArrayBufferpath is captured before anencodinggetter can shrink it. A call with a valid path runs as before.fs.watchvalidates aURLpath andignorein its JS wrapper. The wrapper now asserts the encoding first, so it wins over the other options too.test/js/node/fs/fs.test.ts, 27 new tests. All fail on canary 1.4.3. Node v26.3.0 passes all 426 calls of the tables.Background
getOptions(lib/internal/fs/utils.js) is Node's helper foroptions. It turns a string into{ encoding }and asserts the encoding.ArgumentsSliceis the cursor of the fs bindings over the JS arguments. The newpeek_at(index)readsoptionswhile the cursor is on the path.take_exceptionremoves the pending JS exception and returns it.throw_valuethrows it again. The helper holds the path error that way during the encoding check.Notes
Repro (Node v26.3.0 prints
ERR_INVALID_ARG_VALUEfour times, canary 1.4.3 printsERR_INVALID_ARG_TYPE,ERR_INVALID_ARG_TYPE,ERR_INVALID_URL_SCHEME,ENAMETOOLONG):No user reported this. A differential run against Node found it.
Node source.
readlinkSyncshows the order in four lines: https://github.com/nodejs/node/blob/v26.3.0/lib/fs.js#L1761-L1764.getOptionsis at https://github.com/nodejs/node/blob/v26.3.0/lib/internal/fs/utils.js#L324-L345.Kinds of invalid path in the test. A bigint, an object,
null,undefined, a boolean, a symbol, a string with a NUL byte and anhttp:URL, each with"bogus"and with{ encoding: "bogus" }. A bad fd (-1,1.5,NaN), an emptyUint8Arrayand a path that is too long behave the same way under Node and under this branch. I checked those by hand.fs.watchwith a second invalid option. Node reports the encoding beforepersistent,recursive,signalandignoretoo, with a valid path or an invalid one.fs.watch(dir, { encoding: "bogus", ignore: 5 })threw theignoreerror in Bun, and{ encoding: "bogus", persistent: 1 }threw thepersistenterror. One test covers 10 such calls. The native watcher parser is unchanged: the JS wrapper asserts the encoding before it reaches that parser.Probe. 32 calls x 7 invalid paths x 3 invalid encodings is 672 calls. Canary 1.4.3 matches Node on 129. This branch matches on 597. The 75 that still differ:
opendirSync,opendirandfs.promises.opendir(54 calls). Node checks the path first there, and Bun checks the encoding first. The cause is insrc/js/node/fs.ts, not in these parsers. It is tracked as a separate task.fs.promises.watch(21 calls). Node returns an async iterator and reports every error on the firstnext(). Bun throws at the call. fs.promises.watch: implement as a real async generator #34718 covers that.Why the parsers do not assert the encoding first. My first version did that. It broke the test
sync fs calls read a Buffer path captured at call time when an option getter shrinks its resizable ArrayBuffer: theencodinggetter ran before the parser captured the path. This version keeps that test green. It also keeps theencodinggetter at one read per call.Also checked on this branch. A throwing
encodinggetter wins over an invalid path, as in Node. An invalid path with a valid encoding throws the same path error as before, with its stack. The probes run clean underBUN_JSC_validateExceptionChecks=1.Self-review. I reviewed the diff by hand. One concern came up and is fixed: the pending exception can be the termination exception of the VM, which must keep unwinding.
or_encoding_errorreturns at once in that case (has_pending_termination_exception), asfetchand the Valkey client do.Not changed here:
databefore the path.fs.writeFileSync(123n, 456)reports the path in Bun anddatain Node (also the callback and promise forms). Node validatesdatabefore it opens the path. That order does not involve the encoding, so it is not part of this PR.Not changed here.
getOptionsalso validatessignalbefore the path, and it rejects anoptionsvalue that is not a string or an object. #41928 adds that second check toparse_encoding_arg. With this PR it then runs before the path error too, becauseor_encoding_errorcallsparse_encoding_arg. Thesignalcheck keeps its place after the path.utf-16le. Bun'sENCODING_MAPdoes not know Node'sutf-16lealias (#43373 fixes that). Until it lands,fs.readFileSync(123n, "utf-16le")reports the encoding on this branch, and Node reports the path. With a valid path that call already throws the encoding error onmain.Related PRs. #41928, #39330 and #42812 edit the options code of the same parsers. This PR does not touch those lines, only the path line above them. #38432 changes the message of the encoding error. The new tests accept both messages: they check the code, the name, and that the message names the encoding.
Suites run with the debug build.
test/js/node/fs/fs.test.ts(591 pass, 8 skip, 0 fail),test/js/node/watch/fs.watch.test.ts,promises.test.js,dir.test.ts,fs-path-length.test.ts,fs-promises-writeFile-async-iterator.test.ts. 37 vendored Node tests pass, among themtest-fs-assert-encoding-error.js,test-fs-read-file-assert-encoding.js,test-fs-null-bytes.js,test-fs-whatwg-url.js,test-fs-error-messages.js,test-fs-watch-encoding.js,test-fs-mkdtemp.js,test-fs-readdir.js,test-fs-readfile.js,test-fs-realpath.jsandtest-fs-promises.js.abort-signal-leak-read-write-file.test.tshits its 300 s limit on a debug build (100,000 iterations). A 3,000 iteration run of the same loop leaves 3AbortSignalobjects.no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/fs/fs.test.ts