Skip to content

FormData: name an unnamed Blob entry "blob", and let the filename argument name a File entry - #45373

Open
robobun wants to merge 5 commits into
mainfrom
robobun/14cd5d82/formdata-entry-name
Open

robobun wants to merge 5 commits into
mainfrom
robobun/14cd5d82/formdata-entry-name

Conversation

@robobun

@robobun robobun commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #14725

Part of #43691 and #35439.

Problem

  • fd.append("f", new Blob(["x"])) serializes as filename="". The spec, Node and browsers send filename="blob". Strict servers reject the part.
  • fd.append("f", file, "renamed.txt") keeps the File's own name on the value that fd.get() returns.
  • JSDOMFormData.cpp:311 defaults the entry filename to the Blob's name or an empty string. Blob__setAsFile (src/runtime/webcore/Blob.rs:3997) applies it only to a Blob with no name.

Fix

  • entryFileName() follows create an entry: the filename argument, else the Blob's own name, else "blob".
  • toJS names a dupe of the entry's Blob, so the caller's File keeps its name.
  • get_loader (Blob.rs:1957) uses the type when the name has no known extension, so an entry named "blob" still imports by its type.
  • Verified: test/js/web/html/FormData.test.ts, FormData-multipart-serialization.test.ts and test/js/web/fetch/blob.test.ts. 13 tests fail on main.

Background

Downsides

  • An unnamed Blob goes out as filename="blob", and fd.get() returns a File named "blob". A server that read the part as a text field now sees a file part. Bun.serve sends that name in content-disposition.
  • A typed File whose name has no known extension imports by its type, not as TSX.
  • fd.set(name, blob): 664 to 701 instructions. fd.append(name, blob): 544 to 545. Allocations +0, .text +0 bytes, .data +64 bytes.
Notes

Before and after (Node v26.3.0 gives the "after" column, except for the two import rows, which Node does not have)

Call main this PR
fd.append("f", new Blob(["x"])), multipart part filename="" filename="blob"
same, fd.get("f").name undefined "blob"
fd.append("f", new File(["x"], "orig.txt"), "over.txt"), fd.get("f").name "orig.txt" "over.txt"
fd.append("f", new Blob(["x"]), ""), fd.get("f").name undefined ""
fd.append("f", file), fd.get("f").name the File's name the File's name
fd.set("f", new File(["x"], "a\uD800.txt")), fd.get("f").name "a\uD800.txt" "a\uFFFD.txt"
a parsed part with filename="", .name undefined ""
a WebSocket binaryType = "blob" message, .name undefined undefined
that message appended to a FormData filename="" filename="blob"
Bun.serve with new Response(fd.get("f")), entry from a Blob of type application/octet-stream with no name no Content-Disposition filename="blob"
import(URL.createObjectURL(fd.get("f"))), entry from a JSON Blob with no name { a: 1 } { a: 1 }
import(URL.createObjectURL(new File([json], "data", { type: "application/json" }))) BuildMessage (parsed as TSX) { a: 1 }

The Content-Disposition row. Bun.serve sends the name of a File body as the filename. A File that req.formData() parses from a part with filename="blob" (what a browser and Node send for an unnamed Blob) gets that header on main too. A test pins the header for an entry File.

The get_loader change. get_loader (Blob.rs:1957) is used only for an import of a blob: URL. The extension decides when the name has one that maps to a loader. If not, the type decides, as it does for a Blob with no name. Without this change, the import in the second to last row fails with this PR, because the entry has the name "blob".

Issue scripts

Measurements. Release builds of the merge base 5f5799e and of this PR (6e2f30e). The instruction counts are a gdb stepi count of one native call after 30 warm-up calls. The counts repeat exactly.

