In a Go net/http handler, what does r.FormFile("avatar") return and what must the handler do with it?
answer
- three return values, not two
- one to read, one to describe
- the descriptor carries the client's filename
- it satisfies io.Closer for a reason
- the first call parses the whole body
basics
~10 sr.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 sIt 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 linesfunc 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
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.
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.
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.
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