Skip to content

node: pass ERR_INVALID_ARG_TYPE expected types as lists in the C++ bindings - #38690

Open
robobun wants to merge 1 commit into
mainfrom
farm/0512901b/invalid-arg-type-lists
Open

robobun wants to merge 1 commit into
mainfrom
farm/0512901b/invalid-arg-type-lists

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • Several native (C++) entry points throw ERR_INVALID_ARG_TYPE with a different message than Node for the same input, for example:
    • new StringDecoder().write(123): Bun The "buf" argument must be of type Buffer, TypedArray, or DataView., Node ... must be an instance of Buffer, TypedArray, or DataView.
    • process.setuid({}): Bun The "id" argument must be of type number or string., Node ... must be one of type number or string.
    • crypto.createDiffieHellman({}): Bun ... must be of type number, string, ArrayBuffer, Buffer, TypedArray, or DataView., Node ... must be one of type number or string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView.
    • process.emitWarning(123): Bun ... must be of type string or Error., Node ... must be of type string or an instance of Error.
  • Cause: these call sites pass one pre-flattened string ("number or string"_s, "Buffer, TypedArray, or DataView"_s, ...) to the single-string overload of Bun::ERR::INVALID_ARG_TYPE, which always emits "of type " + string (src/jsc/bindings/ErrorCode.cpp, Message::ERR_INVALID_ARG_TYPE). Node's lib/internal/errors.js receives a list and groups it into primitive names and class names.
  • The list renderer already exists (Message::ERR_INVALID_ARG_TYPE(..., ArgList, ...), exposed as the std::span<const ASCIILiteral> overload of ERR::INVALID_ARG_TYPE); process.initgroups was its only caller.
  • Affected sites (verified one by one against Node v26.3.0, full table in the details block): StringDecoder#write, subtle.digest, createDiffieHellman (sizeOrKey, generator), getCipherInfo, generatePrime/generatePrimeSync (options.add, options.rem), hkdf/hkdfSync (salt, info), generateKeyPair("dh") options.prime, Sign#sign, process.emitWarning, process.setuid/seteuid/setgid/setegid, process.setgroups.
  • Four vendored Node tests (test-string-decoder, test-process-uid-gid, test-process-euid-egid, test-process-setgroups) had been edited when they were vendored so that they expect Bun's wording.

Fix

  • Each site now passes the list Node's own source passes (static constexpr ASCIILiteral xs[] = {...}) through the span overload, so the grouping is computed by the same renderer Node uses instead of being hand-written per site. The two class-only lists (StringDecoder#write, dh options.prime) use the existing ERR::INVALID_ARG_INSTANCE, the same helper validateBuffer uses for this exact list.
  • The span overload's arg_name parameter is widened from ASCIILiteral to const WTF::String& so setgroups can pass its computed groups[i] name; ASCIILiteral callers convert implicitly and the existing overloads stay unambiguous (no other overload accepts an array or a span as the expected type).
  • BunProcess.cpp: the initgroups list becomes a shared idTypes used by every site that corresponds to Node's validateId() (set*id, setgroups, initgroups), so all seven ids render identically.
  • subtle.digest(): Node's message here comes from its webidl BufferSource converter, not from the list renderer, so this site uses the literal webidl message that importKey() in the same file already emits.
  • Sign#sign(): besides the list, the argument is named privateKey as in Node's sig.js (preparePrivateKey(privateKey, 'privateKey')).
  • CryptoHkdf.cpp: the old string also omitted DataView and had no "or"; the list is Node's validateByteSource list.
  • The four vendored tests are restored to their upstream v26.3.0 text (one line each); they fail on the released binary and pass with this change.
  • Why this is correct: every expected message in the added tests is Node v26.3.0's verbatim output for the same call (each table was executed against node v26.3.0 before being committed), and the error codes, error classes and accepted inputs are unchanged; only the wording differs.
  • Verification:
    • bun bd test test/js/node/crypto/node-crypto.test.js (new describe, 16 cases: all fail on Bun 1.4.0, pass with this change)
    • bun bd test test/js/node/process/process.test.js (new id and emitWarning tests: 6 of 8 cases fail on 1.4.0, the 2 initgroups cases document the already-correct site; the one pre-existing failure in that file, process.env.USER being unset in the container, is unrelated)
    • bun bd test test/js/node/string_decoder/string-decoder.test.js, bun bd test test/js/web/crypto/web-crypto.test.ts (one new test each, fail on 1.4.0, pass now)
    • bun bd test/js/node/test/parallel/<file>.js for the 4 restored tests plus test-process-initgroups, test-process-emitwarning, test-crypto-dh, test-crypto-dh-errors, test-crypto-hkdf, test-crypto-prime, test-crypto-getcipherinfo, test-crypto-sign-verify, test-crypto-keygen-dh-classic, test-webcrypto-digest, test-process-exception-capture-errors: all exit 0
    • A probe of 57 calls across these APIs went from 42 messages differing from Node to 9; the remaining 9 are the histogram site owned by node:perf_hooks: align createHistogram/record/add validation with Node.js #33585 plus different bugs (listed in the details block) that are tracked separately.
  • Deliberately not changed here:

Background

  • ERR_INVALID_ARG_TYPE rendering in Node: expected is an array; each entry that is one of Node's primitive type names (string, number, bigint, Function, Object, ...) goes into a "types" bucket rendered as of type X (or one of type X or Y for several), each entry matching /^[A-Z][a-zA-Z0-9]*$/ goes into an "instances" bucket rendered as an instance of A, B, or C, and the buckets are joined with or. So the order of the list does not matter, but which bucket each entry lands in does.
  • Bun::ERR::INVALID_ARG_TYPE overloads (src/jsc/bindings/ErrorCode.h): the (name, const String& expected, value) overload is for a single type and prints of type <expected> verbatim; the (name, std::span<const ASCIILiteral>, value) overload is the port of Node's bucketing above; INVALID_ARG_INSTANCE(name, "A, B, or C", value) prints an instance of ... for lists that are classes only. addParameter picks argument or property depending on whether the name contains a dot, which is why options.add renders as a property.
  • subtle.digest is a WebCrypto method; Node validates its arguments with its own webidl layer, whose failures are TypeErrors with code: 'ERR_INVALID_ARG_TYPE' and a Failed to execute '<method>' on 'SubtleCrypto': <n>th argument ... message, which is why that one site does not use the list renderer.
Before/after for every changed site (Node v26.3.0 is the "after" text)
Call Bun before Now (= Node v26.3.0)
new StringDecoder().write(123) The "buf" argument must be of type Buffer, TypedArray, or DataView. Received type number (123) The "buf" argument must be an instance of Buffer, TypedArray, or DataView. Received type number (123)
crypto.subtle.digest("SHA-256", 123) The "data" argument must be of type ArrayBuffer, Buffer, TypedArray, or DataView. Received type number (123) Failed to execute 'digest' on 'SubtleCrypto': 2nd argument is not instance of ArrayBuffer, Buffer, TypedArray, or DataView.
createDiffieHellman({}) The "sizeOrKey" argument must be of type number, string, ArrayBuffer, Buffer, TypedArray, or DataView. Received an instance of Object The "sizeOrKey" argument must be one of type number or string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView. Received an instance of Object
createDiffieHellman(prime, {}) same shape for "generator" same shape for "generator"
getCipherInfo({}) The "nameOrNid" argument must be of type string or number. Received an instance of Object The "nameOrNid" argument must be one of type string or number. Received an instance of Object
generatePrimeSync(8, { add: {} }) (also rem, also async generatePrime) The "options.add" property must be of type ArrayBuffer, Buffer, TypedArray, DataView, or bigint. Received an instance of Object The "options.add" property must be of type bigint or an instance of ArrayBuffer, TypedArray, Buffer, or DataView. Received an instance of Object
hkdfSync("sha256", "key", {}, "info", 16) (also info, also async hkdf) The "salt" argument must be of type string, ArrayBuffer, TypedArray, Buffer. Received an instance of Object The "salt" argument must be of type string or an instance of ArrayBuffer, TypedArray, DataView, or Buffer. Received an instance of Object
generateKeyPairSync("dh", { prime: "x" }) The "options.prime" property must be of type Buffer, TypedArray, or DataView. Received type string ('x') The "options.prime" property must be an instance of Buffer, TypedArray, or DataView. Received type string ('x')
createSign("SHA256").sign(123) The "key" argument must be of type ArrayBuffer, Buffer, TypedArray, DataView, string, KeyObject, or CryptoKey. Received type number (123) The "privateKey" argument must be of type string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, KeyObject, or CryptoKey. Received type number (123)
process.emitWarning(123) The "warning" argument must be of type string or Error. Received type number (123) The "warning" argument must be of type string or an instance of Error. Received type number (123)
process.setuid({}) (also seteuid, setgid, setegid) The "id" argument must be of type number or string. Received an instance of Object The "id" argument must be one of type number or string. Received an instance of Object
process.setgroups([1, true]) The "groups[1]" argument must be of type number or string. Received type boolean (true) The "groups[1]" argument must be one of type number or string. Received type boolean (true)