Native call Instructions Conditional branches Lock-prefixed malloc calls
fd.append(name, blob), Blob with no name 544 -> 545 65 -> 70 4 -> 9 2 -> 2
fd.append(name, file) 547 -> 549 65 -> 66 10 -> 10 2 -> 2
fd.set(name, blob), Blob with no name 664 -> 701 89 -> 99 6 -> 12 2 -> 2
fd.set(name, file) 673 -> 707 91 -> 96 14 -> 14 2 -> 2
fd.get(name) 534 -> 527 56 -> 59 3 -> 6 1 -> 1
  • set() now replaces a lone surrogate in the entry filename, as append() does. That is the call that adds the instructions to fd.set.
  • The added atomic refcount operations are the entry's name string "blob". A named File entry pays the same today. The default name is a static string, so there is no allocation.
  • slice(), text() and stream() of a Blob: 399, 543 and 408 instructions on both builds.
  • size: .text 106,006,149 -> 106,006,149 bytes, .data 115,160 -> 115,224 bytes.
  • No host function or extern is added. Dynamic exports: 692 -> 692.

Not changed here

Other open PRs in the same lines

Review. This change was reviewed together with a larger Blob change (file.slice() returns a plain Blob). The review asked to land this part alone, because it needs no design decision. The Blob change is #45401, on top of this one. The later commits answer the review comments on this PR: set() and append() store the same filename, the tests for a parsed part and for Bun.serve, and the get_loader change.


no test proof · iteration 11 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/web/html/FormData-multipart-serialization.test.ts

FormData.append(name, blob, filename) stored the filename on the C++
wrapper, and the multipart serializer read it from there. get(),
getAll() and the iterators returned a dupe of the original blob, and
Blob__setAsFile kept the dupe's own name when it had one. So the
filename override reached the wire but not JS.

The entry now owns the name. Blob__setAsFile always sets it on the
dupe that toJS returns. A Blob with no name becomes a File named
"blob", as the create-an-entry algorithm says, so its part serializes
with filename="blob" instead of filename="". An empty filename
argument gives the File an empty name.
A Blob that is not a FormData entry (a WebSocket message) has no
filename, so it keeps having no name. DOMFormData stores a non-null
filename for every entry, so the two cases can be told apart.

The default entry name "blob" is a static string: no allocation per
append.
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: ca694714-b788-4173-a987-72f0d7a9762a
📥 Commits

Reviewing files that changed from the base of the PR and between 1a2245b and 6e2f30e.

📒 Files selected for processing: 6
  • src/jsc/bindings/DOMFormData.cpp
  • src/jsc/bindings/blob.cpp
  • src/jsc/bindings/webcore/JSDOMFormData.cpp
  • src/runtime/webcore/Blob.rs
  • test/js/web/fetch/blob.test.ts
  • test/js/web/html/FormData.test.ts
💤 Files with no reviewable changes: 1
  • src/jsc/bindings/webcore/JSDOMFormData.cpp

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

Walkthrough

FormData Blob entries now receive the filename "blob" when no filename exists. Explicit and empty filenames are handled separately. Blob URL loader selection now checks MIME type when the filename extension has no configured loader.

Changes

FormData Blob Filename Handling

Layer / File(s) Summary
Blob filename state
src/jsc/bindings/blob.cpp, src/runtime/webcore/Blob.rs
Blob conversions preserve the distinction between a missing filename and an empty filename. File-name assignment accepts an empty string.
FormData entry filename selection
src/jsc/bindings/webcore/JSDOMFormData.cpp, src/jsc/bindings/DOMFormData.cpp
append and set select an explicit filename, retain an existing filename, or use "blob" when the Blob has no name. Filename handling sanitizes null values and replaces unpaired surrogates.
Filename behavior tests
test/js/web/html/FormData.test.ts, test/js/web/html/FormData-multipart-serialization.test.ts
Tests cover filename selection and preservation, empty filenames, and serialization through multipart data, responses, and WebSocket Blob messages.

Blob URL Loader Selection

Layer / File(s) Summary
Extension and MIME loader selection
src/runtime/webcore/Blob.rs, test/js/web/fetch/blob.test.ts
Blob URL loader selection checks the filename extension first, then checks the MIME type if no extension loader is configured. Tests cover extensionless Blob and File inputs and a JavaScript extension.

Suggested reviewers: dylan-conway, jarred-sumner

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 6e2f3

The supplied evidence identifies no issue that needs resolution before merging; normal checks still apply.

