After a browser form POST, when should the handler render a page directly and when should it answer with a redirect?
answer
- what should a reload repeat?
- write happened, hand back a GET URL
- see-other, then follow with GET
- rejected form keeps request-only state
- not 200 for a rejected submission
basics
~20 sRedirect after a successful write, so the browser's final URL is a safe GET and a reload repeats nothing. Render directly when the response needs state only this request holds, a rejected form's values and messages, under a 4xx status.
solid answer
~50 sThe rule is about what a reload should do. On success the write has happened, so answer `303 See Other` with a `Location` pointing at a GET resource; the browser follows it with a GET, the address bar holds a safe URL, and reload or back re-runs a read instead of re-posting. On failure the response must carry state that only this request has — the values the user typed and the messages about them — so render the form template again in the same response, with a `422` or `400` status rather than `200`, because the submission was rejected. Redirecting instead would throw that state away unless you carry it across by some other mechanism. Direct rendering is also fine for a read-shaped POST, such as a search whose parameters are too large for a URL.
code
http · 10 linesPOST /orders HTTP/1.1
Host: shop.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
item=A-19&quantity=2&ship=std
HTTP/1.1 303 See Other
Location: /orders/1042
Content-Length: 0go deeper
Recall the shape: after a successful form write, answer with a redirect to a page the browser fetches with GET, so refreshing does not send the form again.
Explain why the two endings differ — a rejected submission carries values and messages that exist only in this request — and pick honest status codes, 303 on success and a 4xx on rejection.
Show what the pattern does not cover: concurrent double submissions need a token or a constraint, non-browser clients want a representation rather than a page, and a message across the redirect needs its own mechanism.
Decide the convention for a codebase and how it holds across browser pages, background fragment updates and programmatic clients, so one endpoint does not have to serve three contradictory response styles.
A browser form POST has two possible endings, and which one you choose is visible to users months later as a duplicate record or a "confirm form resubmission" dialog. ## Why a successful write should redirect When a POST responds with a rendered page and status `200`, the browser's current entry is the POST itself. Everything that re-issues the current entry — reload, back-then-forward, the dialog the browser shows about resending data — re-issues the POST. The post-redirect-get shape removes that: 1. The handler performs the write. 2. It responds with `303 See Other` and a `Location` header naming a URL that answers a GET. 3. The browser issues that GET and replaces the history entry, so the address bar shows the result resource. 4. Reload, back and bookmark now all exercise a safe, repeatable read. The cost is one extra round trip, and the fact that anything the handler wanted to say ("saved") lives in the request that is about to end — carrying a message across the redirect needs a mechanism outside the request-scoped model, which is a separate concern from rendering. ## Why a rejected submission should render A failed validation response needs three things that exist only in the request being handled: the values the user typed, the messages attached to individual fields, and the form's own structure. Redirecting discards all three unless you stash them somewhere and pick them up again, which is more moving parts than simply rendering the form template with the submitted model. The status code matters here and is frequently wrong. Returning `200` with a page full of error messages tells caches, clients and monitoring that the request succeeded. `422` says the body was understood but its contents failed the application's rules; `400` says the request itself was malformed. Either is honest; `200` is not. Re-rendering with a 4xx also means the browser keeps the POST as the current entry, so a reload will offer to resubmit — acceptable precisely because the write did not happen, and the user is looking at a form they are about to correct anyway. ## Choosing the status code | Status | What the client does | Use it for | |---|---|---| | `303 See Other` | follows `Location` with a GET regardless of the original method | the normal ending of a successful write | | `302 Found` | in practice also switches to GET, though the specification does not require it | legacy equivalent; prefer the explicit one | | `307` / `308` | repeats the original method and body at the new URL | moving an endpoint, never for post-redirect-get | | `422` / `400` | renders the returned page | re-showing a form after rejection | | `200` after a write | renders and keeps the POST as the current history entry | a deliberate exception, not the default | ## What post-redirect-get does not solve It is a fix for browser history and reload behaviour, not a concurrency mechanism. Specifically: - A double click submits twice before any redirect exists; both requests reach the handler. Preventing the second write needs a submission token, a uniqueness constraint, or a client-side disable — the redirect happens too late to help. - It does not make the write idempotent. Anything that replays the POST — a retrying proxy, a restored tab — still hits the handler. - It does nothing for non-browser clients. An API consumer wants the created resource's representation or its location, not a page, and follows redirects on its own terms. ## When direct rendering is the right answer anyway - **A read-shaped POST.** A search or report request whose parameters do not fit a URL is a read; nothing was written, so there is no duplicate-submission hazard and rendering the result directly is simpler. The tradeoff is that the result is not linkable or bookmarkable. - **A multi-step flow that stays on the same step.** If the next screen is derived from what was just submitted and no write has happened, rendering it keeps the data in hand. - **An interactive fragment update.** When the request is issued in the background and only a region of the page is replaced, there is no history entry to protect, so returning the rendered fragment is the point. The decision rule that covers all of these: **if the request changed state, make the browser's final URL a GET; if the response depends on state that exists only inside this request, render it now.** When both are true — a write succeeded but you want to say something about it — redirect, and carry the message across by a mechanism built for that.
- Why 303 rather than 307 for post-redirect-get?`303` instructs the client to fetch the target with GET whatever the original method was, which is the whole point: the browser's final entry becomes a safe read. `307` and `308` deliberately preserve the method and body, so the client would re-POST to the new URL — useful when an endpoint has moved, and exactly wrong when you are trying to stop a write from being repeated.
- What status belongs on a re-rendered form after validation fails?A 4xx. `422` fits when the body parsed but violated application rules, `400` when the request was malformed. The page content is identical either way; the status is what tells caches, client code and monitoring that the request did not succeed. Answering `200` with an error page inside it hides real failure rates and misleads anything that reads status rather than markup.
- Does post-redirect-get prevent duplicate records from a double click?No. Both clicks produce a POST before any redirect can be followed, so both reach the handler. The redirect only fixes what a reload or back-navigation does afterwards. Preventing the second write needs a per-submission token the server accepts once, a uniqueness constraint on the data, or disabling the control client-side — usually a combination, since only the server-side ones are trustworthy.
saying these in an interview costs you the question
- Renders a success page directly from the POST and calls reload a browser problem
- Returns 200 with validation errors inside the rendered form
- Redirects after failed validation and loses the values the user typed
- Believes post-redirect-get stops duplicate submissions from a double click
- Uses a method-preserving redirect status for post-redirect-get