Repository navigation
Conversation
…n instance of"
The single-string ERR_INVALID_ARG_TYPE formatter printed every expected
type after "must be of type", so callers passing one class name (C++
ERR::INVALID_ARG_TYPE(..., "AbortSignal"_s, ...) and the builtins'
$ERR_INVALID_ARG_TYPE(name, "ReadableStream", value)) produced
"must be of type AbortSignal" where node prints "must be an instance of
AbortSignal". Node treats a string expected type as a one-entry list and
classifies the entry; do the same: kTypes names render "of type x"
(lower-cased), class names render "an instance of X", everything else is
unchanged.
Also use node's argument and class names at two sites whose names
differed as well: Histogram#add ("other"/"RecordableHistogram") and
X509Certificate#checkIssued ("otherCert"/"X509Certificate").
|
Warning Review limit reached
Next review available in: 6 minutes 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 (8)
Comment |
|
Status: fix pushed, self-review in progress. Reproduced on bun 1.4.0 / main with, for example: Same divergence for every caller that passes one class name ( Tests: |
There was a problem hiding this comment.
LGTM — focused Node.js error-message compat fix.
What was reviewed:
- The single-string
ERR_INVALID_ARG_TYPEformatter now classifies kTypes vs class name vs phrase using the sameisPrimitiveTypeName/isClassNamehelpers the list overload already uses; checked ordering so"Object"/"Function"hit kTypes before the class-name regex and lower-case correctly. - Verified the pre-rendered-list fallback (
"string or an instance of Buffer, ...") and the free-form phrase branch are byte-identical to before, and that theWTF::String&→StringViewparam widening on the two static helpers is safe for the existing list-overload callers. - Two call-site name fixes (
Histogram#add→"other"/RecordableHistogram,X509Certificate#checkIssued→ structured"otherCert"/X509Certificate) match node v26.3.0; the 17-case test file plus per-API tests pin exact strings.
Extended reasoning...
Overview
This PR fixes the single-string overload of Message::ERR_INVALID_ARG_TYPE in src/jsc/bindings/ErrorCode.cpp so that a bare class name renders as "an instance of X" (matching Node's lib/internal/errors.js) instead of "of type X". It reuses the existing isPrimitiveTypeName/isClassName helpers from the list overload, forward-declares them, and widens their parameter from const WTF::String& to StringView so both overloads can share them. Two call sites with wrong argument/class names are corrected: Histogram#add ("argument"/"Histogram" → "other"/"RecordableHistogram") and X509Certificate#checkIssued (hand-written message → structured ERR::INVALID_ARG_TYPE with "otherCert"/"X509Certificate"). A header comment documents the classification. Four test files add exact-string assertions verified against node v26.3.0.
Security risks
None. This is pure error-message text formatting on the throw path; no parsing, allocation sizing, auth, or crypto logic changes. The X509 change only replaces one throw call with another that formats the same rejection differently.
Level of scrutiny
Medium-low. The formatter is shared across ~50 call sites, so the important thing is that the three unchanged rendering classes (lower-case primitive, pre-rendered list, free-form phrase) stay byte-identical. I traced each: primitives hit isPrimitiveTypeName and lower-case to themselves; pre-rendered lists contain " or " so isPhrase is false and they get the verbatim "of type " prefix; phrases fall through unchanged. The kTypes-before-classRegExp ordering matches Node so "Object"/"Function" correctly render "of type object"/"of type function" rather than "an instance of Object". The StringView widening is safe — the list overload passes a WTF::String, which implicitly converts.
Other factors
No CODEOWNERS on touched files. No prior human review comments. Test coverage is thorough: a new 17-case file exercises all three entry routes (C++ literal name, JS-supplied dotted name via validateAbortSignal, builtin $ERR_INVALID_ARG_TYPE) plus an async rejection, the "Object" lower-casing, and the three unchanged-rendering controls; per-API tests cover the two renamed call sites and the seven Bun.readableStreamTo* consumers. The PR description records that 14/17 fail on the unfixed build and all pass after, and that a 20-call diff against real node v26.3.0 is identical.
Problem
ERR_INVALID_ARG_TYPEmessages whose expected type is a single class name saymust be of type X; node saysmust be an instance of X. For exampleevents.on(ee, "x", { signal: 1 })throwsThe "options.signal" property must be of type AbortSignal. Received type number (1); node v26.3.0 throws... must be an instance of AbortSignal. ....Message::ERR_INVALID_ARG_TYPE(src/jsc/bindings/ErrorCode.cpp:644on main) printsof type+ the string for anything that is not a phrase with spaces. Node'slib/internal/errors.jsturns a stringexpectedinto a one-entry list and classifies the entry (kTypes name, class name, other) the same way it classifies list entries; the list overload a few lines below already ports that classification, the single-string one never did.ERR::INVALID_ARG_TYPE(..., "ClassName"_s, ...)sites (validateAbortSignal,Buffer.copyBytesFrom,X509Certificate#checkPrivateKey/#verify,Histogram#add, theBun.readableStreamTo*consumers, ...) and the 26 builtin$ERR_INVALID_ARG_TYPE(name, "ClassName", value)sites, which compile to the same formatter (ErrorCode.cpp:1673):stream.addAbortSignal,Readable/Writable/Duplex.fromWeb,events.addAbortListener,events.once,perf_hooks.timerify,new Readable({ signal }), and so on. The two builtin sites that pass"Object"(inspector.Session#postand the internal zlib transform params check) renderof type Objectwhere node lower-cases toof type object, same cause.Histogram#addreportsThe "argument" argument ... Histogram(node:"other"/RecordableHistogram), andX509Certificate#checkIssuedthrows a hand-writtenissuer must be a JSX509Certificate(node:The "otherCert" argument must be an instance of X509Certificate. Received ...).Fix
Message::ERR_INVALID_ARG_TYPE(single-string overload) now classifies its argument like one node list entry, reusing the list overload'sisPrimitiveTypeName/isClassName: a kTypes name rendersof type+ lower-cased name, a class name rendersan instance of X. The existing phrase handling and the verbatimof type ...fallback for pre-rendered lists ("string or an instance of Buffer, ...") are unchanged, so callers passing a lower-case primitive name or a pre-rendered list produce byte-identical output.Histogram#addpasses"other"/"RecordableHistogram";X509Certificate#checkIssuedgoes throughERR::INVALID_ARG_TYPEwith"otherCert"/"X509Certificate". Nothing else at the call sites changes; the other 19 C++ sites and all builtin sites are fixed by the formatter. (Buffer.copyBytesFromis deliberately left alone so this does not collide with node:buffer: reject DataView in Buffer.copyBytesFrom #35239, which switches that line toINVALID_ARG_INSTANCEfor a different reason; either order of landing gives the same text.) Pre-rendered list sites are a separate change (node: pass ERR_INVALID_ARG_TYPE expected types as lists in the C++ bindings #38690, node: pass ERR_INVALID_ARG_TYPE expected types as lists so fs streams, zlib and diagnostics_channel messages match node #38657).node:*APIs node's output is the contract, and node's classification is exactly the one-entry case of the list port that already exists in this file. Every assertion added below is the string node v26.3.0 prints for the same call; the three Bun-only sites whose text changes as a side effect (Bun.readableStreamTo*,CompressionStream/TextDecoderStreamchunk errors, the$checkBufferReadguard) go fromof type ReadableStreamtoan instance of ReadableStream, which is the wording the rest of the tree already uses for class checks viaINVALID_ARG_INSTANCE. No test in the tree pinned any of the old strings.test/js/node/errors/invalid-arg-type-expected.test.ts(new): 17 cases covering the three routes into the formatter (C++ name, JS-supplied dotted name via validateAbortSignal, builtin$ERR_INVALID_ARG_TYPEincluding an async rejection), the"Object"lower-casing, and three unchanged renderings (primitive, pre-rendered list, phrase). 14 fail on the unfixed build, 17 pass with the fix.test/js/bun/perf_hooks/histogram.test.ts:add()name and class name (fails before, passes after).test/js/node/crypto/x509-subclass.test.ts:checkIssued,checkPrivateKeyandverifymessages for five non-KeyObject values, plus wrong-key-type and success paths (6 fail before, 19 pass after).test/js/web/streams/streams.test.js: the sevenBun.readableStreamTo*consumers (7 fail before, pass after).buffer.test.js,event-emitter.test.ts,node-stream.test.js,perf_hooks.test.ts,inspector.test.ts,compression.test.ts,text-decoder-stream.test.ts,x509.test.ts, all ofstreams.test.js, and 17 vendored node tests for the affected APIs (test-stream-add-abort-signal,test-events-once,test-events-on-async-iterator,test-crypto-x509,test-buffer-from, the fivetest-whatwg-webstreams-adapters-*, ...) pass on the debug build.Background
ERR_INVALID_ARG_TYPEmessage shape:The "<name>" argument|property must be <expected>. Received <value>. Node builds<expected>from a list: entries in itskTypeslist (string,number,object,Function, ...) becomeof type x(lower-cased), entries matching/^[A-Z][a-zA-Z0-9]*$/becomean instance of X, anything else is printed as written. A stringexpectedis wrapped into a one-entry list first, so'AbortSignal'and['AbortSignal']render identically.ErrorCode.cpp: the list overload (used when a caller passes an array or astd::span) is a port of the grouping above; the single-string overload is reached by C++ callers passing oneWTF::Stringand by$ERR_INVALID_ARG_TYPEwhen its second argument is a string. Some C++ callers pass the single-string overload a list they already rendered by hand ("string or an instance of Buffer"); node has no equivalent, which is why that overload keeps a verbatim fallback instead of delegating to the list renderer outright.INVALID_ARG_INSTANCEis the existing helper that always printsan instance of <text>; it stays, since it also takes pre-rendered instance lists. After this change passing a bare class name toINVALID_ARG_TYPEproduces the same text, so callers no longer have to know about it.bun 1.4.0 vs node v26.3.0 for the calls asserted in the tests
With this change every bun line above is identical to the node line.