Submission formats
Multipart form data
Use multipart/form-data when uploading files and let the browser generate the boundary required by the request body.
URL encoding carries text. When a form sends a file, you need multipart/form-data. Set it with the enctype attribute:
<form action="/support" method="post" enctype="multipart/form-data">
<input name="subject">
<input name="screenshot" type="file" accept="image/png,image/jpeg">
<button type="submit">Send request</button>
</form>
What the body looks like
Instead of one line of name=value pairs, the body is split into parts. Each part has its own small headers and its own content. The text field is one part, the file is another:
------WebKitFormBoundaryx7Pq2
Content-Disposition: form-data; name="subject"
Login button does nothing
------WebKitFormBoundaryx7Pq2
Content-Disposition: form-data; name="screenshot"; filename="login.png"
Content-Type: image/png
(binary bytes of the PNG)
------WebKitFormBoundaryx7Pq2--
The file part carries the original filename and the media type the browser guessed. The bytes go in raw, no escaping. That’s why this format can carry an image without blowing up the size.
The boundary
Those ------WebKitFormBoundary... lines are the boundary. It’s a random string the browser generates for each request. It must not appear inside any of the parts, and the server needs to know it to split the body. So the browser puts it in the request header:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx7Pq2
The server reads the header, finds the boundary, and cuts the body at every occurrence.
The mistake everyone makes with fetch
When you send a FormData object with fetch(), do not set the Content-Type header yourself:
await fetch(form.action, {
method: 'POST',
body: new FormData(form)
})
The browser adds the header and the matching boundary for you. If you write 'Content-Type': 'multipart/form-data' by hand, the header has no boundary. The server receives a body it can’t split and typically rejects it or returns empty fields. I’ve debugged this more than once, and the fix is always the same: delete the header.
What multipart doesn’t do
It moves bytes. It doesn’t make them safe. The server still needs a total request size limit, a per-file limit, a check of the actual file content, a generated storage name, and an authorization check. All of that comes in the file uploads lesson later in this course.
Try this: submit the form above with a small PNG and open the request in the Network panel. Find the boundary in the request header, then find the same string separating the subject part from the screenshot part in the payload.
Lesson completed