Blob: stringify null/undefined parts, accept iterable blobParts, coerce slice() arguments#33023
Blob: stringify null/undefined parts, accept iterable blobParts, coerce slice() arguments#33023robobun wants to merge 2 commits into
Conversation
|
Warning Review limit reached
Next review available in: 8 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 (4)
Comment |
|
Updated 9:31 AM PT - Jul 8th, 2026
❌ @robobun, your commit eb5c074 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 33023That installs a local version of the PR into your bun-33023 --bun |
|
Rebased onto current CI on build 70492: 282 test jobs passed across Linux (glibc, musl, ASAN, baseline), Windows, and macOS. The two red jobs are both I have already retriggered once in this PR and will not push further empty commits; the two red jobs can be retried from the Buildkite UI if a green check is wanted before merging. |
…) args null and undefined entries in a blobParts array were silently dropped, so the same code produced different bytes in Bun than in Node and browsers (the File API converts non-Blob/BufferSource parts with ToString). The single-element fast path already stringified them, so Bun disagreed with itself too. - Stop skipping null/undefined in the multi-part loop; they stringify to "null"/"undefined" like every other non-Blob/BufferSource part, and array holes behave the same. - new Blob(null) and new File(null, name) throw a TypeError; undefined still means the optional argument was omitted. - Accept any iterable object (Set, Map, generators) as blobParts by materializing it into a JSArray with the JS iteration protocol (JSValue::to_array_from_iterable); the array keeps generator-produced values rooted. - Blob.slice(): provided, non-undefined start/end go through ToNumber (null, booleans, objects with valueOf; symbols throw) and a non-string contentType goes through ToString. The documented Bun overloads slice(contentType) / slice(begin, contentType) and new Blob(typedArray) are unchanged.
WebIDL converts arguments before the operation body, so ToNumber on start/end (including its side effects and exceptions) and ToString on contentType must run even when the source Blob is empty, and a provided contentType applies to the returned empty Blob.
318956a to
eb5c074
Compare
What does this PR do?
nullandundefinedentries in ablobPartsarray are silently dropped, so the same code produces different bytes in Bun than in Node, Deno and browsers. The File API takes a WebIDLsequence<BlobPart>whose non-Blob/BufferSource members are converted withToString, sonullcontributes the four bytes"null":Blob bytes usually end up on the wire (multipart uploads,
Response(blob),Bun.write), so a straynullin a parts array yields a payload whose length and hash silently differ from every other runtime. Related conversions in the same constructor and inslice()were also off:Cause:
Blob::from_js_without_defer_gcskipsis_undefined_or_null()items in the multi-part loop, only walks realJSArrays, and treats anullargument as "no parts".Blob::get_sliceonly honorsstart/endvalues that are already numbers and acontentTypethat is already a string, instead of applying the WebIDL conversions ([Clamp] long longvia ToNumber,DOMStringvia ToString).Changes:
null/undefined; they flow into the existing ToString arm. Array holes behave the same (new Blob([, "x"])is"undefinedx", as in Node).new Blob(null)andnew File(null, name)throw aTypeError.new Blob()/new Blob(undefined)still produce an empty Blob (the argument is optional).Proxyof an array) is materialized into a JSArray via the JS iteration protocol (newJSValue::to_array_from_iterableinsrc/jsc/JSValue.rs) and then joined like an array. The array is what keeps generator-produced values alive for the join, since they have no other GC root.slice()runs ToNumber on provided, non-undefinedstart/endvalues (nullis 0, objects usevalueOf, symbols throw, NaN is 0) and ToString on a provided, non-stringcontentType.Deliberate Bun extensions that are kept, now with comments saying so:
new Blob(typedArray)still copies the bytes. The spec iterates a top-level typed array as a sequence of numbers (new Blob(new Uint8Array([65,66]))is"6566"in Node), which is never what the caller wants.slice(contentType)/slice(begin, contentType)overloads onBlob/BunFile/S3File(a string in the start/end position) are unchanged, soblob.slice("1", "3")still resolves to the contentType overload rather than ToNumber.ArrayBufferpart still contributes 0 bytes: that is what the current WebIDL "get a copy of the bytes held by the buffer source" specifies and what browsers do (Node throws).Two existing assertions encoded the old coercion and were updated in the same spirit as the change:
blob.slice(Symbol(), "-123")andblob.slice(Object.create(null), "-123")now throwTypeError(ToNumber would throw in any other runtime), and the sparse-array fast-path test expects holes to stringify.How did you verify your code works?
New tests in
test/js/web/fetch/blob.test.tscover the part stringification,new Blob(null)rejection, iterables (including a generator interleaved withBun.gc(true)and an iterator that throws mid-way), and theslice()coercions. All of them fail on current main and pass with this change:Also ran the surrounding suites with the debug (ASAN) build: blob-cow, blob-write, blob-oom, blob-file-name-ownership, deno blob, structured-clone-blob-file, worker_blob, websocket-blob, FormData, response, body, bun-write. No regressions.