Repository navigation
Conversation
… is a file The arm asks for the kind of store only when the part has a type. A part with no type has nothing to drop, so it costs one compare. Tests: a file descriptor and fs.openAsBlob() as a part, the Blob of a Response made from the new Blob, and the #33599 ASAN test now also drives a Bun.file() part, which is the one part that still brings a heap-owned type into the constructor. The doc of Bytes.stored_name named the File constructor, which stopped writing that field in #40436.
|
Updated 6:55 AM PT - Oct 20th, 2026
❌ @robobun, your commit 9f7f49f has 1 failures in
🧪 To try this PR locally: bunx bun-pr 46173That installs a local version of the PR into your bun-46173 --bun |
|
Status: draft, waiting for a maintainer's decision on the table in the description. How I reproduced it. On release builds of main and of #46114, and on Node v26.3.0: const typed = new Blob(["hi"], { type: "a/b" });
console.log(JSON.stringify([new Blob([typed]).type, new File([typed], "n").type, new Blob([typed, typed]).type]));Tests. 28 new tests in CI. 180 of 181 jobs pass in build 129401. The one failed job is |
Stacked on #46114. The diff of this PR is its last two commits.
This is a draft, because it changes what an upload sends. It needs a maintainer's decision on the table below. If the answer is yes: 1.4.4 or 1.5?
new Blob([typed]).type,new File([typed], "n").typea/b""""new File([audioBlob], "a.mp3")audio/mpegapplication/octet-streamapplication/octet-streamtoFile(typedBlob, "doc.pdf").typein openai, @anthropic-ai/sdk, groq-sdk, cloudflareapplication/pdf""""new Response(new Blob([typed]))Content-Typea/bnew File([b], name, { type: b.type }), aBun.file()part, a Blob that is a bodyProblem
new Blob([part])andnew File([part], name)with one in-memory Blob part return a copy of the part that keeps itstype. The File API takes the bytes from the parts and the type fromoptions.type. Node and browsers give"". Bun also gives""for two parts.from_js_without_defer_gc(Blob.rs:3098on main), which returnsblob.dupe().Fix
dupe_without_type(): the same store, window and name, and no type. It copies no bytes, as before.Bun.file(), S3,fs.openAsBlob(), an embedded file) keeps the full copy, because its type is read from the file. This exception is provisional.REQUIRE_ARRAY). A Blob that is a body keeps its type.blob.test.ts. 26 fail on the head of Blob: remove the type from the shared byte store #46114.Background
dupe()copies each field of the part: window, type, name, File brand. The name and the brand stay as they are on main. Blob: a copy of a File is a plain Blob (file.slice(), new Blob([file]), response.blob()) #45401 changes those.dupe(): each call copies the type and then drops it. Blob/File: do not inherit part metadata on the single-Blob-part fast path #33603 was an earlier attempt at this fix. It closed with no review.Downsides
application/octet-stream(the table). The migration isnew File([b], name, { type: b.type }), which gives the same result on main and on Node.Bun.servehandler that returnsnew Response(new Blob([htmlBlob]))senttext/html;charset=utf-8. It now sendstext/plain;charset=utf-8for ASCII bytes andapplication/octet-streamfor other bytes.new Blob([untypedBlob])goes from 470 to 476 instructions, andnew Blob([typedBlob])from 464 to 481. A call with no Blob part does not change (760 to 759).Notes
Every row that moves. Release builds of the head of #46114 and of this branch, Node v26.3.0.
typedisnew Blob(["hi"], { type: "a/b" }).new Blob([typed]).typewith no options,{},{ type: undefined }or a type that is not valida/b""""new File([typed], "n").typea/b""""new Blob([file]).type,fileisnew File(["hi"], "a.txt", { type: "text/x" })text/x""""new File([audioBlob], "a.mp3")audio/mpegapplication/octet-streamapplication/octet-streamform.append("e", new Blob([file]))filename="a.txt",text/xfilename="a.txt",application/octet-streamfilename="blob",application/octet-streamnew Response(new Blob([typed])),new Request(url, { body: new Blob([typed]) })Content-Typea/b(await new Response(new Blob([typed])).blob()).typea/btext/plain;charset=utf-8""Bun.servehandler,new Response(new Blob([htmlBlob])), ASCII bytestext/html;charset=utf-8text/plain;charset=utf-8text/html;charset=utf-8application/octet-streamtext/html;charset=utf-8The
text/plain;charset=utf-8rows are what Bun gives every Blob that has no type:(await new Response(new Blob(["hi"])).blob()).typeistext/plain;charset=utf-8on main too.Rows that do not move:
new Blob([typed], { type: "c/d" }),new Blob([typed], { type: "" })(already""on main),new Blob([typed, typed]),new Response(typed),typed.slice(0, 1),new Blob([file]).nameandinstanceof File, and each part that is a file.SDK uploads. openai@6.49.0,
client.audio.transcriptions.create({ file })against a local server, Content-Type of the multipart part:fileawait fetch(url)audio/mpegapplication/octet-streamapplication/octet-streamtoFile(typedBlob, "audio.mp3")audio/mpegapplication/octet-streamapplication/octet-streamtoFile(fetch(url), "audio.mp3")audio/mpegapplication/octet-streamapplication/octet-streamnew File([bytes], "audio.mp3", { type })audio/mpegaudio/mpegaudio/mpegBun.file(path)audio/mpegaudio/mpegfs.createReadStream(path),toFile(buffer, "audio.mp3")application/octet-streamapplication/octet-streamapplication/octet-streamtoFile(typedBlob, "doc.pdf").typeisapplication/pdfon main and""here for @anthropic-ai/sdk@0.135.0, openai@6.49.0, openai@5.23.2, groq-sdk@1.6.0 and cloudflare@7.3.0. It is""on Node for the four that load there.What this PR does not change. Each of these still differs from the File API:
Bun.file(), a file descriptor,fs.openAsBlob(), S3, a file embedded bybun build --compile. The type, the name and the time of such a Blob are read through the store, so a copy with no type needs more than this arm. The tests pin the current values.a File part keeps its name and its brand, and loses only its typepins what main does. The PR that changes the name changes that test.slice()keeps a parent type that is interned.new Response(typed.stream()).blob()keeps the type.new Response([typed])keeps the type.options.typeis rewritten for some names (text/plainreadstext/plain;charset=utf-8, Different MIME type than in Node fortext/plain#23777).The file test.
store_is_a_file()readsBytes.stored_nameto find an embedded file. Its doc still named the File constructor, which stopped writing that field in #40436, so this PR corrects the doc.The #33599 test. That ASAN test wraps a Blob that has a heap-owned type and gives it a type from the table. An in-memory part no longer brings its type into the constructor, so the test now also runs with a
Bun.file(path, { type })part, which still does.Measurements. gdb single-step of one native call, 3 hits, release builds of the head of #46114 and of this branch.
new Blob(["hello world"])new Blob([new Uint8Array(11)])new Blob(["hello world"], { type: "text/plain" })new File(["x"], "n")new Blob([untypedBlob])new Blob([typedBlob]), heap-owned typenew Blob([typedBlob]), interned typenew Blob([typedBlob], { type: "c/d" })new Blob([file])new Blob([Bun.file(p)])new File([untypedBlob], "n")new File([typedBlob], "n")One malloc per call with a Blob part on both builds. Release
.text81,693,413 to 81,693,669 bytes.Self-review. The review ran before the arm was narrowed to the type, and the final shape has not had a review of its own. The six concerns: state the change as breaking and give the migration, the
Bun.servehandler rows, keep the filename of a File part, find an embedded file by its path and not by a non-emptystored_name, pin each kind of file part, keep the #33599 test on its path.