Pre-merge checks: Passed 3 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Out of Scope Changes check Warning The FormData and Blob filename changes support #14725. However, Blob::get_loader() now changes import loader selection for all Blobs with an unrecognized or missing filename extension. The added `te… Remove the unrelated Blob::get_loader() behavior change and its dedicated import-loader tests, or link and define an issue requirement that requires this behavior.
Passed Passed checks: 3 passed
Check name Status Explanation
Linked Issues check Passed Issue #14725 requires an unnamed Blob in multipart FormData to serialize with filename="blob". entryFileName() supplies that default for append() and set(). The entry name is applied to a dupl…
Title check Passed The title clearly summarizes the main changes: defaulting unnamed Blob entries to "blob" and applying the filename argument to the returned File entry.
Description check Passed The description explains the problem, implementation, scope, tradeoffs, and verification results. It does not use the exact template headings, but it provides equivalent content for both required sect…
Full details: Out of Scope Changes check

Explanation

The FormData and Blob filename changes support #14725. However, Blob::get_loader() now changes import loader selection for all Blobs with an unrecognized or missing filename extension. The added test/js/web/fetch/blob.test.ts cases cover this separate import behavior. The linked issue and current PR description do not require this behavior.

  • Fix all pre-merge checks with AI
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:05 PM PT - Oct 15th, 2026

❌ @robobun, your commit 6e2f30e has some failures in Build #126115 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 45373

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

bun-45373 --bun

@robobun

robobun commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

How I reproduced it. On main 5f5799e (debug and release builds) and on Bun 1.4.2:

const fd = new FormData();
fd.set("fname", new Blob(["hello, world"], { type: "text/plain" }));
console.log((await new Response(fd).text()).match(/Content-Disposition:[^\r\n]*/)[0]);
// main:    Content-Disposition: form-data; name="fname"; filename=""
// this PR: Content-Disposition: form-data; name="fname"; filename="blob"   (Node v26.3.0 prints the same)

const form = new FormData();
form.append("f", new File(["x"], "orig.txt"), "over.txt");
console.log(form.get("f").name);
// main: "orig.txt"    this PR: "over.txt"

test/js/web/html/FormData.test.ts, test/js/web/html/FormData-multipart-serialization.test.ts and test/js/web/fetch/blob.test.ts fail 13 tests with the source of main and pass with this PR.

The second PR on top of this one is the file.slice() change: #45401.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked the BunString ownership in the new paths: Blob__getFileNameString returns a +1 clone that transferToWTFString() consumes, and the Dead branch holds no reference; in toJS, Bun::toString borrows the entry name and Blob__setAsFile clones it into the dupe, which Blob__create then owns. I also looked at Bun.file()/S3 blobs appended with an explicit filename argument — the override naming the dupe is the intended entry semantics and does not touch the caller's blob.

Extended reasoning...

The change touches FormData entry naming across the C++ DOMFormData/JSDOMFormData bindings, blob.cpp toJS, and the Rust Blob__setAsFile/Blob__getFileNameString FFI helpers, with no security-sensitive surface. Inline findings already flag the set/append surrogate inconsistency, the un-updated toJSNewlyCreated caller, the changed multipart-parse entry point, and the Bun.file path-as-filename behavior, so a human should weigh those; the refcount and ownership paths across the FFI boundary were checked and are balanced.

Comment thread src/jsc/bindings/DOMFormData.cpp Outdated
Comment thread src/jsc/bindings/webcore/JSDOMFormData.cpp
Comment thread src/jsc/bindings/DOMFormData.cpp Outdated
Comment thread src/jsc/bindings/blob.cpp
Comment thread src/jsc/bindings/webcore/JSDOMFormData.cpp
append() replaced each lone surrogate in the filename. set() stored the
filename as it came. get() returns the entry filename, so the two gave
different names for the same File. Both now use one helper.

toJSNewlyCreated maps a null filename to no name, the same as toJS.

