skip to content

Multipart Forms and Uploads

r.ParseMultipartForm keeps a set amount in memory and spills the rest to temp files you are expected to clean up, while r.MultipartReader streams instead. Interviewers ask how you bound an upload.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

In a Go net/http handler, what does r.FormFile("avatar") return and what must the handler do with it?

level: juniorimportance: should knowfreq 55%

answer

  1. three return values, not two
  2. one to read, one to describe
  3. the descriptor carries the client's filename
  4. it satisfies io.Closer for a reason
  5. the first call parses the whole body

basics

~10 s

r.FormFile returns a multipart.File to read the upload from, a *multipart.FileHeader carrying the client-supplied filename, size and part headers, and an error. The handler must close the file, normally with defer file.Close().

solid answer

~40 s

It returns three values: a `multipart.File` (an interface bundling `io.Reader`, `io.ReaderAt`, `io.Seeker` and `io.Closer`) for the bytes, a `*multipart.FileHeader` describing the part, and an error. The header gives you `Filename`, `Size` and the part's own MIME headers — all of it supplied by the client, so never use `Filename` as a path without running it through `filepath.Base` or replacing it with a name you generate. If no file part matches the key you get `http.ErrMissingFile`. The first call parses the whole request body for you (`ParseMultipartForm` with a 32 MB default in-memory budget), so by the time you have the handle the upload has already been received. Always `defer file.Close()`: for a part that spilled to disk that handle is an open file descriptor.

code

go · 14 lines
go
func upload(w http.ResponseWriter, r *http.Request) {
	file, fh, err := r.FormFile("avatar")
	if errors.Is(err, http.ErrMissingFile) {
		http.Error(w, "avatar is required", http.StatusBadRequest)
		return
	} else if err != nil {
		http.Error(w, "malformed upload", http.StatusBadRequest)
		return
	}
	defer file.Close()

	name := filepath.Base(fh.Filename) // client-supplied; may contain separators
	log.Printf("got %q, %d bytes, part type %q", name, fh.Size, fh.Header.Get("Content-Type"))
}

go deeper

for a junior

Recall the three return values and that the file must be closed. Be ready to write the eight-line handler that reads an upload and copies it somewhere, using defer for the close.

for a middle

Explain that the first call parses the entire body with a default 32 MB memory budget, what the FileHeader's fields mean, and why every one of them is client-controlled input rather than server-verified metadata.

for a senior

Show the production instincts: generate your own storage name, sniff content rather than trusting the declared type, copy the bytes inside the request's lifetime, and know that the returned handle may be a descriptor on a temp file.

for a principal

Frame it as an API contract question — what your service promises about accepted names, sizes and types at the edge, and where that validation lives so every upload path in the estate enforces it the same way.

## The body you are reading from When a client posts a form containing a file, it sends `Content-Type: multipart/form-data; boundary=...`. The body is a sequence of parts separated by that boundary. Each part has a small header block of its own — most importantly `Content-Disposition: form-data; name="avatar"; filename="cat.png"` — then a blank line, then the part's raw bytes. Text fields and file fields travel in the same body; a *file* part is simply one whose `Content-Disposition` carries a `filename` parameter. ## The signature ```go func (r *http.Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error) ``` **First value — `multipart.File`.** This is an interface, not an `*os.File`: ```go type File interface { io.Reader io.ReaderAt io.Seeker io.Closer } ``` Because it is a `ReaderAt` and a `Seeker` you can read it more than once (seek back to 0) and read ranges out of order — that holds whether the part was small enough to stay in memory or large enough to have been written to a temporary file. Because it is a `Closer`, it owns a resource: for a part backed by a temp file, an open file descriptor. Not closing it holds that descriptor for the life of the request and pins the disk blocks even after the file is unlinked. **Second value — `*multipart.FileHeader`.** A small descriptor with three exported fields: `Filename` (the name the client claimed), `Size` (the part's length in bytes), and `Header` (a `textproto.MIMEHeader` holding that part's own headers, such as its `Content-Type`). It also has an `Open()` method that hands you a fresh `multipart.File` for the same part — useful when you want a second independent reader, or when you got the header out of `r.MultipartForm.File[key]` rather than from `FormFile`. Everything in the header comes from the client. `Filename` may contain slashes, `..`, control characters, or a 4 KB name; the part's `Content-Type` is whatever the client typed and is not verified against the bytes. Use `filepath.Base` on it, or better, store the upload under a name you generate and keep the client's name only as a display label. If you care what the file really is, sniff the leading bytes yourself with `http.DetectContentType`. **Third value — `error`.** If the request is not multipart, or the body is malformed, you get the parse error. If the body parsed fine but there is no *file* part under that key, you get `http.ErrMissingFile` — test it with `errors.Is(err, http.ErrMissingFile)` when you want to distinguish "the user did not attach anything" from "the request was broken". ## What happens on the first call `FormFile` is a convenience wrapper. If `r.MultipartForm` is still nil it calls `r.ParseMultipartForm` for you with a default in-memory budget of 32 MB, which reads the **entire** request body: text parts land in `r.MultipartForm.Value`, file parts in `r.MultipartForm.File` as `[]*multipart.FileHeader`. So the error from `FormFile` may really be a parse error, and the work of receiving the upload has already been done by the time it returns. One consequence worth internalising early: `FormFile` returns only the **first** file for that key. A form with several file inputs sharing one name produces several parts under that name, and you reach the rest through `r.MultipartForm.File["docs"]`, calling `Open()` on each header. ## The shape of a correct handler The three habits that make a handler correct: close what you were handed, do not trust `fh.Filename`, and copy the bytes to a destination you control *inside* the handler rather than stashing the header for later — the storage behind it does not outlive the request.

  • Where do the bytes actually live before you read them?
    In memory or on disk, depending on size. The first call parses the body with a 32 MB in-memory budget by default; parts that fit stay in a byte slice, and anything beyond that budget is written to a temporary file in the OS temp directory. `multipart.File` hides the difference behind the same Reader/Seeker interface.
  • Why is fh.Filename unsafe to use directly as a path?
    It is whatever the client put in the part's `Content-Disposition` header — nothing validates it. It can contain `..`, absolute paths or separators, so joining it onto an upload directory is a path-traversal write. Run it through `filepath.Base`, or ignore it and store under a name you generate.
  • The form allows several files under one field name. How do you reach them all?
    `FormFile` gives you only the first. After parsing, iterate `r.MultipartForm.File["docs"]`, which is a `[]*multipart.FileHeader`, and call `Open()` on each one to get its `multipart.File`. Close each handle as you finish with it rather than deferring all of them inside the loop.

saying these in an interview costs you the question

  • Says FormFile returns an *os.File you can rename into place
  • Never closes the returned multipart.File
  • Writes the upload to fh.Filename verbatim
  • Treats the part's Content-Type as verified proof of the file type
  • Thinks FormFile returns every file sent under that field name
open as a page

In Go's r.ParseMultipartForm(maxMemory), what does the maxMemory argument actually bound?

level: middleimportance: should knowfreq 44%

basics

~20 s

maxMemory caps the bytes of file parts kept in RAM; the rest spills to temporary files on disk. It is not an upload size limit — a huge body is still received in full, just written to disk.

open as a page

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

level: middleimportance: should knowfreq 36%

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.

open as a page

Why do multipart temp files pile up on disk in a Go upload service after every request?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

net/http removes the temp files behind r.MultipartForm when the handler returns. They pile up when your own code called mime/multipart's ReadForm, because the server never sees that form — defer its RemoveAll, or stream the parts so nothing spills.

open as a page