Skip to content

Blob: new Blob([blob]) and new File([blob], name) do not take the type of the part - #46173

Draft
robobun wants to merge 2 commits into
robobun/ee41b22f/blob-store-no-typefrom
robobun/ee41b22f/blob-part-type-only
Draft

robobun wants to merge 2 commits into
robobun/ee41b22f/blob-store-no-typefrom
robobun/ee41b22f/blob-part-type-only

Conversation

@robobun

@robobun robobun commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

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?

main this PR Node 26
new Blob([typed]).type, new File([typed], "n").type a/b "" ""
Multipart part of new File([audioBlob], "a.mp3") audio/mpeg application/octet-stream application/octet-stream
toFile(typedBlob, "doc.pdf").type in openai, @anthropic-ai/sdk, groq-sdk, cloudflare application/pdf "" ""
new Response(new Blob([typed])) Content-Type a/b none none
new File([b], name, { type: b.type }), a Bun.file() part, a Blob that is a body no change no change

Problem

  • new Blob([part]) and new File([part], name) with one in-memory Blob part return a copy of the part that keeps its type. The File API takes the bytes from the parts and the type from options.type. Node and browsers give "". Bun also gives "" for two parts.
  • The cause is the one-part fast path in from_js_without_defer_gc (Blob.rs:3098 on main), which returns blob.dupe().

Fix

  • For a part that has a type and is in memory, the fast path returns dupe_without_type(): the same store, window and name, and no type. It copies no bytes, as before.
  • A part that is a file (Bun.file(), S3, fs.openAsBlob(), an embedded file) keeps the full copy, because its type is read from the file. This exception is provisional.
  • Only the two constructors change (REQUIRE_ARRAY). A Blob that is a body keeps its type.
  • Verified: 28 tests in blob.test.ts. 26 fail on the head of Blob: remove the type from the shared byte store #46114.
  • Self-reviewed in an earlier shape, which also dropped the name of a File part: 6 concerns raised, 6 addressed.

Background

Downsides

  • An upload that relied on the copied type sends application/octet-stream (the table). The migration is new File([b], name, { type: b.type }), which gives the same result on main and on Node.
  • A Bun.serve handler that returns new Response(new Blob([htmlBlob])) sent text/html;charset=utf-8. It now sends text/plain;charset=utf-8 for ASCII bytes and application/octet-stream for other bytes.
  • new Blob([untypedBlob]) goes from 470 to 476 instructions, and new 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. typed is new Blob(["hi"], { type: "a/b" }).

Row main this PR Node 26
new Blob([typed]).type with no options, {}, { type: undefined } or a type that is not valid a/b "" ""
new File([typed], "n").type a/b "" ""
new Blob([file]).type, file is new File(["hi"], "a.txt", { type: "text/x" }) text/x "" ""
Multipart part of new File([audioBlob], "a.mp3") audio/mpeg application/octet-stream application/octet-stream
Multipart part of form.append("e", new Blob([file])) filename="a.txt", text/x filename="a.txt", application/octet-stream filename="blob", application/octet-stream
new Response(new Blob([typed])), new Request(url, { body: new Blob([typed]) }) Content-Type a/b none none
(await new Response(new Blob([typed])).blob()).type a/b text/plain;charset=utf-8 ""
Bun.serve handler, new Response(new Blob([htmlBlob])), ASCII bytes text/html;charset=utf-8 text/plain;charset=utf-8
The same, bytes that are not ASCII text/html;charset=utf-8 application/octet-stream
The same as a static route text/html;charset=utf-8 none

The text/plain;charset=utf-8 rows are what Bun gives every Blob that has no type: (await new Response(new Blob(["hi"])).blob()).type is text/plain;charset=utf-8 on 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]).name and instanceof 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:

file main this PR Node 26
await fetch(url) audio/mpeg application/octet-stream application/octet-stream
toFile(typedBlob, "audio.mp3") audio/mpeg application/octet-stream application/octet-stream
toFile(fetch(url), "audio.mp3") audio/mpeg application/octet-stream application/octet-stream
new File([bytes], "audio.mp3", { type }) audio/mpeg audio/mpeg audio/mpeg
Bun.file(path) audio/mpeg audio/mpeg
fs.createReadStream(path), toFile(buffer, "audio.mp3") application/octet-stream application/octet-stream application/octet-stream

toFile(typedBlob, "doc.pdf").type is application/pdf on 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:

The file test. store_is_a_file() reads Bytes.stored_name to 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.

Call #46114 this PR
new Blob(["hello world"]) 760 759
new Blob([new Uint8Array(11)]) 613 613
new Blob(["hello world"], { type: "text/plain" }) 2078 2077
new File(["x"], "n") 760 759
new Blob([untypedBlob]) 470 476
new Blob([typedBlob]), heap-owned type 464, 2 atomic ops 481, 1 atomic op
new Blob([typedBlob]), interned type 461 481
new Blob([typedBlob], { type: "c/d" }) 1545, 9 atomic ops 1550, 7 atomic ops
new Blob([file]) 472 489
new Blob([Bun.file(p)]) 470 488
new File([untypedBlob], "n") 577 583
new File([typedBlob], "n") 571 588

One malloc per call with a Blob part on both builds. Release .text 81,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.serve handler rows, keep the filename of a File part, find an embedded file by its path and not by a non-empty stored_name, pin each kind of file part, keep the #33599 test on its path.

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

robobun commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:55 AM PT - Oct 20th, 2026

❌ @robobun, your commit 9f7f49f has 1 failures in Build #129401 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 46173

That installs a local version of the PR into your bun-46173 executable, so you can run:

bun-46173 --bun

@robobun

robobun commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator Author

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]));
main and #46114: ["a/b","a/b",""]
this branch and node v26.3.0: ["","",""]

Tests. 28 new tests in blob.test.ts. 26 fail on a release build of #46114. The other two pin rows that must not move: a Blob that is a body, and a part that is a file.

CI. 180 of 181 jobs pass in build 129401. The one failed job is darwin any aarch64 - test-bun. It ran no test: the agent darwin-aarch64-26.6.2-1 timed out after 120 s while it downloaded the build artifact. The same agent failed the same way on other PRs in that hour. The same lane is green on #46114, which this PR is stacked on.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant