Conversation
StringDecoder.prototype.end() only accepted an ArrayBufferView and threw
a plain TypeError ("Expected Uint8Array") for anything else, including
an explicit undefined and strings. In Node, end(buf) is write(buf)
followed by the flush, so undefined just flushes, a string is returned
ahead of the flushed bytes, and other values throw ERR_INVALID_ARG_TYPE.
write() and end() now resolve the buf argument through one helper, which
also makes write() with no argument throw Node's ERR_INVALID_ARG_TYPE
instead of ERR_MISSING_ARGS.
|
Warning Review limit reached
Next review available in: 31 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 (2)
Comment |
|
Status: fix pushed, waiting on CI. Reproduced on Bun 1.4.0 and main with Tests: the new |
There was a problem hiding this comment.
LGTM — focused Node-compat fix with thorough coverage.
What was reviewed:
endBody/writeBodycontrol flow: undefined → flush-only, string → flush thenstr + flushedviajsStringrope (matches Node'swrite(buf) + flushorder); non-view throws beforeResetScopefires so buffered state is preserved.- Exception discipline:
RETURN_IF_EXCEPTIONafterbufArgumentand after the flush;RELEASE_AND_RETURNon the throwingjsStringtail;flushed->length()is non-throwing so the early return is clean undervalidateExceptionChecks. write()no-arg now routes throughargument(0)→ERR_INVALID_ARG_TYPE(wasERR_MISSING_ARGS); grepped — no existing test asserts the old error, andtest/parallel/test-string-decoder.jsalready pins the shared message wording.- Tests cover all encodings, 10 rejected value types, state-after-throw, and a DataView guard for the unchanged path.
Extended reasoning...
Overview
The PR aligns StringDecoder.prototype.end(buf) argument handling with Node.js: end() now accepts exactly what write() accepts (undefined → flush only, string → passed through then flush appended, ArrayBufferView → decode+flush, anything else → ERR_INVALID_ARG_TYPE). It touches two functions in src/jsc/bindings/JSStringDecoder.cpp (writeBody, endBody) and extracts their shared view-or-throw check into bufArgument. writeBody also switches from argumentCount()<1 + uncheckedArgument to argument(0), so write() with no argument now throws Node's ERR_INVALID_ARG_TYPE instead of ERR_MISSING_ARGS. 31 new test cases in the existing string-decoder.test.js file cover the matrix.
Security risks
None. This is argument-type validation and string concatenation for a text decoder. No untrusted lengths drive allocation; the string path calls end(nullptr, 0) (pure flush of ≤4 buffered bytes) then rope-concatenates two existing JSStrings. No new pointer arithmetic on user data.
Level of scrutiny
Moderate — native C++ in the JSC bindings layer, so exception-scope discipline and null-safety matter, but the change is small (~35 net lines), localized to two prototype bodies, and reuses idioms already present in the same file (jsString(globalObject, a, b) for concatenation, RETURN_IF_EXCEPTION after end(), RELEASE_AND_RETURN on throwing tails). I traced each path:
bufArgumentthrows via the caller'sThrowScope, callers checkRETURN_IF_EXCEPTIONimmediately — matches the ErrorCode pattern used elsewhere.- String path in
endBody:end(nullptr, 0)returns non-null on success (all branches inJSStringDecoder::endreturnjsEmptyStringor anencodingToStringresult);flushed->length()is a plain field read (non-throwing), so the earlyreturn encode(buffer)is exception-check-clean;asString(buffer)is safe becausebuffer.isString()was checked. - Error-before-state-mutation: for a rejected value,
bufArgumentthrows beforecastedThis->endruns, soResetScopenever fires and the buffered partial survives — matches Node (wherewrite()throws before the flush) and is directly tested. - The utf16le single-byte partial (flushed as "") exercises the
length()==0fast path returning the string unchanged.
Other factors
- The PR description documents cross-verification against Node v26.3.0 and a run under
BUN_JSC_validateExceptionChecks=1, and confirms 30/31 new cases fail on released Bun. - I grepped for existing assertions on the old
write()no-arg error (ERR_MISSING_ARGS/ "Not enough arguments") in string_decoder tests — none, so no test regression from that change.test/js/node/test/parallel/test-string-decoder.js:199already pins themust be of typewording this PR keeps for both methods. - Tests are placed in the existing test file per repo convention, use
it.eachfor the matrix, assert error class/code/message, and include a positive guard (end(view)) for the path the refactor left in place.
Problem
new StringDecoder().end(undefined),.end("abc")and.end(123)all throwTypeError: Expected Uint8Array(no.code). Node returns"","abc", and throwsTypeError [ERR_INVALID_ARG_TYPE]: The "buf" argument must be ... Received type number (123)respectively.end("abc")returns"abc\ufffd"; Bun throws, and the partial stays buffered.jsStringDecoderPrototypeFunction_endBody(src/jsc/bindings/JSStringDecoder.cpp:430) has its own argument check that only accepts anArrayBufferViewwhen an argument is present, whilewriteBodyright above it already implements Node's string pass-through andERR_INVALID_ARG_TYPE. In Node,end(buf)is(buf === undefined ? "" : this.write(buf))plus the flush (https://github.com/nodejs/node/blob/v26.3.0/lib/string_decoder.js#L96-L101), so it accepts exactly whatwrite()accepts.@types/nodedeclaresend(buffer?: string | ArrayBufferView)accordingly, so this compiles and then throws at runtime on Bun.Fix
write()andend()resolvebufthrough one helper (bufArgument), which throws theERR_INVALID_ARG_TYPEwrite()already threw;end()no longer has its own error.end()now mirrors Node's three outcomes:undefined(explicit or omitted) only flushes; a string is returned followed by whatever the flush produces; any other non-view throws before the decoder state is touched, so the buffered partial is still returned by the nextend()(also Node's behavior, sincewrite()throws before the flush runs).write()reads its argument withcallFrame->argument(0), sowrite()with no argument now throws Node'sERR_INVALID_ARG_TYPE(Received undefined) instead ofERR_MISSING_ARGS.node:assert(output below); the only remaining difference is themust be of typevsmust be an instance ofwording of the shared message, whichtest/js/node/test/parallel/test-string-decoder.jscurrently pins to Bun's wording and which this PR deliberately leaves alone sowrite()andend()keep throwing the identical message.text(buf, offset)keeps its own check, because Node'stext()iswrite(buf.slice(offset))and fails with a plainTypeErrorfrom.slice, not withwrite()'s error.test/js/node/string_decoder/string-decoder.test.js, newdescribe("end(buf) handles its argument like write(buf)"): 30 of the 31 new cases fail on the released Bun (USE_SYSTEM_BUN=1), all 126 in the file pass with this build. The one case that passes both ways (end(view)decodes and flushes) guards the path the refactor kept.test/js/node/test/parallel/test-string-decoder.js,test-string-decoder-end.js,test-string-decoder-fuzz.js(exit 0), and the new cases underBUN_JSC_validateExceptionChecks=1.Background
StringDecoderdecodes a byte stream that may split multi-byte characters across chunks:write(chunk)returns the complete characters and keeps the trailing incomplete bytes;end()flushes those (as U+FFFD for utf8, a lone surrogate for utf16le, padding for base64) and resets the decoder. Bun implements it natively inJSStringDecoder.cpp;JSStringDecoder::end(ptr, len)decodeslenbytes and then flushes, soend(nullptr, 0)is a pure flush.ERR_INVALID_ARG_TYPEis Node's error for a wrongly typed argument;Bun::ERR::INVALID_ARG_TYPE(src/jsc/bindings/ErrorCode.cpp) builds the sameThe "name" argument must be ... Received ...message with Node's rendering of the received value and sets.code. Callers pass theThrowScopein and checkRETURN_IF_EXCEPTIONafterwards, which is the pattern the new helper follows.Node v26.3.0 vs Bun 1.4.0 for the reported calls
The new test's scenarios rewritten with
node:assertpass under Node v26.3.0 and under this build (all scenarios agree); the released Bun fails at the firstend(undefined).