skip to content

An HTML form contains <input type="file">. Why must it set enctype="multipart/form-data", and what does the browser send differently from the default encoding?

level: middleimportance: must knowfreq 56%

answer

  1. the default is flat text pairs
  2. no room for bytes or a filename
  3. one part per field, split by a boundary
  4. each part carries its own content type
  5. set it on the form, not the input

basics

~20 s

The default encoding, application/x-www-form-urlencoded, can only carry percent-encoded text pairs, so a file input contributes just its filename. multipart/form-data splits the submission into labelled parts separated by a boundary, each able to carry raw bytes plus a filename and its own content type.

solid answer

~40 s

Both encodings serialize the same entry list; they differ in what an entry can hold. The default `application/x-www-form-urlencoded` percent-encodes each pair and joins them with `&` into one flat text blob — there is no place for binary content, no filename and no per-field type, so a file entry degrades to the file's name only and the bytes never leave the browser. `enctype="multipart/form-data"` instead emits one part per entry, separated by a generated boundary string; each part carries a `Content-Disposition: form-data` line with the field's `name`, plus `filename` and its own `Content-Type` when it is a file, followed by the raw bytes. That is why file uploads require it. `enctype` applies only to POST submissions, and a submit button can override it for its own activation with `formenctype`.

go deeper

for a junior

Remember that any form with a file input needs enctype="multipart/form-data" on the form element, and that the default encoding sends only the filename.

for a middle

Describe both wire formats: urlencoded pairs joined by ampersands versus boundary-separated parts with Content-Disposition, filename and per-part Content-Type, and say why bytes need the second.

for a senior

Explain the failure mode end to end — the upload that silently sends a filename — and treat the declared filename and type as untrusted, deriving storage names and real types server-side.

for a principal

Decide where upload bodies terminate: direct multipart to the app, or pre-signed uploads to storage with the form carrying only metadata, and weigh size limits, timeouts and scanning against operational cost.

## enctype describes the shape of the body The browser always builds the same entry list first. `enctype` chooses how that list is written into the request body, and there are exactly three values a form may use: `application/x-www-form-urlencoded` (the default), `multipart/form-data`, and `text/plain`. Because it describes a body, `enctype` is meaningful only with `method="post"`; on a GET form it is ignored. ## The default: one flat line of text `application/x-www-form-urlencoded` percent-encodes each name and value, turns spaces into `+`, and joins the pairs with `&`: ``` name=Ada+Lovelace&role=engineer&topic=html&topic=a11y ``` Compact, easy to parse, and perfectly good for text fields — this is why it is the default. What it cannot express is anything *about* a value. There is no slot for a filename, no slot for a per-field content type, and raw bytes would have to be percent-encoded, inflating binary data badly and still leaving nowhere to record what it was. So when a form with a file input is left at the default, the browser submits the **file's name as a plain text value** and the contents are simply not sent. The request succeeds, the server stores an empty or nonsense value, and nothing in the markup looks obviously wrong — which is what makes this a classic bug. ## multipart/form-data: labelled compartments ```html <form action="/avatar" method="post" enctype="multipart/form-data"> <label for="caption">Caption</label> <input id="caption" name="caption"> <label for="photo">Photo</label> <input id="photo" name="photo" type="file"> <button>Upload</button> </form> ``` With this encoding the browser picks a **boundary** — a random token that does not occur in the data — declares it alongside the content type, and writes one part per entry: ``` --boundary123 Content-Disposition: form-data; name="caption" On the beach --boundary123 Content-Disposition: form-data; name="photo"; filename="dog.png" Content-Type: image/png <raw bytes> --boundary123-- ``` Each part carries its own metadata, so binary content passes through untouched and the server learns the original filename and the type the browser guessed. Nothing needs percent-encoding, at the cost of a header block per field, which is why multipart is not the default for ordinary text forms. ## Practical consequences - **Set it on the form, not the input.** A very common mistake is putting `enctype` on the `<input type="file">` or omitting it because the file picker visibly works. The picker always works; only the form's `enctype` decides whether bytes are sent. - **The filename and type are client-supplied.** Both come from the browser and are trivially forged, so a server must never trust the declared `Content-Type` of a part, nor use the supplied filename directly as a path. Treat them as untrusted input like any other field. - **`accept` and `multiple` are separate concerns.** They shape the picker and how many entries the input contributes; neither has any effect on the encoding. - **Per-button override.** `formenctype` on a submit button changes the encoding for that button's activation only, alongside its siblings `formaction`, `formmethod` and `formtarget`. - **`text/plain` exists but is not for production.** It writes `name=value` lines with no escaping, so a value containing a newline is ambiguous. It was intended for `mailto:` debugging, and no server should be parsing it. ## When you submit from script If you take submission over yourself and pass a `FormData` object as a `fetch` body, the browser encodes it as `multipart/form-data` and generates the boundary for you. The mistake to avoid is setting the `Content-Type` header manually: the browser will then use your header, which lacks the boundary token, and the server cannot split the parts — the upload fails with a confusing parse error. Leave the header off and let the browser write it. If you want the urlencoded shape instead, convert with `new URLSearchParams(formData)`, which the browser encodes as `application/x-www-form-urlencoded`; note that this only makes sense when the entries are all text, since URLSearchParams has nowhere to put a file.

  • A team sets enctype on the <input type="file"> rather than on the <form> and files arrive empty. What is happening?
    `enctype` is a form attribute; on an input it is ignored, so the form stays at the default urlencoded encoding. The picker still works and a value is still submitted — the file's name as plain text — so the request looks successful while the bytes never leave the browser. Move `enctype="multipart/form-data"` onto the `<form>`, or override it for one button with `formenctype`.
  • Why should you not set the Content-Type header yourself when sending a FormData object with fetch?
    Because multipart bodies are only parseable with the boundary token, and the browser generates that token when it encodes the body. If you set `Content-Type: multipart/form-data` manually, your header ships without a boundary and the server cannot find the part separators. Omit the header entirely and the browser writes the correct one, boundary included.
  • Can a server trust the filename and Content-Type that arrive in a multipart part?
    No. Both are supplied by the client and are trivially forged, so they are untrusted input like any other field. Derive the storage name yourself rather than using the submitted one, and determine the real type by inspecting the content server-side. Treat the declared values as hints for display, never as authorization or as a path.
  • If multipart can carry anything, why is it not the default encoding?
    Overhead. Every entry gets its own boundary line and header block, which is significant when a form is a handful of short text fields; urlencoded packs the same data into one compact line that is cheap to parse. Multipart earns its cost only when an entry needs bytes, a filename, or a per-field content type.

Urlencoded is a postcard — one line of text, everything squeezed onto it. Multipart is a parcel divided into labelled compartments, each with its own tag saying what is inside and what it was called.

saying these in an interview costs you the question

  • Puts enctype on the file input instead of the form
  • Thinks a file input uploads regardless of the form's enctype
  • Sets Content-Type manually when posting a FormData body
  • Trusts the submitted filename or part Content-Type server-side
  • Believes enctype has an effect on a GET form

context