Tests: a lone surrogate through append() and set(), a parsed part with
filename="", and the Content-Disposition filename that Bun.serve sends
for an entry File.
Comment thread src/jsc/bindings/DOMFormData.cpp Outdated
Comment thread src/jsc/bindings/blob.cpp Outdated
Comment thread src/jsc/bindings/webcore/JSDOMFormData.cpp Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated
Comment thread src/runtime/webcore/Blob.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I also checked the reordered toJS in src/jsc/bindings/blob.cpp (dupe first, then Blob__setAsFile on the dupe): Blob__dupe's +1 is still consumed by Blob__create, and the entry's own impl is no longer mutated, so the caller's File keeps its name as the description claims. The points raised in the prior review (set-path sanitization, toJSNewlyCreated null-to-Dead mapping, parse-side filename="" and Bun.serve content-disposition coverage) are addressed in 1a2245b.

Extended reasoning...

The latest push adds sanitizedFileName() and fileNameOrDead() helpers shared by both append/set and both toJS overloads, and extends the test suite with the parse-side, Bun.serve and WebSocket variants. The remaining inline finding is a behavioral regression for createObjectURL imports of FormData entries, which is not a nit, so approval is not appropriate.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Still open from earlier reviews (1):

  • Unresolved: 1 minor or pre-existing.

Comment thread src/runtime/webcore/Blob.rs
Comment thread src/runtime/webcore/Blob.rs Outdated
…wn extension

get_loader used the TSX loader for every Blob that has a name with no
known extension, and ignored the type. A FormData entry made from an
unnamed Blob is now a File named "blob", so an import of that entry
through URL.createObjectURL parsed JSON as TSX. The extension decides
when the name has one that maps to a loader. If not, the type decides.

Also shorten the comments of the FormData entry helpers to one line.
They cover an empty File and an empty Blob with a filename argument,
one File appended under two names, and a File over Bun.file() that is
appended with no filename argument.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed the latest push (commits 1a2245b..6e2f30e) and found no bugs; the new commits address the points raised in the earlier inline threads. Because this changes user-visible Web API behavior (an unnamed Blob entry now serializes and reads back as "blob") and widens get_loader fallback beyond FormData, a human maintainer should still sign off on the behavior change.

What was reviewed:

  • Ownership in toJS(Blob&): Blob__dupe returns a fresh +1 allocation that Blob__create consumes, same transfer as before; Blob__setAsFile clones the borrowed BunString view, so no leak or double-free.
  • All callers of Blob__getFileNameString/Blob__setAsFile (both toJS overloads, entryFileName) now share the Dead-means-no-name convention; the set() path and the multipart parser's filename="" case are now sanitized and tested.
  • get_loader restructure: Path::loader returns None for an unknown extension, so a name like "blob" or "data" falls through to the MIME type rather than forcing Tsx; this also changes blob: imports of any extension-less File, which the new blob.test.ts case covers.
Extended reasoning...

The diff touches FormData entry naming across three C++ binding files and Blob.rs, plus three test files; it changes the default entry filename to "blob", makes an explicit filename argument win over a File's own name, dupes the entry before naming so the caller's File is not mutated, and restructures loader selection for named Blobs. No security-sensitive surface (no auth, crypto, or path handling beyond the pre-existing Bun.file name behavior). The refcount transfer, the Dead/Empty/null distinction across every caller, and the get_loader fallback were verified from source and are covered by new tests. Defer rather than approve because the change deliberately alters user-visible wire output and blob: import loader selection, which is a product decision a maintainer should confirm.

Still open from earlier reviews (1):

  • Unresolved: 1 minor or pre-existing.

@robobun

robobun commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

On the out-of-scope check for Blob::get_loader(): this PR needs that change.

An entry made from an unnamed Blob is now a File named "blob". get_loader used the TSX loader for each name that has no known extension. So import(URL.createObjectURL(fd.get("f"))) of a JSON Blob entry works on main and failed on the first commits of this PR (the review found it). get_loader now uses the type when the name has no known extension. The test "blob: the type picks the loader when the name has no known extension" in test/js/web/fetch/blob.test.ts covers it.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

multipart/form-data filename attribute behavior differs from node

2 participants