What is multipart/form-data Content-Type? File Uploads, Examples

multipart/form-data is the HTTP Content-Type for forms with files. Learn the boundary format, how to send it with fetch, curl and k6, and common errors.

What is multipart/form-data?

multipart/form-data is the HTTP Content-Type for sending a form that contains files or binary data. The request body is split into parts, one per field, separated by a boundary string, and each part carries its own headers. Browsers use it when a form has enctype="multipart/form-data", and it is defined in RFC 7578.

It exists because raw file bytes cannot be packed safely into the key=value&key=value text of application/x-www-form-urlencoded. Multipart lets text fields and binary files travel in one request.

How multipart/form-data works

The Content-Type header names a boundary, a string that must not appear anywhere in the content. Each part starts with -- plus the boundary, and the body ends with --, the boundary and -- again.

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

------example7MA4YW
Content-Disposition: form-data; name="username"

alice
------example7MA4YW
Content-Disposition: form-data; name="avatar"; filename="photo.jpg"
Content-Type: image/jpeg

(binary JPEG bytes)
------example7MA4YW--
  • Every part needs a Content-Disposition: form-data header with the field name. File parts also carry a filename.
  • A part's own Content-Type is optional. Without one, the part is read as text/plain. Files should name their media type, or application/octet-stream when it is unknown.
  • Do not add Content-Transfer-Encoding. RFC 7578 says senders should not use it in HTTP.

multipart/form-data vs urlencoded vs JSON

  • multipart/form-data handles files and binary fields, at the cost of boundary and header overhead per part.
  • application/x-www-form-urlencoded is the default for text-only forms. Compact, but it cannot carry files sensibly.
  • application/json is the usual choice for text-only API requests. It has no file support, so uploads need multipart, a separate upload step or base64 inside the JSON (which makes the data about a third larger).

For the wider picture of what a request body can contain, see API payloads and JSON.

Sending multipart/form-data from code

Browser (fetch and FormData)

const fd = new FormData();
fd.append('username', 'alice');
fd.append('avatar', fileInput.files[0]);

await fetch('/api/upload', { method: 'POST', body: fd });

Do not set the Content-Type header yourself here. MDN warns that doing so stops the browser from adding the boundary it generated, and the server then cannot split the body.

curl

curl -X POST https://api.example.com/upload \
  -F "username=alice" \
  -F "avatar=@photo.jpg"

-F makes curl send multipart/form-data, and the @ prefix reads the value from a file. More flags are in the curl command entry, and you can try a request without installing anything in the free curl online tool.

Python (requests)

import requests

requests.post(
    'https://api.example.com/upload',
    data={'username': 'alice'},
    files={'avatar': open('photo.jpg', 'rb')},
)

k6

import http from 'k6/http';

const img = open('./photo.jpg', 'b');

export default function () {
  http.post('https://api.example.com/upload', {
    username: 'alice',
    avatar: http.file(img, 'photo.jpg', 'image/jpeg'),
  });
}

Passing http.file() inside a body object is how k6 uploads a file. Load the file once in the init stage with open(path, 'b'), not inside the default function.

Reading it on the server

The server needs a multipart parser: Multer for Express, request.FILES in Django, request.files in Flask, UploadFile in FastAPI, MultipartFile in Spring. Parsers differ in whether they buffer files in memory or stream them to disk, so check that before accepting large uploads.

Treat everything in a part as untrusted. RFC 7578 tells receivers not to use a filename as given and to discard any directory path in it. The part's Content-Type is also only a claim by the client, so check the real file type from its content.

Common errors

  • 413 Content Too Large. The body exceeds a server or proxy limit. nginx defaults client_max_body_size to 1m, so a typical photo upload fails until you raise it.
  • 400 with a missing boundary. The Content-Type header had no boundary parameter, or it did not match the body. This usually comes from setting the header by hand.
  • The file field arrives empty. The field name in the form or code does not match the name the server expects, or the client overrode the boundary.
  • Garbled filenames. Non-ASCII names need consistent UTF-8. RFC 7578 allows percent-encoding in filenames and says not to use the RFC 5987 filename* form here.
  • Uploads that stall on slow links. Very large files are better sent straight to object storage with a signed URL or with a resumable protocol, instead of through your API server.

Testing multipart uploads under load

  • Use realistic file sizes and real file contents. Tiny placeholders hide bandwidth and processing cost, and random bytes do not compress the way images and PDFs do.
  • Test just under and just over the size limit and check for a clean 413, not a 500.
  • Run uploads concurrently to see whether the server buffers files in memory and runs out of it.
  • Test from the regions your users are in, because upload speed depends heavily on distance.

LoadFocus runs JMeter and k6 scripts from 25 cloud regions, so an upload script like the k6 one above can be run from several locations at once. To start from an existing cURL command with -F flags, see turning cURL, HAR, Postman or OpenAPI into a load test. For how parameters differ between body types, read how parameters are sent in an HTTP POST request. General guidance on API testing applies to upload endpoints too.

FAQ: multipart/form-data

Why is the boundary in the Content-Type header?

The parser needs the boundary before it can read the body, and it cannot find it inside the body it has not split yet. Putting it in the header makes it available up front.

Can I use multipart/form-data without files?

Yes, it works for text-only fields. For text-only requests, urlencoded or JSON is lighter because there is no per-part overhead.

What is the difference between multipart/form-data and multipart/mixed?

multipart/form-data is for form submissions, and every part carries a Content-Disposition with a field name. multipart/mixed is the generic container used in email and other MIME messages.

How do I send JSON and a file together?

Send two parts in one multipart request: a text part holding the JSON (with its own Content-Type of application/json) and a file part. Embedding the file as base64 inside the JSON also works but makes the data about a third larger.

Why does my upload fail when I set Content-Type manually?

Because the value lacks the boundary, or the boundary does not match the body. Leave the header unset and let your HTTP client or browser create it.

What is the maximum upload size?

HTTP sets none. The limit comes from your server, proxy or gateway: nginx defaults to 1m and can be raised with client_max_body_size. For very large files, use signed URLs to object storage or a resumable upload.

How fast is your website?

Elevate its speed and SEO seamlessly with our Free Speed Test.

Free Website Speed Test

Analyze your website's load speed and improve its performance with our free page speed checker.