Skip to content

node: render the Rust validators' ERR_INVALID_ARG_TYPE messages like Node - #38663

Open
robobun wants to merge 1 commit into
mainfrom
farm/8a1bb495/node-validators-invalid-arg-type-message
Open

robobun wants to merge 1 commit into
mainfrom
farm/8a1bb495/node-validators-invalid-arg-type-message

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • Every argument type error thrown by the Rust port of Node's validators has a Bun-specific message. path.join(1) throws The "paths[0]" property must be of type string, got number; Node v26.3.0 throws The "path" argument must be of type string. Received type number (1). The code (ERR_INVALID_ARG_TYPE) is right, only the message differs.
  • Cause: throw_err_invalid_arg_type in src/runtime/node/util/validators.rs formats the message by hand ("property", , got <typeof>) instead of using the shared renderer. It is behind validate_string, validate_int32, validate_uint32, validate_boolean, validate_object, the string/boolean array validators, and the mode errors of fs.chmod/fs.fchmod, so node:path, util.parseArgs, net.BlockList, crypto.scrypt and others all produce the Bun wording. validate_array (must be an instance of Array, got ...) and validate_string_enum (options.x.type must be one of: boolean|string) hand-format their messages the same way.
  • Three smaller mismatches in the same area: the string/boolean array validators pass the array instead of the offending element to the error, path.join() names its argument paths[i] (Node's join() uses path; only resolve() uses paths[i]), and path.basename() names its second argument ext (Node: suffix).
  • The shared Rust renderer itself (JSGlobalObject::throw_invalid_argument_type_value) always says argument. Node says property when the name is dotted, so fs.rmSync(p, { maxRetries: "a" }) and new SocketAddress({ address: 1 }) said The "options.maxRetries" argument ... where Node says property.

Fix

  • validators.rs: throw_err_invalid_arg_type delegates to throw_invalid_argument_type_value; validate_array and validate_string_enum go through throw_invalid_argument_type_value2 with Node's phrases (an instance of Array, ('string|boolean')); the array validators report the element; get_type_name (the only remaining user of the old wording) is deleted. OptionValueType::VALUES_INFO is reordered to string|boolean, the order Node passes to validateUnion.
  • JSGlobalObject.rs: the three throw_invalid_argument_type_value* renderers format the name through a small Display type that applies Node's rule ("x" argument, "a.b" property, a name ending in argument verbatim). This is the same rule Bun::Message::addParameter already applies for C++ callers, so Rust and C++ errors now agree.
  • path.rs: join() validates each argument as path; basename() validates the suffix as suffix.
  • Why this is right: every message in this PR is byte-identical to Node v26.3.0 (the version process.versions.node reports). A 196-call matrix over path.{posix,win32} x 14 functions x 7 bad values, plus the parseArgs, fs, scrypt, BlockList and SocketAddress cases below, diffs clean against a real node binary; 192 of the 196 path lines differed before.
  • Intentionally left alone: JSGlobalObject::throw_invalid_property_type (the JSValue option getters behind fs.mkdir's recursive and Bun's own option bags) still uses the old wording; that is a different helper with mostly Bun-API callers and needs dotted-name plumbing (options.recursive) to match Node. parseArgs's ERR_INVALID_ARG_VALUE messages and fs.rm's hand-written recursive/force messages are different templates and are not touched. Section 1 of the draft node: restore 9 concealed-gap tests to verbatim upstream (arg-type messages, win32 paths, parseEnv) #35423 (and the renderer change in node: Node-accurate ERR_INVALID_ARG_TYPE / ERR_INVALID_THIS / ERR_MISSING_ARGS; resync test/common and 117 vendored tests with v26.3.0 #35429) covers the message shape part of this; this PR is the focused version and also fixes the array element, join, basename and validateUnion mismatches, which those drafts do not.
  • Tests (all fail on the current release, pass with this change):
    • test/js/node/path/path.test.js: exact messages for every path function on both namespaces (path, paths[i], from/to, suffix, pathObject).
    • test/js/node/path/parse-format.test.js, resolve.test.js: existing assertions on the old wording updated (the former carried a TODO describing this divergence).
    • test/js/node/util/parse_args/parse-args.test.mjs: validate_array, validate_boolean, validate_object, validate_string, validate_string_enum, the array element validators, and argument vs property naming.
    • test/js/node/fs/fs.test.ts: chmodSync/fchmodSync without a mode (direct throw_err_invalid_arg_type callers); rmSync retry options (dotted name through the shared renderer).
    • test/js/node/crypto/scrypt.test.ts: validate_int32 (keylen) and validate_uint32 (N, cost).
    • test/js/node/net/socketaddress.spec.ts: dotted name from a direct caller of the shared renderer.
    • The 35 vendored Node tests for path, parseArgs, BlockList, SocketAddress, scrypt, chmod, rm and zlib constructors still pass; grep for the old must be of type ..., got wording finds no remaining assertions that this change affects (the leftovers are the throw_invalid_property_type callers above).

Background

  • ERR_INVALID_ARG_TYPE(name, expected, actual) in Node's lib/internal/errors.js renders The <subject> must be <expected>. Received <determineSpecificType(actual)>. The subject is "name" argument, or "name" property when the name contains a . (it is then an option path like options.foo). expected renders as of type string for a primitive type name, an instance of Array for a class name, and verbatim for anything else (validateUnion passes ('string|boolean')).
  • determineSpecificType is Node's description of the received value: type number (1), type string ('x'), an instance of Object, null, undefined. Bun implements it once in C++ (ErrorCode.cpp) and exposes it to Rust as JSGlobalObject::determine_specific_type, which the shared renderers already used.
  • src/runtime/node/util/validators.rs is the Rust port of Node's lib/internal/validators.js, used by the natively implemented parts of node:path, util.parseArgs, net.BlockList, crypto.scrypt, dgram and zlib.
Before / after for a few of the calls (right column is also Node v26.3.0's output)
call before after
path.join(1) The "paths[0]" property must be of type string, got number The "path" argument must be of type string. Received type number (1)
path.join("a", 2n) The "paths[1]" property must be of type string, got bigint The "path" argument must be of type string. Received type bigint (2n)
path.resolve("a", null) The "paths[1]" property must be of type string, got object The "paths[1]" argument must be of type string. Received null
path.basename("a", 1) The "ext" property must be of type string, got number The "suffix" argument must be of type string. Received type number (1)
path.format([]) The "pathObject" property must be of type object, got array The "pathObject" argument must be of type object. Received an instance of Array
parseArgs({ args: "x" }) The "args" property must be an instance of Array, got string The "args" argument must be an instance of Array. Received type string ('x')
parseArgs(... default: ["a", 1]) The "options.alpha.default[1]" property must be of type string, got array The "options.alpha.default[1]" property must be of type string. Received type number (1)
parseArgs(... type: "nope") options.alpha.type must be one of: boolean|string The "options.alpha.type" property must be ('string|boolean'). Received type string ('nope')
fs.chmodSync(p) The "mode" property must be of type number, got undefined The "mode" argument must be of type number. Received undefined
fs.rmSync(p, { maxRetries: "a" }) The "options.maxRetries" argument must be of type number. Received type string ('a') The "options.maxRetries" property must be of type number. Received type string ('a')
new BlockList().addSubnet("1.1.1.1", "x") The "prefix" property must be of type number, got string The "prefix" argument must be of type number. Received type string ('x')
scryptSync("p", "s", "x") The "keylen" property must be of type number, got string The "keylen" argument must be of type number. Received type string ('x')

validators.rs hand-formatted `The "x" property must be of type T, got
<typeof>`. Route it through the shared renderer so it produces Node's
`The "x" argument must be of type T. Received <specific type>`, and make
the shared renderer say "property" for dotted names like Node does.

Also: validateArray/validateUnion use Node's templates, the string and
boolean array validators report the offending element instead of the
array, path.join() names its argument "path" and path.basename() names
its second argument "suffix", both as in Node.
@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 12:47 PM PT - Aug 14th, 2026

@robobun, your commit 647f89a is building: #96508

@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 89fc55f6-2986-4f76-b329-7153917b1b8f

📥 Commits

Reviewing files that changed from the base of the PR and between 9cff2a1 and 647f89a.

📒 Files selected for processing (11)
  • src/jsc/JSGlobalObject.rs
  • src/runtime/node/path.rs
  • src/runtime/node/util/parse_args_utils.rs
  • src/runtime/node/util/validators.rs
  • test/js/node/crypto/scrypt.test.ts
  • test/js/node/fs/fs.test.ts
  • test/js/node/net/socketaddress.spec.ts
  • test/js/node/path/parse-format.test.js
  • test/js/node/path/path.test.js
  • test/js/node/path/resolve.test.js
  • test/js/node/util/parse_args/parse-args.test.mjs

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

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced and fixed.

Reproduced on Bun 1.4.0 and main by diffing the ERR_INVALID_ARG_TYPE messages of path.*, util.parseArgs, fs.chmodSync/rmSync, crypto.scryptSync, net.BlockList and net.SocketAddress against node v26.3.0 (${e.code}: ${e.message} for each call). With this branch the same scripts produce byte-identical output to Node; the new assertions in the touched test files fail on the release build and pass with the debug build of this branch.

Overlaps with section 1 of the draft #35423 (same message-shape fix, plus more); see the PR description for what this PR additionally covers.

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

I reviewed this and didn't find any bugs. Since it changes the shared throw_invalid_argument_type_value* renderers used by ~100 call sites across node/webcore/socket/zlib/crypto, a human look (and green CI) would still be worthwhile before landing.

What was reviewed:

  • InvalidArgTypeName matches Bun::Message::addParameter in ErrorCode.cpp exactly ( argument suffix → verbatim, dotted → property, else argument).
  • Grepped for stale assertions on the old , got <type> wording and on "options.X" argument — the remaining hits go through throw_invalid_property_type or hand-formatted sqlite messages, neither touched here.
  • path.join hot-loop simplification is fine: the removed cold-path helper existed to avoid building format_args!("paths[{i}]") per iteration; the new name is a static &str so that concern is gone, and throw_err_invalid_arg_type is still #[cold].
  • Array-element fix (item instead of value) and get_type_name deletion look correct.
Extended reasoning...

Overview

The PR routes the Rust validators.rs error paths (throw_err_invalid_arg_type, validate_array, validate_string_enum, and the string/boolean array validators) through the shared JSGlobalObject::throw_invalid_argument_type_value{,2,_one_of} renderers, and teaches those three renderers Node's argument-vs-property naming rule via a new InvalidArgTypeName Display type. It also fixes three argument-name mismatches in node:path (join → "path", basename suffix → "suffix"), reorders OptionValueType::VALUES_INFO to match Node's validateUnion order, makes the array validators report the offending element rather than the whole array, and deletes the now-dead get_type_name. Seven test files add or tighten exact-message assertions.

Security risks

None. All changes are on cold error-message paths; no parsing, allocation, or control-flow changes on the happy path.

Level of scrutiny

Medium. The logic is small (~50 net source lines), but throw_invalid_argument_type_value* has ~100 call sites across node_fs, SocketAddress, zlib, crypto, h2_frame_parser, socket_body, ffi_body, etc. Every caller passing a dotted name now says "property" instead of "argument", which is Node-correct and matches the existing C++ addParameter, but any missed exact-message test assertion in the vendored Node suite or Bun's own tests would surface as a CI failure. I spot-checked: the surviving "options.X" argument must be a ... assertions are hand-formatted in NodeSqlite.cpp (different template, unaffected), and the surviving must be of type ..., got assertions go through throw_invalid_property_type (explicitly left alone per the PR description). handle.fd in node_cluster_binding.rs will flip to "property", which is Node-correct.

Other factors

The path.join change removes an explicit #[cold] #[inline(never)] cold helper. That helper's stated purpose was avoiding a 48-byte fmt::Arguments build per iteration for format_args!("paths[{i}]"); with a static "path" the concern disappears and throw_err_invalid_arg_type remains #[cold], so this is a justified simplification rather than a perf regression. CI (#96508) was still building when I reviewed; given the breadth of callers, waiting for green CI plus a human glance seems prudent. Test coverage is thorough (path × posix/win32 × 14 fns × 7 values, plus parseArgs/fs/scrypt/SocketAddress) and the PR description documents byte-equality with Node v26.3.0.

This branch has not been deployed

No deployments
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