skip to content

After r.ParseForm in a Go handler, what is the difference between r.Form and r.PostForm?

level: middleimportance: should knowfreq 60%

answer

  1. one field is body-only
  2. the other is a merge
  3. who wins on a duplicated key
  4. Content-Type decides whether the body is parsed at all
  5. FormValue reads the merge and eats the error

basics

~10 s

r.PostForm holds only values parsed from a urlencoded request body. r.Form holds those plus the URL query string merged in, with body values taking precedence. r.FormValue reads r.Form; r.PostFormValue reads r.PostForm.

solid answer

~40 s

`r.ParseForm()` always parses the URL query string, and for POST, PUT and PATCH it additionally reads the body and parses it — but only when the `Content-Type` is `application/x-www-form-urlencoded`. Body values land in `r.PostForm`; `r.Form` gets the union of body and query values, with the body value first for a duplicated key, so `r.FormValue("limit")` returns the body's value when both sides send `limit`. Both are `url.Values`, so `Get` gives the first value and indexing gives repeats. The distinction matters for security and clarity: if an endpoint means "the body said so", read `r.PostForm` or `r.PostFormValue`, otherwise a caller can smuggle the parameter in through the query string. `r.FormValue` calls `ParseForm` implicitly and discards its error, so call `ParseForm` yourself when you want to answer 400 on a malformed body.

code

go · 9 lines
go
if err := r.ParseForm(); err != nil {
	http.Error(w, "malformed form", http.StatusBadRequest)
	return
}
r.PostForm.Get("limit") // "50" - body only
r.Form.Get("limit")     // "50" - the body value precedes the query value
r.Form.Get("x")         // "2"
r.PostForm.Get("x")     // "2"
r.PostForm.Get("page")  // "" - a query-only key never reaches PostForm

go deeper

for a junior

Recall which field is which: PostForm comes from the body, Form is body plus query. Remember that you must call ParseForm first, or use FormValue, which parses for you.

for a middle

Explain the mechanics: which methods and which Content-Type make ParseForm read the body, that body values precede query values in r.Form, and that FormValue discards the parse error.

for a senior

Demonstrate the judgment of reading PostForm when the contract says body, so query parameters cannot be smuggled in, and of calling ParseForm explicitly so malformed input becomes a 400 rather than an empty string.

for a principal

Own the contract question: whether an endpoint accepts urlencoded forms at all, or standardises on JSON, and what that decision costs in browser support, logging exposure and the amount of per-handler input code your teams must keep consistent.

## The two fields `*http.Request` carries two parsed-input fields of type `url.Values` (`map[string][]string`), both of them nil until something populates them: - **`r.PostForm`** — parameters that came from the **request body**, parsed as `application/x-www-form-urlencoded`. - **`r.Form`** — the **merge** of those body parameters with the parameters in the URL query string. `r.ParseForm()` is what fills them. ## What ParseForm actually does For every request method, `ParseForm` parses `r.URL.RawQuery` into `r.Form`. For `POST`, `PUT` and `PATCH` it also looks at the `Content-Type` header: - `application/x-www-form-urlencoded` — it reads the body to completion, parses it, and puts the result in `r.PostForm`, then copies those values into `r.Form` before appending the query values. - `multipart/form-data` — `ParseForm` does **not** parse it; that is `ParseMultipartForm`'s job. - anything else, `application/json` included — the body is left completely untouched, `r.PostForm` ends up empty, and `ParseForm` returns a **nil error**. Nothing tells you the body was ignored. That last bullet is the trap. A handler that posts JSON and then calls `r.FormValue("id")` gets `""` with no complaint at all. ## Precedence when a key appears on both sides Send `POST /search?limit=10` with the body `limit=50`. After `ParseForm`: - `r.PostForm["limit"]` is `["50"]` - `r.Form["limit"]` is `["50", "10"]` — body first, query appended - `r.Form.Get("limit")` and `r.FormValue("limit")` therefore return `"50"` Body parameters take precedence in `r.Form`. Both values survive in the slice, so a handler that cares can detect the collision by checking `len(r.Form["limit"]) > 1`. ## Which one should a handler read? This is a real API-design decision, not a style preference. If your endpoint's contract is "the parameters are in the body", read `r.PostForm` / `r.PostFormValue`. If you read `r.Form` instead, a caller can supply a parameter your endpoint expected in the body by putting it in the URL — which shows up in access logs, in referrers, and in any cache key, and lets a crafted link carry a parameter the designer assumed only a form could send. Conversely, an endpoint that deliberately accepts either (a search form that also works as a bookmarkable URL) is exactly what `r.Form` is for. ## The convenience wrappers - `r.FormValue(key)` calls `ParseMultipartForm`/`ParseForm` if needed, then returns `r.Form.Get(key)`. - `r.PostFormValue(key)` does the same parsing, then returns `r.PostForm.Get(key)`. Both **swallow the parse error**. That is fine for a throwaway handler and wrong for an API that owes clients a 400 on garbage input: call `r.ParseForm()` explicitly, check its error, and read the fields afterwards. `ParseForm` reports a malformed query string as well as a malformed body, so one check covers both. ## Other details worth knowing - Both fields are `url.Values`, so all the query-string rules carry over: `Get` returns the first value or `""`, indexing returns every repeat, and `Has` reports presence. - `ParseForm` is idempotent — calling it twice does no extra work, because it returns early once `r.Form` is non-nil. - The urlencoded body read is itself size-capped by `net/http` (on the order of 10 MB); for a tighter, explicit cap on any body, wrap `r.Body` in `http.MaxBytesReader` before parsing. - On the client side of the same coin, `url.Values.Encode()` produces the very body format `ParseForm` consumes, which is what `http.PostForm` sends. ## Interview-ready summary `PostForm` = body only. `Form` = body plus query, body wins. `ParseForm` only parses a body that is urlencoded and only for POST/PUT/PATCH, and it reports nothing when it skips one. Read `PostForm` when the contract says "body", and call `ParseForm` explicitly so you can act on its error.

  • A handler posts JSON and calls r.FormValue("id"), which returns empty. Why is there no error?
    `ParseForm` only parses a body whose `Content-Type` is `application/x-www-form-urlencoded`. With `application/json` it parses the query string, leaves the body untouched, and returns a nil error, so `r.PostForm` is empty and `r.FormValue` returns `""`. Nothing in the API reports that the body was skipped — decode JSON bodies with `json.NewDecoder(r.Body)` instead.
  • Why might reading r.Form rather than r.PostForm be a security problem?
    `r.Form` merges the URL query string in, so a parameter your endpoint expected in a form body can be supplied through the URL by anyone who can make the victim's browser follow a link. It also puts that parameter into access logs and cache keys. If the contract is "this comes from the body", read `r.PostForm` or `r.PostFormValue`.
  • How do you notice that a client sent the same key in both the query string and the body?
    Index rather than `Get`: after `ParseForm`, `r.Form["limit"]` holds every occurrence — the body value first, then the query value — so `len(r.Form["limit"]) > 1` means the client sent it twice. `Get` and `FormValue` only ever show you the first, which is why collisions go unnoticed.

saying these in an interview costs you the question

  • Says r.Form is body-only and r.PostForm includes the query
  • Thinks the URL query value wins over the body value in r.Form
  • Believes ParseForm parses a JSON body
  • Expects an error when the Content-Type is not urlencoded
  • Relies on FormValue and never checks the ParseForm error
  • Assumes ParseForm handles multipart/form-data too