node:fs: report mkdir/readdir/rm boolean option type errors like node - #39328
node:fs: report mkdir/readdir/rm boolean option type errors like node#39328robobun wants to merge 1 commit into
Conversation
…rties like node
fs.mkdir and fs.readdir validated `recursive` (and readdir's
`withFileTypes`) through JSValue::get_boolean_strict, whose message is
`The "recursive" property must be of type boolean, got string`, and fs.rm
hand-formatted `The "options.recursive" property must be of type boolean.`
without node's `Received ...` tail. Node validates all of these with
validateBoolean(value, 'options.<name>'), so the message is
`The "options.recursive" property must be of type boolean. Received type
string ('x')`.
Route these sites (and rm's non-object `options` error) through
JSGlobalObject::throw_invalid_argument_type_value with the dotted name,
and teach that renderer (plus the `..._value2` / `..._one_of` variants)
node's subject rule: a dotted name is a "property", a plain name an
"argument". rm now checks `force` before `recursive`, the order node's
validateRmOptions uses.
test-fs-mkdir.js goes back to upstream's assertion text.
|
Warning Review limit reached
Next review available in: 26 minutes Limit details: You’ve used all 5 included reviews currently available under your plan. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
Comment |
|
Status: reproduced, fixed, CI green (Buildkite build 99292, all lanes passed); ready for review.
|
|
Heads-up on the readdir part of this PR: #39330 changes which values |
There was a problem hiding this comment.
LGTM — error-message wording change with comprehensive coverage. The inline nit about rmdir's force-before-recursive order is real but non-blocking (only observable when both options are bad on rmdir, where force isn't a documented option and Bun validating it at all is already a tracked pre-existing divergence).
Extended reasoning...
Overview
This PR aligns the ERR_INVALID_ARG_TYPE error text for boolean options in fs.mkdir/readdir/rm/rmdir (all sync/callback/promise entry points) with Node v26.3.0's exact wording. Two mechanical pieces: a new InvalidArgTypeName formatter in src/jsc/JSGlobalObject.rs implementing Node's subject rule (dotted name → "x.y" property, trailing argument verbatim, otherwise "x" argument) applied to the three throw_invalid_argument_type_value* renderers; and a small validate_boolean_option helper in node_fs.rs that replaces get_boolean_strict at four call sites and the hand-formatted messages in RmDir::from_js_impl. force is now validated before recursive to match validateRmOptions. Tests: an 11-case block in fs.test.ts covering all three entry-point styles plus the un-special-casing of the vendored test-fs-mkdir.js.
Security risks
None. Pure error-message text change; no new syscalls, no allocation-size arithmetic, no untrusted-input parsing beyond what already existed.
Level of scrutiny
Medium-low. The node_fs.rs hunks are localized and I confirmed val.get(ctx, name) returns None for undefined/missing (JSValue.rs:1136), so acceptance semantics for mkdir/readdir are unchanged vs. the old get_boolean_strict path — only the error text differs. The shared-renderer hunk has broader reach, but the PR enumerated every existing dotted-name caller (options.retryDelay/maxRetries, net.SocketAddress, http2 request options, handle.fd, compress.encoding, socket fault-injection rules), verified none pinned argument in a test, and ran the affected suites. Plain (non-dotted) names render byte-identically to before. The same renderer hunk is already carried by three other open PRs.
Other factors
The one inline finding is a nit: the force-before-recursive reorder also applies to rmdir via the shared parser, so rmdirSync(d, { recursive: 'x', force: 'y' }) now names force where Node names recursive. Impact is negligible — same error code, only the property named in the message differs, and only when both are non-boolean on an API where force isn't documented; the description already scopes "rmdir validates force at all" out as a separately-tracked delta. Not worth blocking on. Test quality is high: hermetic tempDirs, exact { name, code, message } assertions, negative checks that nothing was created/removed, and the vendored Node test now asserts upstream text instead of a Bun-specific branch.
|
On the Node's |
|
Updated 1:06 AM PT - Aug 16th, 2026
✅ @robobun, your commit 2a15b56a9a7dbc4d95a172d50bd4436603dce968 passed in 🧪 To try this PR locally: bunx bun-pr 39328That installs a local version of the PR into your bun-39328 --bun |
Problem
fs.mkdirSync(p, { recursive: "x" })throwsERR_INVALID_ARG_TYPEwithThe "recursive" property must be of type boolean, got string. Node v26.3.0 throws the same code withThe "options.recursive" property must be of type boolean. Received type string ('x').fs.mkdirandfs.promises.mkdirshare the parser, as dofs.readdir/readdirSync/promises.readdirforrecursiveandwithFileTypes.fs.rmSync(p, { recursive: "x" })and{ force: "x" }(alsofs.rm,fs.promises.rm) sayThe "options.recursive" property must be of type boolean.with noReceived ...half;fs.rmSync(p, "x")andfs.rmdirSync(d, "x")sayThe "options" argument must be of type object.without it either.rmSync(p, { recursive: "x", force: "y" })reportsrecursive; node reportsforce.src/runtime/node/node_fs.rs:Mkdir::from_js(:3395) andReaddir::from_js(:3488, :3491) read the option throughJSValue::get_boolean_strict, whose error is Bun's..., got <typeof>template with the bare key;RmDir::from_js_impl(:3307-3323, :3346) formats its messages by hand. Node validates every one of these withvalidateBoolean(value, 'options.<name>')(andvalidateObject(options, 'options')).JSGlobalObject::throw_invalid_argument_type_value(src/jsc/JSGlobalObject.rs:559), hardcodes"<name>" argument, so it could not produce"options.recursive" propertyeither.test-fs-mkdir.jscarried atypeof Bunbranch with the old wording andtest-fs-rm.jsmatches the message with a prefix regex.Fix
node_fs.rs: a smallvalidate_boolean_option(node'svalidateBoolean) throws throughthrow_invalid_argument_type_valuewith the dotted name;mkdir's andreaddir'srecursive,readdir'swithFileTypes, andrm/rmdir'sforceandrecursiveuse it.rm's non-objectoptionserror goes through the same renderer.forceis checked beforerecursive, the order of node'svalidateRmOptions.JSGlobalObject.rs: the threethrow_invalid_argument_type_value*renderers format the name throughInvalidArgTypeName, node's subject rule ("x" argument,"a.b" property, a name ending inargumentverbatim), which is the ruleBun::Message::addParameterinErrorCode.cppalready applies to every C++ caller. This hunk is byte-identical to the one carried by the open node: render the Rust validators' ERR_INVALID_ARG_TYPE messages like Node #38663, node:fs: report a bad flush option as "options.flush", render dotted names as properties in ERR_INVALID_ARG_TYPE #39266 and node:fs: validate options.signal in readFile, writeFile and watch like node #39274, so these PRs merge in any order; whichever lands first carries it and the others reduce to their call sites.node:fserrors node's text is the contract, and theReceived ...half already comes from the samedetermineSpecificTypethe JS builtins use, so the result is node's exact text. A 160-case matrix (mkdir, readdir, rm, rmdir; sync, callback and promise entry points; 8 non-boolean values each) against a local node v26.3.0 changes 122 lines with this PR, and every line where both runtimes throw now carries identical text; the lines still differing are pre-existing behavioral differences (which valuesreaddirandrmdiraccept, callback-formrmdelivering the error through the callback where node throws synchronously, thestatfamily validating options node ignores), listed below and not touched here.get/get_ownlookups and the sameis_booleantest, sorm's own-keyrecursive: undefinedis still rejected (node's spread-over-defaults semantics) andmkdir's missing orundefinedrecursiveis still the default. Only the text and theforce/recursiveorder change..render differently. The ones passed today areoptions.retryDelay/options.maxRetries(fs.rm),options.address/options.flowlabel(net.SocketAddress),options.silent/endStream/exclusive/parent/weight/signal(http2 request options),handle.fd(cluster), and Bun's owncompress.encoding(fetch) andrule.syscall/rule.action(socket fault injection). Node renders all of its own asproperty; no test in the repo pinnedargumentfor any of them. Plain names render exactly as before.get_boolean_strictitself and itsstat/fstat/statfs/watchFilecallers (bigint,throwIfNoEntry,persistent), which node does not validate at all, so there is no node text to match;validators::validate_boolean(that is node: render the Rust validators' ERR_INVALID_ARG_TYPE messages like Node #38663's change); the behavioral deltas the matrix surfaced, which are tracked separately:readdirrejectsrecursive: null, non-booleanwithFileTypes, and non-booleanrecursiveinpromises.readdirwhere node coerces;rmdirstill validatesforce/retryDelay/maxRetries, which node v26 ignores; and the JSrmwrappers reportERR_FS_EISDIRbefore the options are validated when the target is a directory. The draft node: restore 9 concealed-gap tests to verbatim upstream (arg-type messages, win32 paths, parseEnv) #35423 carries a broader variant of themkdir/readdirrecursivepart (it renamesthrow_invalid_property_typefor all callers); it does not coverwithFileTypes,rm, or theoptionsargument, and the two overlapping hunks rebase trivially in either order.test/js/node/fs/fs.test.ts: newfs boolean options report ERR_INVALID_ARG_TYPE like nodeblock (mkdir, readdir and rm through sync, callback and promise entry points, theReceivedshapes, theforce-before-recursiveorder, theoptionsargument for rm and rmdir, and that the file is still there afterwards) plus the updatedmkdirSync > throws for invalid options: 11 tests, all fail on the released build, all pass with this change. The whole file: 521 pass; the one remaining failure is noted below.test/js/node/test/parallel/test-fs-mkdir.jsnow asserts upstream's text; it fails on the released build and passes here.test-fs-rm.js,test-fs-promises.js,test-fs-readdir.js,test-fs-readdir-recursive.js,test-fs-readdir-types.js,test-fs-rmdir-type-check.jspass.test/js/web/fetch/wasm-streaming.test.ts,test/js/node/child_process/child_process-node.test.js,test/js/node/net/socketaddress.spec.ts,test/js/web/fetch/fetch-compress.test.ts,test-crypto-random.js,test-socketaddress.js,test-http2-client-request-options-errors.jspass (one unrelated timeout below).cargo clippy -p bun_runtime -p bun_jscis clean.Background
ERR_INVALID_ARG_TYPE(name, expected, actual)(lib/internal/errors.js) rendersThe <subject> must be of type <expected>. Received <description of actual>. The subject is"name" argument, unless the name contains a., in which case it is an option path such asoptions.recursiveand renders as"name" property; a name that already ends inargumentis used verbatim. The description of the actual value (type string ('x'),null,an instance of Array) isdetermineSpecificType.ErrorCode.cpp, behind$ERR_INVALID_ARG_TYPEin the JS builtins and the C++ bindings) and the Rust helpers onJSGlobalObjectused by natively implemented modules such asnode:fs. The Rust helpers already call the C++determineSpecificTypefor theReceivedhalf; only the subject rule was missing.fs.rmvalidates its options the way node'svalidateRmOptionsdoes: the caller's own keys are spread over the defaults, thenforce,recursive,retryDelay,maxRetriesare validated in that order. That is why an ownrecursive: undefinedis an error forrmbut not formkdir, and whyforceis reported first when both booleans are bad.Unrelated failures seen while running the suites in this (debug, ASAN) container
fs.test.ts > fs/promises > readdirSync(path, {recursive: true} should work x 100exceeds its 10 s budget here (12 s). Timing the body: the four recursivereaddirSynccalls take 0.4 s; sorting the four 6660-entry arrays takes 7.5 s under the debug build. No code on that path is touched by this PR.socketaddress.spec.ts > does not leak memoryexceeds its 5 s budget here (6.8 s); it constructs 101k valid addresses and never reaches an error renderer.Before / after (right column is also node v26.3.0's output)
mkdirSync(p, { recursive: "x" })The "recursive" property must be of type boolean, got stringThe "options.recursive" property must be of type boolean. Received type string ('x')promises.mkdir(p, { recursive: null })The "recursive" property must be of type boolean, got objectThe "options.recursive" property must be of type boolean. Received nullreaddirSync(d, { recursive: [] })The "recursive" property must be of type boolean, got arrayThe "options.recursive" property must be of type boolean. Received an instance of ArrayreaddirSync(d, { withFileTypes: 1 })The "withFileTypes" property must be of type boolean, got numberThe "options.withFileTypes" property must be of type boolean. Received type number (1)(node coerces this one; Bun keeps rejecting it)rmSync(f, { force: "x" })The "options.force" property must be of type boolean.The "options.force" property must be of type boolean. Received type string ('x')rmSync(f, { recursive: undefined })The "options.recursive" property must be of type boolean.The "options.recursive" property must be of type boolean. Received undefinedrmSync(f, { recursive: "x", force: "y" })The "options.recursive" property must be of type boolean.The "options.force" property must be of type boolean. Received type string ('y')rmSync(f, "x")The "options" argument must be of type object.The "options" argument must be of type object. Received type string ('x')rmSync(f, { maxRetries: "x" })(renderer only)The "options.maxRetries" argument must be of type number. Received type string ('x')The "options.maxRetries" property must be of type number. Received type string ('x')