skip to content

When should a Go handler use r.MultipartReader() instead of r.ParseMultipartForm?

level: middleimportance: should knowfreq 36%

answer

  1. one part at a time, in order
  2. the body is read exactly once
  3. no Size field on a streamed part
  4. moving on burns the previous part
  5. a field after the file arrives too late

basics

~20 s

Use r.MultipartReader when the upload should be processed as a stream: it hands back one part at a time, so nothing is buffered whole or spilled to a temp file. ParseMultipartForm instead reads the entire body first.

solid answer

~40 s

`r.MultipartReader()` returns a `*multipart.Reader`; you loop on `NextPart()` until it returns `io.EOF`, and each `*multipart.Part` is itself an `io.Reader` you copy straight to its destination. Nothing is buffered whole, so a multi-gigabyte upload costs a fixed-size copy buffer and no temp files — which is the reason to choose it. The costs are real too: the two APIs are mutually exclusive (calling one after the other returns an error, because the body is read once), you lose random access to fields, and you must handle parts strictly in the order the client sent them. `NextPart` advances the underlying stream, so once you move on, the previous part is unreadable. If your validation needs a text field the client placed *after* the file, you either buffer it yourself or you cannot stream.

code

go · 33 lines
go
mr, err := r.MultipartReader()
if err != nil {
	http.Error(w, "expected a multipart/form-data body", http.StatusBadRequest)
	return
}

for {
	p, err := mr.NextPart()
	if errors.Is(err, io.EOF) {
		break
	}
	if err != nil {
		http.Error(w, "malformed part", http.StatusBadRequest)
		return
	}
	if p.FileName() == "" {
		p.Close() // a text field; read it here if you need it
		continue
	}
	dst, err := os.Create(filepath.Join(uploadDir, newID()))
	if err != nil {
		p.Close()
		http.Error(w, "storage unavailable", http.StatusInternalServerError)
		return
	}
	_, copyErr := io.Copy(dst, p) // fixed-size buffer, no spill
	dst.Close()
	p.Close()
	if copyErr != nil {
		http.Error(w, "upload failed", http.StatusInternalServerError)
		return
	}
}

go deeper

for a junior

Recall that there are two ways to read a multipart body — parse it all up front, or take the parts one at a time — and that only the streaming one avoids buffering the whole upload.

for a middle

Explain the NextPart loop, the io.EOF terminator, what a Part exposes, and why the two APIs are mutually exclusive because a request body can only be read once.

for a senior

Argue the tradeoff for a real endpoint: unbounded sizes and early rejection against the loss of random access and field ordering, and spot upstream middleware that quietly consumes the body first.

for a principal

Decide the contract you offer clients — which fields must precede the file, what a part-size ceiling is, whether large uploads belong on this path at all — since streaming pushes ordering requirements onto every client you cannot change later.

## Two ways to read one body A `multipart/form-data` body is a linear stream: part, part, part, closing boundary. `ParseMultipartForm` consumes that stream eagerly and materialises the whole thing as a `*multipart.Form` — a map of text values and a map of file descriptors, with the file bytes in memory or in temp files. `r.MultipartReader()` gives you the stream itself and leaves the materialising to you. ```go func (r *http.Request) MultipartReader() (*multipart.Reader, error) ``` ## The loop `*multipart.Part` gives you `FormName()` (the `name` from the part's `Content-Disposition`), `FileName()` (the `filename` parameter, empty for a text field), and `Header`, a `textproto.MIMEHeader` with that part's own headers. There is no `Size` — the length is not known until you have read the part, which is precisely the property that lets a client stream an upload of unknown length. The loop ends when `NextPart` returns `io.EOF`. ## Why you would choose it **Fixed memory, no temp files.** Copying a part with `io.Copy` uses a small buffer regardless of the part's size. A 4 GB upload never occupies 4 GB of anything, and the temp directory stays empty — so there is no spilled file to account for, clean up, or run out of space on. **Early rejection.** You see each part's name, filename and declared type before reading its bytes, so a handler can refuse a part and stop reading rather than accepting the whole body first and validating afterwards. **Unknown or unbounded part counts.** A client sending an arbitrary number of files does not force a proportional amount of buffering. ## What you give up **Order becomes your problem.** The parts arrive in the order the client wrote them, and `NextPart` advances the reader past the current part — reading part *n+1* makes part *n* permanently unreadable. There is no seeking backwards and no index. If the checksum or the destination folder arrives as a text field *after* the file part, a streaming handler cannot consult it while writing the file; it must buffer the file somewhere first, at which point it has re-invented the spill it was trying to avoid. Clients that emit fields in a predictable order make streaming easy; ones you do not control may not. **The convenience accessors stop working.** `r.MultipartReader()` and `r.ParseMultipartForm` are mutually exclusive because a request body is read once. If the form accessors have already parsed the body, `MultipartReader` returns an error saying the multipart body was handled by `ParseMultipartForm`; calling `MultipartReader` twice is also an error. And any helper that reads form values — including one in a logging or auth middleware upstream of your handler — will trigger the eager parse and take the streaming option away from you. This is a genuine trap: middleware that touches a form field silently converts every upload on the service to the buffered path. **You write the accounting yourself.** Buffered parsing gives you `fh.Size` for free. Streaming does not, so if you need a per-part byte ceiling you count the bytes as you copy — for example with an `io.LimitReader` wrapping the part — and decide what to do when the count is exceeded, mid-copy, with a partially written destination. ## Choosing between them Use `ParseMultipartForm` when uploads are small and bounded and you want random access to the fields: it is less code and the maps are convenient. Reach for `MultipartReader` when a single request can carry more bytes than you are willing to hold, when part counts are unbounded, or when you need to reject a part before receiving it. The decision is usually made once per endpoint rather than per request, because the two styles do not compose within one handler.

  • What happens if a middleware calls r.FormValue before your handler calls r.MultipartReader?
    The accessor parses the whole body eagerly, so by the time your handler runs the body is consumed and `MultipartReader` returns an error saying the multipart body was handled by the form parser. Any upstream code that touches a form field silently forces every upload onto the buffered path — worth checking before blaming the handler.
  • Must you read a part fully before calling NextPart again?
    You do not have to, but you cannot come back. `NextPart` advances the underlying stream past whatever is left of the current part, and the old `*multipart.Part` becomes unreadable. Skipping a part you do not want is fine; deferring one for later is not.
  • How do you enforce a per-file size ceiling when streaming?
    Count as you copy: wrap the part in an `io.LimitReader` set one byte above your ceiling, or copy through a counting writer, and abort when the count is exceeded. There is no `Size` to check up front, so the check is mid-copy and you must clean up the partially written destination.

saying these in an interview costs you the question

  • Believes both APIs can be used on the same request
  • Expects a Size field on a streamed part
  • Keeps a Part around after calling NextPart
  • Assumes text fields are available before the file part
  • Thinks streaming still writes temp files behind the scenes