devkult_
tools30converters34
home/mime/multipart/form-data

multipart/form-data

Multipart form datarequest body

multipart/form-data

The request body for forms that upload files. Each field travels as its own part, and a boundary string separates the parts.

MIME typemultipart/form-data
Used forRequest body
Parametersboundary — required, generated by the client
SpecificationRFC 7578

What it looks like on the wire

POST /upload HTTP/1.1
Host: api.example.com
Content-Type: multipart/form-data; boundary=----devkult7MA4YWxk

------devkult7MA4YWxk
Content-Disposition: form-data; name="title"

Quarterly report
------devkult7MA4YWxk
Content-Disposition: form-data; name="file"; filename="report.pdf"
Content-Type: application/pdf

%PDF-1.7 …binary data…
------devkult7MA4YWxk--
Content-Type: multipart/form-data

Send it

HTML form
<form action="/upload" method="post" enctype="multipart/form-data">
  <input name="title">
  <input type="file" name="file">
  <button>Upload</button>
</form>
fetch with FormData
const body = new FormData();
body.append("title", "Quarterly report");
body.append("file", fileInput.files[0]);

// Don't set Content-Type yourself: the browser adds it with the boundary.
await fetch("/upload", { method: "POST", body });
curl
curl -F title="Quarterly report" -F file=@report.pdf https://api.example.com/upload

Receive it

Express (multer)
import multer from "multer";

const upload = multer({ dest: "uploads/", limits: { fileSize: 10 * 1024 * 1024 } });

app.post("/upload", upload.single("file"), (req, res) => {
  // req.body.title, req.file.originalname, req.file.size
});
Web standard Request (Next.js route handlers, Deno, Bun, Workers)
export async function POST(req: Request) {
  const form = await req.formData();
  const file = form.get("file") as File;
  const bytes = await file.arrayBuffer();
}
Python (Flask)
from werkzeug.utils import secure_filename

@app.post("/upload")
def upload():
    title = request.form["title"]
    f = request.files["file"]
    f.save(os.path.join(UPLOAD_DIR, secure_filename(f.filename)))
Go
r.ParseMultipartForm(10 << 20) // up to 10 MB in memory, the rest on disk
file, header, err := r.FormFile("file")

Good to know

  • Never set Content-Type: multipart/form-data by hand when sending a FormData body with fetch or axios. Without the boundary parameter the server can't split the parts — the usual cause of an empty req.body or a "Multipart: Boundary not found" error.
  • Express's express.json() and express.urlencoded() skip multipart bodies. You need a multipart parser such as multer, busboy or formidable.
  • A part's filename and its own Content-Type are whatever the client claims. Check the file's signature on the server and generate your own filename before saving — see the MIME type list for signatures.
  • For a single large file, a raw PUT with the file's own type (or a presigned upload URL) avoids the multipart overhead.

Frequently asked questions

When should I use multipart/form-data instead of application/x-www-form-urlencoded?

Whenever the form includes a file. URL encoding has no efficient way to carry binary data, while multipart sends each file as raw bytes in its own part. For forms with only short text fields, x-www-form-urlencoded is smaller and simpler.

What is the boundary in multipart/form-data?

A random string that doesn't occur in the content, declared in the header as boundary=…. Each part starts with -- plus the boundary, and the body ends with --, the boundary and -- again. When you use FormData or curl -F, the client picks it for you.

Why is req.body empty when I send FormData?

Either the server has no multipart parser (Express needs multer or similar), or the Content-Type was set by hand and lacks the boundary. Remove the manual header and let the browser set it.

Related types

Headers that go with it: Content-Type · Content-Disposition · Content-Length