Handle multipart/form-data upload in Bun
--
Introduction
In many applications, it is possible for a user to be presented with a form. The user will fill out the form, including information that is typed, generated by user input, or included from files that the user has selected. When the form is filled out, the data from the form is sent from the user to the receiving application.
The multipart/form-data contains a series of parts. Each part is expected to contain a content-disposition header, where the disposition type is “form-data”, and where the disposition contains an (additional) parameter of “name”, where the value of that parameter is the original field name in the form.
A multipart/form-data is a very commonly used format for sending mixed data such as fields and files. In this article, we’ll see how to handle multipart/form-data uploads in Bun.
Native support
As of Bun v0.5.6:
- There is no native API to handle multipart/form-data.
- There is no native support for the web standard FormData APIs (Deno supports this)
Using multer
In the Node.js world, express+multer is a very popular combination for handling multipart uploads. The multer middleware has about 3.4M weekly downloads.
As of Bun v0.5.6, express+multer fails to parse the multipart data in Bun. There is no exception thrown, but there is no data available either. The req.body comes as {}, and req.files comes as [].
const express = require("express");
const multer = require("multer");
const upload = multer({ dest: "./" });
const app = express();
app.post("/", upload.any(), function (req, res, next) {
console.log("Fields=", req.body, "\nFiles=", req.files);
const resp = {
numFields: Object.keys(req.body).length,
numFiles: Object.keys(req.files).length,
};
res.json(resp);
});
The output is as follows:
> curl -F file=@./sample.txt -F name=Mayank http://localhost:3000/
{"numFields":0,"numFiles":0}
> bun run app.js
Fields= {}
Files= []
The exact same code works fine in Node.js.
Using formidable
Again, in the Node.js world, formidable is another solid option that can be used to parse an incoming body from Node’s native HTTP request. Formidable has around 7M weekly downloads on NPM.
As of Bun v0.5.6, the formidable library throws an exception when trying to parse the incoming form.
const http = require("http");
const formidable = require("formidable");
http.createServer((req, res) => {
const form = formidable({ multiples: true });
form.parse(req, (err, fields, files) => {
if (err) {
res.writeHead(err.httpCode || 400, { "Content-Type": "text/plain" });
res.end(String(err));
return;
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ fields, files }, null, 2));
});
}).listen(3000);
The output is as follows:
> curl -F file=@./sample.txt -F name=Mayank http://localhost:3000/
curl: (52) Empty reply from server
> bun run app.js
22 | this.hash = null;
23 | }
24 | }
25 |
26 | open() {
27 | this._writeStream = new fs.WriteStream(this.filepath);
^
TypeError: undefined is not a constructor (evaluating 'new fs.WriteStream(this.filepath)')
at open (/Users/mayankc/Work/source/bunExamples/node_modules/formidable/src/PersistentFile.js:27:24)
at _handlePart (/Users/mayankc/Work/source/bunExamples/node_modules/formidable/src/Formidable.js:341:4)
.....
Using parse-multipart-data
You might be wondering if it is even possible to handle multipart/form-data in Bun? It is, but through a relatively unknown NPM module: parse-multipart-data. This module works fine with Bun. The feature set is very limited, but the body gets parsed. This module has around 80K weekly downloads on NPM.
const multipart = require("parse-multipart-data");
Bun.serve({
fetch: async (req) => {
const rawBody = Buffer.from(await req.arrayBuffer());
const boundary =
req.headers.get("content-type").split(";")[1].split("=")[1];
const parts = multipart.parse(rawBody, boundary);
const resp = {
numFields: 0,
numFiles: 0,
};
for (const p of parts) {
console.log("Found part=", p);
if (p.filename) {
resp.numFiles++;
} else {
resp.numFields++;
}
}
return Response.json(resp);
},
});
The output is as follows:
> curl -F file=@./sample.txt -F name=Mayank http://localhost:3000/
{"numFields":1,"numFiles":1}
> bun run app.js
Found part= {
filename: "sample.txt",
type: "text/plain",
name: "file",
data: Uint8Array(41) [ 76, 101, 97, 114, 110, 105, 110, 103, 32, 74, 97, 118, 97, 83, 99, 114, 105, 112, 116, 32, 38, 32, 84, 121, 112, 101, 115, 99, 114, 105, 112, 116, 32, 73, 115, 32, 70, 117, 110, 33, 10 ]
}
Found part= {
name: "name",
data: Uint8Array(6) [ 77, 97, 121, 97, 110, 107 ]
}
As mentioned earlier, this library/module has a very limited feature set, but it works. The parsed fields/files still needs to be parsed. An example is below:
const multipart = require("parse-multipart-data");
Bun.serve({
fetch: async (req) => {
const rawBody = Buffer.from(await req.arrayBuffer());
const boundary =
req.headers.get("content-type").split(";")[1].split("=")[1];
const parts = multipart.parse(rawBody, boundary);
for (const p of parts) {
if (p.filename) {
console.log("File = ", new TextDecoder().decode(p.data));
} else {
console.log("Field = ", new TextDecoder().decode(p.data));
}
}
return new Response(null, { status: 202 });
},
});
The output is as follows:
> curl -F file=@./sample.txt -F file2=@./sample2.txt -F name=Mayank -F city=Vegas http://localhost:3000/
> bun run app.js
File = Learning JavaScript & Typescript Is Fun!
File = Utilitatis causa amicitia est quaesita.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Collatio igitur ista te nihil iuvat. Honesta oratio, Socratica, Platonis etiam. Primum in nostrane potestate est, quid meminerimus? Duo Reges: constructio interrete. Quid, si etiam iucunda memoria est praeteritorum malorum? Si quidem, inquit, tollerem, sed relinquo. An nisi populari fama?
Quamquam id quidem licebit iis existimare, qui legerint. Summum a vobis bonum voluptas dicitur. At hoc in eo M. Refert tamen, quo modo. Quid sequatur, quid repugnet, vident. Iam id ipsum absurdum, maximum malum neglegi.
Field = Mayank
Field = Vegas