Remaining differences from the probe: histogram.record("x") (#33585), and, each a separate bug, StringDecoder#end(123) throws a plain Expected Uint8Array TypeError; Sign#sign({ key: 123 }) and privateDecrypt(123, ...) say key/key.key where Node says privateKey/privateKey.key (the name is not threaded through KeyObject::prepareAsymmetricKey); process.kill(pid, {}) throws ERR_INVALID_ARG_TYPE where Node throws ERR_UNKNOWN_SIGNAL; and the kill EINVAL errno message uses Bun's general system-error format.

…ndings

Several native entry points handed ERR_INVALID_ARG_TYPE an already
flattened "A, B, or C" string, which the single-string overload always
renders as "must be of type A, B, or C". Node passes a list and groups it
into primitive names ("one of type number or string") and class names
("an instance of Buffer, TypedArray, or DataView").

Pass the same lists Node passes through the span overload (which now
also accepts a dynamic argument name, for setgroups' "groups[i]"), use
INVALID_ARG_INSTANCE for the two class-only lists, and give
subtle.digest() the same webidl-style message importKey() already uses.
Sign#sign() also names the argument "privateKey" like Node does.

Sites: StringDecoder#write, subtle.digest, createDiffieHellman
(sizeOrKey, generator), getCipherInfo, generatePrime/generatePrimeSync
(options.add, options.rem), hkdf/hkdfSync (salt, info), generateKeyPair
"dh" options.prime, Sign#sign, process.emitWarning, process.set*id,
setgroups and initgroups.

The four vendored Node tests that had been edited to expect the old
wording are restored to their upstream text.
@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: 18 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: 7cc67eed-f7b4-4202-9579-fdd9a4d0ea31

📥 Commits

Reviewing files that changed from the base of the PR and between 9805144 and fe0f237.

📒 Files selected for processing (19)
  • src/jsc/bindings/BunProcess.cpp
  • src/jsc/bindings/ErrorCode.cpp
  • src/jsc/bindings/ErrorCode.h
  • src/jsc/bindings/JSStringDecoder.cpp
  • src/jsc/bindings/node/crypto/CryptoGenDhKeyPair.cpp
  • src/jsc/bindings/node/crypto/CryptoHkdf.cpp
  • src/jsc/bindings/node/crypto/CryptoPrimes.cpp
  • src/jsc/bindings/node/crypto/JSDiffieHellmanConstructor.cpp
  • src/jsc/bindings/node/crypto/JSSign.cpp
  • src/jsc/bindings/node/crypto/node_crypto_binding.cpp
  • src/jsc/bindings/webcrypto/JSSubtleCrypto.cpp
  • test/js/node/crypto/node-crypto.test.js
  • test/js/node/process/process.test.js
  • test/js/node/string_decoder/string-decoder.test.js
  • test/js/node/test/parallel/test-process-euid-egid.js
  • test/js/node/test/parallel/test-process-setgroups.js
  • test/js/node/test/parallel/test-process-uid-gid.js
  • test/js/node/test/parallel/test-string-decoder.js
  • test/js/web/crypto/web-crypto.test.ts

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; ready for review.

  • Reproduced on Bun 1.4.0 and a debug build of main (97a4363115) by running the same calls under Node v26.3.0 and diffing the messages: 42 of 57 probed ERR_INVALID_ARG_TYPE messages differed, all because the C++ callers pass a pre-flattened type string.
  • Fix and per-site before/after table are in the PR description. New tests live in test/js/node/crypto/node-crypto.test.js, test/js/node/process/process.test.js, test/js/node/string_decoder/string-decoder.test.js and test/js/web/crypto/web-crypto.test.ts; all fail on 1.4.0 and pass with this branch. Four vendored Node tests are restored to their upstream text.
  • Not in this PR: KeyObject.cpp (node:crypto: render prepareAsymmetricKey ERR_INVALID_ARG_TYPE lists like Node #38431) and histogram.record() (node:perf_hooks: align createHistogram/record/add validation with Node.js #33585), which already have open PRs.
  • Automated review so far raised no findings; nothing outstanding on this branch.

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

LGTM — mechanical Node-compat fix that swaps pre-flattened error strings for the existing list-rendering overload at ~13 C++ sites; only error-message wording changes.

What was reviewed:

  • Each call site swaps one ERR::INVALID_ARG_TYPE/INVALID_ARG_INSTANCE helper for another; error codes, classes, and accepted inputs are unchanged.
  • The arg_name widening from ASCIILiteral to const WTF::String& on the span overload stays unambiguous — the second parameter (std::span vs String vs JSValue) disambiguates, and C arrays convert to span directly.
  • JSSubtleCrypto.cpp digest: throwError returns EncodedJSValue like the helper it replaces, and the prior tryClearException() is preserved; the exception-scope-fixture transcript tests the algorithm arg, not data, so unaffected.
  • Every changed site has a new exact-message test; four vendored Node tests are restored to upstream v26.3.0 text.
Extended reasoning...

Overview

This PR fixes Node.js compatibility for ERR_INVALID_ARG_TYPE error message wording at ~13 native C++ call sites across BunProcess.cpp, JSStringDecoder.cpp, six node/crypto/ files, and JSSubtleCrypto.cpp. Each site previously passed a pre-flattened string like "number or string"_s to the single-string overload (always renders "must be of type X"); now they pass static constexpr ASCIILiteral[] lists to the existing span overload, which buckets primitives vs classes exactly as Node's lib/internal/errors.js does. One signature widening in ErrorCode.{h,cpp} (arg_name: ASCIILiteral → const WTF::String&) lets setgroups pass its computed groups[i] name. Test additions cover every changed site with Node v26.3.0's verbatim messages, and four vendored Node parallel tests are restored to their upstream text.

Security risks

None. The changes are entirely to error-message text on already-rejected inputs. No validation logic, accepted types, error codes, or control flow changes. The static constexpr arrays are compile-time constants with no user input.

Level of scrutiny

Low-to-medium. This is a mechanical pattern application: the span-overload renderer already existed (used by initgroups), and each site is a 1-3 line swap between error helpers with identical signatures. The only non-trivial pieces are (a) the parameter-type widening, which I checked stays unambiguous because the second parameter type (span vs String vs JSValue) disambiguates all overloads and C arrays bind to std::span via the array-reference constructor; and (b) JSSubtleCrypto.cpp's switch from ERR::INVALID_ARG_TYPE to Bun::throwError, both of which throw and return EncodedJSValue, with the preceding tryClearException() unchanged.

Other factors

  • Test coverage is thorough: 16 new crypto cases, 8 new process cases, plus string-decoder and web-crypto, all asserting exact Node v26.3.0 messages with name/code/message. The PR description documents that each expected string was executed against Node before committing.
  • Restoring vendored tests to upstream text moves toward parity rather than away.
  • The Sign#sign argument rename "key" → "privateKey" matches Node's preparePrivateKey(privateKey, 'privateKey') and is covered by two new tests.
  • The CryptoHkdf.cpp list also fixes a pre-existing omission (DataView was missing, no "or") — now matches Node's validateByteSource.
  • No prior human reviews or outstanding comments on the PR.

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