Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
5bbadad926 fix: default FormData Blob filename to "blob" per XHR spec
When a plain Blob (not a File) is appended to FormData without an
explicit filename, the XHR spec requires the filename to default to
"blob". Previously Bun returned an empty string, causing servers that
check the filename to reject uploads with "No file sent" errors.

Closes #21788

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-20 04:47:47 +00:00
2 changed files with 88 additions and 1 deletions

View File

@@ -649,7 +649,10 @@ export fn Blob__getFileNameString(this: *Blob) callconv(.c) bun.String {
return bun.String.fromBytes(filename);
}
return bun.String.empty;
// Per the XHR spec ("create an entry" algorithm), when a Blob (not a File)
// is appended to FormData without an explicit filename, the name should
// default to "blob". See: https://xhr.spec.whatwg.org/#dom-formdata-append
return bun.String.static("blob");
}
comptime {

View File

@@ -0,0 +1,84 @@
import { expect, test } from "bun:test";
// https://github.com/oven-sh/bun/issues/21788
// When a plain Blob (not a File) is appended to FormData without an explicit
// filename, the XHR spec says the filename should default to "blob".
// Previously Bun set it to "" (empty string).
test("FormData.set with Blob defaults filename to 'blob'", async () => {
using server = Bun.serve({
port: 0,
fetch(req) {
return req.text().then(t => new Response(t));
},
});
const formData = new FormData();
formData.set("file", new Blob(["hello"], { type: "text/plain" }));
const res = await fetch(server.url, { method: "POST", body: formData });
const text = await res.text();
// The Content-Disposition header in the multipart body should contain filename="blob"
expect(text).toContain('filename="blob"');
});
test("FormData.append with Blob defaults filename to 'blob'", async () => {
using server = Bun.serve({
port: 0,
fetch(req) {
return req.text().then(t => new Response(t));
},
});
const formData = new FormData();
formData.append("file", new Blob(["hello"], { type: "text/plain" }));
const res = await fetch(server.url, { method: "POST", body: formData });
const text = await res.text();
expect(text).toContain('filename="blob"');
});
test("FormData.set with Blob and explicit filename uses provided name", async () => {
using server = Bun.serve({
port: 0,
fetch(req) {
return req.text().then(t => new Response(t));
},
});
const formData = new FormData();
formData.set("file", new Blob(["hello"], { type: "text/plain" }), "custom.txt");
const res = await fetch(server.url, { method: "POST", body: formData });
const text = await res.text();
expect(text).toContain('filename="custom.txt"');
});
test("FormData.set with File preserves File name", async () => {
using server = Bun.serve({
port: 0,
fetch(req) {
return req.text().then(t => new Response(t));
},
});
const formData = new FormData();
formData.set("file", new File(["hello"], "myfile.txt", { type: "text/plain" }));
const res = await fetch(server.url, { method: "POST", body: formData });
const text = await res.text();
expect(text).toContain('filename="myfile.txt"');
});
test("FormData.get returns File with name 'blob' for plain Blob", () => {
const formData = new FormData();
formData.set("file", new Blob(["hello"], { type: "text/plain" }));
const entry = formData.get("file");
expect(entry).toBeInstanceOf(File);
expect((entry as File).name).toBe("blob");
});