Repository navigation
Conversation
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.
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.
…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.
Blob::dupe() copied the File brand, name and lastModified, so every producer built on it handed the source File's identity to a new object: file.slice(), new Blob([file]), and Response/Request.blob(). The default copy of a File is now a plain view of its bytes. The holders that give the same File back to JS ask for dupe_with_identity(): a FormData entry, a Response body (for the Content-Disposition filename and clone()), an object URL, and a route that reuses a Response. Body.blob() strips the identity where the Blob leaves the body. A view of a File has no name, so name_detached stops the fallback to the store's path (a Bun.file path, an S3 key, an embedded file path). Structured clone carries the flag in a new version 5 byte.
slice, the Blob constructor, Body.blob(), structured clone, FormData entries and the Content-Disposition filename that Bun.serve sends.
The stream sources keep the store, not the Blob, so the Blob rebuilt from an unread stream fell back to the store's path. They now carry name_detached. Also pins the Content-Disposition filename of an array body.
The loop reads each object URL through fetch().blob() again, and checks that the Blob has no name.
|
Status: ready for review. It is stacked on #45373, and it asks one design question (see the Background section). How I reproduced it. On main 5f5799e (debug and release builds) and on Bun 1.4.2: const file = new File(["0123456789"], "report.pdf", { type: "application/pdf", lastModified: 1234 });
const chunk = file.slice(0, 5);
console.log(chunk instanceof File, chunk.name, chunk.lastModified);
// main: true "report.pdf" 1234
// this PR: false undefined <the value of a plain Blob> (Node v26.3.0: false undefined undefined)
const fd = new FormData();
fd.append("chunk", chunk);
console.log((await new Response(fd).text()).match(/filename="[^"]*"/)[0]);
// main: filename="report.pdf"
// this PR: filename="blob" (Node prints the same)The repro script from the report (6 checks on the chunk, 5 controls) ends with exit code 1 on main ("6 of 6 checks treat the chunk as the whole file") and with exit code 0 on this PR.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
WalkthroughBlob and File copies now track whether File identity and filename state are retained or detached. The change updates Blob conversions, serialization, streams, and server paths. FormData append and set now apply filename conversion and use ChangesBlob and File semantics
Suggested reviewers: Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
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 version-5 structured-clone pair (the trailing name_detached byte is written at Blob.rs:773 and read only for version >= 5 at Blob.rs:3935, with versions 1-4 still breaking out before it) and the ByteBlobLoader/FileReader/ReadableStream blob-to-stream-to-blob round trip, where the flag is set from is_file_or_view() at construction and copied back at every rebuild site. The Content-Disposition change for new Response(file.stream()) over a Bun.file()-backed File and the absolute-path multipart filename for a bare Bun.file() entry were looked at and are respectively intended by this PR's contract and pre-existing on the base branch.
Extended reasoning...
The PR changes the default semantics of Blob::dupe() for File sources across 32 Rust call sites, adds a name_detached field with a serialization version bump, and reworks FormData entry naming in C++ and Rust; it touches no auth, crypto or injection surface. The inline findings already call for a human look, and the PR itself asks a maintainer whether the changed dupe() default is wanted, so this note only records what else was examined.
| let name = self.name.get(); | ||
| if !name.is_empty() { | ||
| return Some(name.to_utf8()); | ||
| } | ||
| self.store_path().map(bun_core::Utf8Bytes::Borrowed) | ||
| self.store_name().map(bun_core::Utf8Bytes::Borrowed) |
There was a problem hiding this comment.
🟡 (optional) A server that returns a FormData entry appended with an explicit empty filename now sends the underlying disk file's basename in Content-Disposition, where the base branch sent the File's own name. Blob__setAsFile at src/runtime/webcore/Blob.rs:4008 now overwrites name whenever the entry filename is not Dead, so fd.append(k, new File([Bun.file(p)], "public.csv"), "") followed by fd.get(k) yields a File whose name is "". get_file_name (src/jsc/webcore_types.rs:472-476) treats "" as no name and falls back to store_name, which is still the path because the identity copy is not name_detached. …
Why this was flagged
…Fix: an entry whose filename is "" must not fall back to the store path; either mark the store-path fallback off when a File has an explicit (even empty) name, or make get_file_name return None for a File with an empty name at every site (RequestContext.rs:3872, multipart entryFileName).
Trigger: fd.append("f", new File([Bun.file("/srv/private/report-q3.csv")], "public.csv"), ""), then new Response(fd.get("f")) served by Bun.serve. On base, Blob__setAsFile only set the name when path_str was non-empty and no name existed, so fd.get kept "public.csv" and the response carried filename="public.csv". After the diff, src/runtime/webcore/Blob.rs:4008 sets name to "" because the sanitized entry filename is emptyString() (src/jsc/bindings/DOMFormData.cpp:103), not Dead. fd.get returns dupe_with_identity() (Blob.rs:4016) so name_detached stays false, and src/jsc/webcore_types.rs:472-476 falls through store_name() to the Bun.file path; src/runtime/server/RequestContext.rs:3872-3888 writes content-disposition: filename="report-q3.csv".
Verification: nit — trigger: fd.append(k, new File([Bun.file(p)], "x"), "") (explicit empty filename on a File that wraps a disk-backed Bun.file) followed by new Response(fd.get(k)) served by Bun.serve, with a content type whose category autosets the filename (application/*, octet-stream — NOT text/csv, see below).
Mechanism verified:
- src/jsc/bindings/webcore/JSDOMFormData.cpp:291-292
entryFileName: a non-undefined filename arg wins →""(non-null empty WTF::String).
| let from_extension = self.get_file_name().and_then(|filename| { | ||
| bun_resolver::fs::Path::init(&filename).loader(&jsc_vm.transpiler.options.loaders) | ||
| }); | ||
| if let Some(loader) = from_extension { | ||
| return Some(loader); | ||
| } | ||
| if let Some(mime_type) = self.get_mime_type_or_content_type() { | ||
| return Some(bun_ast::Loader::from_mime_type(mime_type)); |
There was a problem hiding this comment.
🟡 (optional) Users who import or run a Worker from an object URL of a named File with an unknown-extension name and a text/* type get a string module instead of executing their code. In get_loader (src/runtime/webcore/Blob.rs:1962-1969) a name whose extension maps to no loader now falls through to the type, and Loader::from_mime_type turns any Category::Text type (text/plain, text/typescript, text/x-...) into Loader::Text. On the base branch a named Blob always got Loader::Tsx there. Fix: when the name has no known extension, let the type pick the loader only when it maps to a code or data loader, and keep the permissive Tsx fallback for Category::Text types so named Files behave as before while fd.get() entries named "blob" with application/json still load as JSON.
Why this was flagged
A user creates new File([code], "worker", { type: "text/plain" }) (or type "text/typescript") and passes URL.createObjectURL(file) to new Worker() or import(); the blob: specifier reaches get_loader through src/runtime/jsc_hooks.rs:3970. src/runtime/webcore/Blob.rs:1962 computes from_extension, which is None for "worker", so line 1968 takes get_mime_type_or_content_type and line 1969 calls Loader::from_mime_type. src/bundler/options.rs:402-403 maps mime_type.category == Category::Text to Loader::Text. The module is then loaded as text: import() resolves to a string and a Worker runs no code. The base branch's get_loader returned Some(Tsx) via unwrap_or whenever get_file_name was Some, so the same File executed as TSX. The new test at test/js/web/fetch/blob.test.ts only covers application/json, which lands on the Json loader, so the Category::Text regression is unpinned.
Verification: Triggered when a blob: URL of a named File whose name has no known extension and whose type is a non-JS text/* is imported or used as a Worker script. Base: any named Blob got Tsx. Head src/runtime/webcore/Blob.rs:1962-1970: when from_extension is None it falls to Loader::from_mime_type, and src/bundler/options.rs:402-403 turns Category::Text into Loader::Text, so the File loads as a text module instead of executing.
| ), | ||
| charset: Cell::new(blob.charset.get()), | ||
| is_jsdom_file: Cell::new(blob.is_jsdom_file.get()), | ||
| name_detached: Cell::new(blob.name_detached.get()), |
There was a problem hiding this comment.
🟣 pre-existing, not blocking: pre-existing: a user who passes a Blob or File inside an array body, new Response([file]), finds their original object emptied afterwards. The single-element array fast path at src/runtime/webcore/Blob.rs:3098 calls blob.take_store() on the caller's own JS Blob when MOVE is true, which nulls that object's store while its size stays. Fix: the array body path must copy the store ref (dupe_with_identity()-style +1) instead of moving it out of a JS-owned Blob, so file.text() still returns the bytes after the Response is built, while the body still keeps the File's name as the new /array-body test pins.
A small fix can ride a push you are already making; otherwise a short reply is enough.
Why this was flagged
Trigger: const f = new File(["abc"], "a.txt"); const r = new Response([f]); await f.text(). Body.rs:1056 sends an array body to Blob::get::<true, false> (from_js_move). In Blob.rs:3035-3043 a one-element array sets top_value to the user's File, and at Blob.rs:3084-3098 the MOVE arm does store: JsCell::new(blob.take_store()), where take_store (src/jsc/webcore_types.rs:264) is self.store.replace(None) on the JS wrapper's Blob. After the call the user's File has store == None and size == 3; shared_view() (webcore_types.rs:321-324) returns b"", so f.text() yields "" and f.size still reports 3. This diff does not introduce the move (it only adds the name_detached copy at Blob.rs:3105), so the consequence is the same on the base branch; the PR text calls this path 'moves the File into the body' and the new test at test/js/web/fetch/blob.test.ts:1153 only uses a temporary makeFile(), so nothing exercises the source after the move. No safeguard prevents it: the non-array path (Body.rs:1012) copies with dupe_with_identity(), only the array literal moves.
Verification: pre-existing. Trigger: any user passes their own Blob/File as a single-element array body and then reads the original object afterwards. At src/runtime/webcore/Blob.rs:3078-3098 the DOMWrapper arm builds the body Blob with store: JsCell::new(blob.take_store()); take_store is self.store.replace(None) on the caller's object, so file.text() resolves to "".
| pub(crate) extern "C" fn Blob__setAsFile(this: &mut Blob, path_str: &BunString) { | ||
| this.is_jsdom_file.set(true); | ||
| if !path_str.is_empty() && this.get_file_name().is_none() { | ||
| if path_str.tag() != bun_core::Tag::Dead { | ||
| this.name.set(path_str.clone()); | ||
| } | ||
| } |
There was a problem hiding this comment.
🟣 pre-existing, not blocking: pre-existing: WebSocket binaryType = "blob" message data and any other non-FormData Blob handed to toJS(Blob&) is reported as instanceof File even though it has no name. Blob__setAsFile at src/runtime/webcore/Blob.rs:4008 sets is_jsdom_file to true unconditionally, and src/jsc/bindings/blob.cpp:20 now calls it for every Blob, passing a Dead string for the not-an-entry case. Fix: brand the copy as a File only when the entry filename is not Dead (or skip Blob__setAsFile in blob.cpp when entryName.isNull()), while keeping FormData entries, including those with an empty filename, as Files.
A small fix can ride a push you are already making; otherwise a short reply is enough.
Why this was flagged
A client sets ws.binaryType = "blob" and the server sends a binary frame. src/jsc/bindings/webcore/WebSocket.cpp:1291 builds a WebCore::Blob with a null fileName and dispatches a MessageEvent; src/jsc/bindings/webcore/JSMessageEventCustom.cpp:64 converts it with toJS<IDLInterface<Blob>>, which lands in toJS(JSC::JSGlobalObject*, JSDOMGlobalObject*, WebCore::Blob&) at src/jsc/bindings/blob.cpp:15. The new code builds BunString { BunStringTag::Dead } via fileNameOrDead and calls Blob__setAsFile(dupe, &filename) at blob.cpp:20. Blob__setAsFile at src/runtime/webcore/Blob.rs:4008 runs this.is_jsdom_file.set(true) before the Dead check, so event.data instanceof File is true and event.data.name is undefined. The base branch already branded the shared impl the same way, so this is pre-existing, but the PR's own comment at blob.cpp:9 says a null fileName means "not a FormData entry", and the new FormData test at test/js/web/html/FormData.test.ts ("a WebSocket Blob message has no name") only asserts name and never checks instanceof File, so the mismatch is not caught.
Verification: pre-existing. Trigger: a WebSocket client with binaryType = "blob" receives a binary frame. toJS(Blob&) at src/jsc/bindings/blob.cpp:15 calls Blob__setAsFile(dupe, &filename) for every Blob, with a Dead string for the null-name case. src/runtime/webcore/Blob.rs:4006-4011 sets is_jsdom_file unconditionally, so event.data instanceof File === true. No test in test/js/web/websocket pins either behavior.
| if path_str.tag() != bun_core::Tag::Dead { | ||
| this.name.set(path_str.clone()); | ||
| } | ||
| } |
There was a problem hiding this comment.
🟣 pre-existing, not blocking: Callers of fd.get() on an entry appended as a plain Blob receive a File whose lastModified is 0 (1970) rather than the time of the append, which browsers and Node return. Blob__setAsFile at src/runtime/webcore/Blob.rs:4007-4011 sets is_jsdom_file and name but never sets last_modified, while the lastModified getter at Blob.rs:1997-1998 reports the raw field once is_jsdom_file is true. Fix: when a Blob that was not a File is turned into an entry File, set last_modified to the current time (the create-an-entry step this PR implements), for both append and set entries.
A small fix can ride a push you are already making; otherwise a short reply is enough.
Why this was flagged
Trigger: fd.append("f", new Blob(["x"])) then fd.get("f").lastModified, or multipart parsing via WebCore__DOMFormData__appendBlob (bindings.cpp:6306) followed by get. toJS(Blob&) at src/jsc/bindings/blob.cpp:19-20 dupes and calls Blob__setAsFile; at Blob.rs:4007 is_jsdom_file becomes true but last_modified stays the copied 0.0 from copy_fields (src/jsc/webcore_types.rs:397-401). The getter at Blob.rs:1997 returns 0 for any is_jsdom_file Blob, so the File reports epoch 0, and serialization at Blob.rs:759 persists it. The base branch had the same gap, but this PR rewrites the entry-creation path under the spec comment at JSDOMFormData.cpp:288 and adds tests pinning name, without the sibling lastModified rule from the same spec step. Population: every FormData consumer reading lastModified from a parsed or appended entry. Remedy: set last_modified to now in Blob__setAsFile when the source was not already a File.
Verification: pre-existing — triggered whenever a plain Blob (not a File) is appended to FormData and then read back via fd.get(). Blob__setAsFile (src/runtime/webcore/Blob.rs:4006-4011) sets is_jsdom_file and name but never touches last_modified, so the entry File reports lastModified === 0. No test in test/js/web/html/FormData.test.ts asserts lastModified for a plain-Blob entry.
|
Updated 4:14 PM PT - Oct 15th, 2026
✅ @robobun, your commit 734ff524ee86d8b284227d587cedbd5e00e7f86b passed in 🧪 To try this PR locally: bunx bun-pr 45401That installs a local version of the PR into your bun-45401 --bun |
Fixes #43691
Part of #35439. Stacked on #45373.
Problem
file.slice()returns aFilewith the whole file'snameandlastModified, so each upload chunk goes out asfilename="report.pdf". The File API returns a plainBlob.Blob::dupe_with_content_type(src/jsc/webcore_types.rs:356) copiesis_jsdom_file,nameandlast_modified.get_slice_from(src/runtime/webcore/Blob.rs:1800),new Blob([file])andresponse.blob()use that copy.Fix
Fileis a plain view of its bytes. Seven holders that return the File calldupe_with_identity(), andBody.blob()strips the identity.name_detachedstops the fallback to the store's path, so a slice ofnew File([Bun.file(p)], "public.csv")never reportsp.test/js/web/fetch/blob.test.ts, 23 of 26 new cases fail without the fix.Background
Blobis a window onto a refcounted store, anddupe()makes another Blob over it.instanceof Filereadsis_jsdom_file.name(Bun.file(p).name). That does not change.get_slice_from(webcore: File.prototype.slice() returns a plain Blob, not a File #33604): six other producers keep returning a File.dupe()wanted? Give File its own prototype that inherits from Blob.prototype #30328 already drops the brand there. The Notes give the other shape.Downsides
file.slice(),new Blob([file])andresponse.blob()lose the File'snameand are notinstanceof File.slice()399 to 395 instructions, 1 more branch. Allocations +0,.text+1,536 bytes,Blobstays 104 bytes.Notes
Stacking. The first five commits are #45373 (FormData entry names). Without it, a chunk that loses its name would go out as
filename="", which some servers read as a text field (#22762). With it, the chunk goes out asfilename="blob", the same as Node. When #45373 merges, I rebase this PR and only its own commits remain.Question for a maintainer.
Blob::dupe()changes meaning for aFilesource under 32 Rust call sites: the default copy is a byte view, and 7 holders ask for the exact copy. #30328 already makes this change for the brand on the same line. If the changed default is not wanted, the other shape is:dupe()stays exact, and slice (Blob.rs:1803), the Blob constructor fast path (Blob.rs:3113) andBody.blob()call a named plain copy.name_detached, the version 5 byte and the tests stay the same. I can make that change in this PR.Before and after (repro from the report; Node v26.3.0 gives the "after" column)
file.slice(0, 5) instanceof Filetruefalsefile.slice(0, 5).name"report.pdf"undefinedfile.slice(0, 5).lastModified1234structuredClone(file.slice(0, 5)) instanceof Filetruefalsefd.append("chunk", file.slice(0, 5)), multipart partfilename="report.pdf"filename="blob"new Blob([file]) instanceof Filetruefalse(await new Response(file).blob()) instanceof FiletruefalseBun.servewithnew Response(file.slice(0, 5))Content-Disposition: filename="report.pdf"Content-DispositionBun.servewithnew Response(file)filename="report.pdf"filename="report.pdf"new File([Bun.file(p)], "public.csv").slice(0, 5).name"public.csv"undefined(neverp)(await new Response(new File([Bun.file(p)], "public.csv").stream()).blob()).namepundefinedBun.file(p).slice(0, 5).nameppfd.get("f")of an appended File:name,lastModifiedThe 32
dupe()call sites (grep -rnE '\.dupe\(\)|borrowed_view\(\)|dupe_with_content_type\(|Blob::dupe\(|dupe_with_identity\(' src --include=*.rs, without the S3 credential dupes and the definitions)Keep the identity (
dupe_with_identity()), 7 sites:Blob.rs:4016(Blob__dupe)fd.get(); the 3 C++ callers areblob.h:53,blob.h:63,blob.cpp:19Body.rs:1016,Body.rs:1590clone(): the name is theContent-DispositionfilenameObjectURLRegistry.rs:43,:79blob:loader reads the nameStaticRoute.rs:196,FileRoute.rs:167Bytes only (the default copy), 25 sites. Nothing after them reads
name,last_modifiedor the brand: the readers areget_name_string,get_file_name(called fromRequestContext.rs:3872,get_loaderandAny::get_file_name) and structured clone.Blob.rs:1803slice(): the result goes to JS as a plain BlobBlob.rs:3113,:3122new Blob([file])goes to JS as a plain Blob,new File([file], name)sets its own nameBlob.rs:4471,:6240Bun.writeof bytes onto a bytes Blob, and a stream read back as a BlobBlob.rs:434,:598,:5570Blob.rs:1328,:4391,:4392,:4407,:4408,:4792,node/types.rs:1771Bun.writesources and destinationsnode/types.rs:148,:169,:190BlobOrStringOrBufferinputsshell/Builtin.rs:744,:765,shell/states/Cmd.rs:762,spawn/stdio.rs:494Bun.spawnstdioStaticRoute.rs:124,FileRoute.rs:181standalone_graph_jsc.rs:78File, so the copy keeps its nameThe array body literal (
Blob.rs:3092,new Response([file])) moves the File into the body and keeps its identity. The new test pins itsContent-Dispositionfilename.Tests. With the source of #45373, 25 tests fail: 23 in
test/js/web/fetch/blob.test.ts, 1 intest/js/web/html/FormData.test.tsand 1 intest/js/web/workers/worker_blob.test.ts. With the source of main, 38 fail. All pass with this PR (346 tests in the 4 files on the debug build).Measurements. Release builds of #45373 (6e2f30e) and of this PR (734ff52). The instruction counts are a gdb
stepicount of one native call after 30 warm-up calls. The counts repeat exactly. malloc calls and syscall instructions do not change on any row.blob.slice(0, 5), plain Blobslice,Bun.file()slice, Filefd.append(name, blob)fd.append(name, file)fd.set(name, blob)fd.set(name, file)fd.get(name)blob.text()blob.stream()new Response(blob)Response.blob()new Blob([blob])Bun.file(p).nameget_loader, comments and tests only.size_of::<Blob>(): 104 -> 104 bytes (release). The flag takes the padding byte beforeref_count, and a const assert in the tree keeps it there.size:.text106,006,149 -> 106,007,685 bytes (+1,536),.dataunchanged.bun:jscserialize(x).byteLength): Blob 42 -> 43, typed Blob 57 -> 58, File 76 -> 77, slice of a File 71 -> 58. Version 4 records written by the base binary deserialize on this build: 2 of 2 (a File and a slicedBun.file()). A test pins a version 4 File record.typerewrite, stays).Response/Request.blob(), FormData entries). Rows whoseinstanceof Fileornamediffer from Node, or that carry the File'slastModified: main 14, FormData: name an unnamed Blob entry "blob", and let the filename argument name a File entry #45373 13, this PR 0.Self-review: the 7 concerns
dupe()change. Addressed: FormData: name an unnamed Blob entry "blob", and let the filename argument name a File entry #45373 is separate and this PR is stacked on it.new Response(file.stream()).blob(), or.blob()after.bodywas read) lost the guard and reported the disk path. Addressed: the stream sources carryname_detached. main has the same leak today./array-bodyrow.Blob__dupekeeps the identity for every C++ caller. Kept: its 3 callers are the FormData entry paths.resolveObjectURL(URL.createObjectURL(file))still returns the File (Node returns a plain Blob). Kept: the cross-thread name test inworker_blob.test.tsneeds a visible name on a resolved object URL. It is one line inresolve_and_dupe_to_jsif a maintainer wants it.Not changed here
chunk.lastModifiedis the value every plain Blob reports (theINIT_TIMESTAMPsentinel), notundefined. The accessor is onBlob.prototype(MovenameandlastModifiedfromBlob.prototypetoFile.prototype#32434, Blob.lastModified: return 0 instead of the INIT_TIMESTAMP sentinel for unresolved stores #44154).slice()keeps an interned parenttype(Node:""). An existing test pins that.new Response([file])leaves the source File empty (Blob: keep the caller's Blob intact after a slice read, an array body, and a rejected bytes() #38503).Blob__setAsFile(Give File its own prototype that inherits from Blob.prototype #30328).Bun.inspect()of a Blob over a file store prints the path.Other open PRs in the same code
dupe_with_content_typeto drop the brand. This PR does that and also dropsnameandlastModified. The two conflict in 4 files (DOMFormData.cpp,blob.cpp,webcore_types.rs,Blob.rs). In the array body literal, this PR needs the brand to stay, becauseBody.blob()strips on the brand.nameandlastModifiedfromBlob.prototypetoFile.prototype#32434: its clears at each site are not needed after this PR. Its accessor move can land on top and giveslastModified === undefined. It also uses serialization version 5. The one that lands second takes 6.get_name_stringandBlob__getFileNameStringwithstore_path(). After this PR those fallbacks must usestore_name()to respectname_detached.name_detachedcovers.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