oven-sh / bun

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
https://bun.sh
Other
74.34k stars 2.78k forks source link

multipart/form-data filename attribute behavior differs from node #14725

Open jennydaman opened 1 month ago

jennydaman commented 1 month ago

What version of Bun is running?

1.1.29+6d43b3662

What platform is your computer?

Linux 6.11.3-arch1-1 x86_64 unknown

What steps can reproduce the bug?

Either using axios:

import axios from "axios";

async function main() {
  const formData = new FormData();
  formData.set("fname", new Blob(["hello, world"], {type: "text/plain"}));
  await axios.postForm("http://localhost:7777", formData);
}

main();

Or fetch:

async function main() {
  const formData = new FormData();
  formData.set("fname", new Blob(["hello, world"], {type: "text/plain", }));
  await fetch("http://localhost:7777", {method: 'POST', body: formData});
}

main();

Create a server listening on port 7777, e.g. using nc:

nc -l -p 7777

What is the expected behavior?

Whether using fetch or axios, node will set filename="blob"

Request using axios:

POST / HTTP/1.1
Accept: application/json, text/plain, */*
Content-Type: multipart/form-data; boundary=axios-1.7.7-boundary-Lmj4KlRCc2SagKIz9nPtBJfQt
User-Agent: axios/1.7.7
Content-Length: 209
Accept-Encoding: gzip, compress, deflate, br
Host: localhost:7777
Connection: keep-alive

--axios-1.7.7-boundary-Lmj4KlRCc2SagKIz9nPtBJfQt
Content-Disposition: form-data; name="fname"; filename="blob"
Content-Type: text/plain

hello, world
--axios-1.7.7-boundary-Lmj4KlRCc2SagKIz9nPtBJfQt--

Using fetch:

POST / HTTP/1.1
host: localhost:7777
connection: keep-alive
content-type: multipart/form-data; boundary=----formdata-undici-014304150360
accept: */*
accept-language: *
sec-fetch-mode: cors
user-agent: node
accept-encoding: gzip, deflate
content-length: 177

------formdata-undici-014304150360
Content-Disposition: form-data; name="fname"; filename="blob"
Content-Type: text/plain

hello, world
------formdata-undici-014304150360--

What do you see instead?

Using bun and axios:

POST / HTTP/1.1
accept: application/json, text/plain, */*
accept-encoding: gzip, compress, deflate, br
content-type: multipart/form-data; boundary=axios-1.7.7-boundary-ZX1iJMlBBoneOT45EFdzNh7BE
user-agent: axios/1.7.7
Connection: keep-alive
Host: localhost:7777
Content-Length: 206

--axios-1.7.7-boundary-ZX1iJMlBBoneOT45EFdzNh7BE
Content-Disposition: form-data; name="fname"
Content-Type: text/plain;charset=utf-8

hello, world
--axios-1.7.7-boundary-ZX1iJMlBBoneOT45EFdzNh7BE--

Notice how filename is missing completely.

Or using fetch:

POST / HTTP/1.1
Content-Type: multipart/form-data; boundary="-WebkitFormBoundary2e5faff92aaa4d40b62e67a823e208a9"
Connection: keep-alive
User-Agent: Bun/1.1.29
Accept: */*
Host: localhost:7777
Accept-Encoding: gzip, deflate, br
Content-Length: 227

---WebkitFormBoundary2e5faff92aaa4d40b62e67a823e208a9
Content-Disposition: form-data; name="fname"; filename=""
Content-Type: text/plain;charset=utf-8

hello, world
---WebkitFormBoundary2e5faff92aaa4d40b62e67a823e208a9--

Additional information

Possibly related to https://github.com/oven-sh/bun/issues/7917

guest271314 commented 1 month ago

Reproducible with Bun (1.1.31+e448c4cc3).

guest271314 commented 1 month ago

Is there any reason you don't set the file name directly at the third parameter to set() or append()?

jennydaman commented 1 month ago

Here I am presenting a minimal reproduction of the bug. In my actual use case, I found that a package I was using behaved differently from node on bun. The package wraps axios and FormData without providing a way to customize how either are used.

Of course, axios itself is a wrapper around node's http module so a more minimal reproduction is possible.

guest271314 commented 1 month ago

If you set(name, blobValue, filename) or append(name, blobValue, filename) the filename directly the expected result is achieved.


async function main() {
  const formData = new FormData();
  const blob = await new Response(formData).blob();
  formData.set("fname", new Blob(["hello, world"], {type: "text/plain", }), "blob");
  await fetch("http://localhost:7777", {method: 'POST', body: formData, headers: {
    "Content-Type": blob.type
  },});
}

main();
nc -l -p 7777
POST / HTTP/1.1
content-type: multipart/form-data; boundary="-WebkitFormBoundary7c9e7ef531cb4055a2eee908443c44df"
Connection: keep-alive
User-Agent: Bun/1.1.31
Accept: */*
Host: localhost:7777
Accept-Encoding: gzip, deflate, br
Content-Length: 231

---WebkitFormBoundaryb88376d3bb39427cb2b93b1919d1d1ac
Content-Disposition: form-data; name="fname"; filename="blob"
Content-Type: text/plain;charset=utf-8

hello, world
---WebkitFormBoundaryb88376d3bb39427cb2b93b1919d1d1ac--

While this language is on MDN

filename

Optional

The filename reported to the server (a string), when a Blob or File is passed as the second parameter. The default filename for Blob objects is "blob". The default filename for File objects is the file's filename.

the language

The default filename for Blob objects is "blob".

is not in the specification.

guest271314 commented 1 month ago

I think setting the file name to "blob" for the case of no filename explicitly set is by convention of stakeholders, that you might be able to find in issues in W3C, WHATWG repositories and in tests on WPT.