What does HTTP status 303 See Other mean, and how is it used in the Post/Redirect/Get pattern after a browser form submission?
answer
- 303 = go GET that other URL, whatever you sent
- Mandates method switch (302 only tolerates it)
- PRG: POST → 303 → GET result page
- Kills refresh re-submit; URL becomes shareable
- Flash message or resource state carries the notice
basics
~20 s303 See Other tells the client to fetch a different URL with GET, whatever method the original request used. After a form POST the server replies 303 with Location pointing at a result page, so the browser lands on a plain GET that is safe to refresh, bookmark, and revisit with the back button.
solid answer
~60 s**303 See Other** means: the response to your request is at another URI, and you should retrieve it **with GET**, regardless of the method you originally sent. Unlike 301/302 — where POST-to-GET rewriting is merely tolerated — 303 *mandates* the switch, so the behaviour is deterministic. It is not cacheable by default. **Post/Redirect/Get** uses this to fix the double-submit problem. Without it, the browser's current URL after a form submission is still the POST target, so pressing refresh re-sends the POST ("Confirm form resubmission") and the back button lands on a stale POST result. With PRG: 1. Browser `POST /orders` — server creates the order. 2. Server replies `303 See Other`, `Location: /orders/1042`. 3. Browser `GET /orders/1042` — renders the confirmation. Now the address bar holds a GET URL: refresh re-renders, the URL is shareable, and back/forward behave normally. Any "you submitted this" message must survive the redirect — via the resource itself, a query parameter, or a one-shot flash entry in the session. 303 is also the natural companion to `202 Accepted` async flows: "the result is over there, go GET it."
code
http · 17 linesPOST /orders HTTP/1.1
Host: shop.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 24
sku=A-1&quantity=3&pay=1
HTTP/1.1 303 See Other
Location: /orders/1042
Cache-Control: no-store
Content-Length: 0
GET /orders/1042 HTTP/1.1
Host: shop.example.com
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8go deeper
Say that 303 makes the browser fetch another URL with GET, and that redirecting after a form POST stops refresh from submitting the form twice.
Contrast 303 with 302 and 307 on method handling, walk the three-step PRG exchange, and explain how history and the address bar change as a result.
Cover flash-message strategies and their costs, when not to redirect (validation failures), open-redirect risk on the Location value, and why APIs should return 201 with Location instead of forcing a second round trip.
Frame PRG as history and idempotency hygiene rather than a UI trick: it makes the terminal navigation safe to repeat, which is the same reasoning behind idempotent write design, and note the extra round trip is a deliberate latency-for-correctness trade appropriate to browsers but not to machine clients.
## What 303 means `303 See Other` says the server has a response for you, but it is not the representation of the URI you requested — it is at the URI in `Location`, and you should retrieve **that** with a `GET`. The method switch is normative, not optional: this is the code whose entire job is to convert a non-GET request into a GET follow-up. It is *not cacheable by default* (a cache may store it only if the response carries explicit freshness headers), and the `Location` URI is not claimed to be an alternative representation of the original resource — it is simply where the answer lives. That distinction matters against its neighbours: - **302 Found** — client *may* switch POST to GET. Ambiguous. - **303 See Other** — client *must* switch to GET. Deterministic. - **307 Temporary Redirect** — client *must not* switch; the POST is replayed. ## The problem Post/Redirect/Get solves When a browser submits a form with POST and the server responds with `200 OK` and an HTML page, the browser's session history entry for that page is the **POST**. Three annoyances follow: 1. **Refresh re-submits.** Pressing F5 re-issues the POST. The browser warns ("Confirm form resubmission"), but users click through, and a second order, payment, or comment is created. 2. **Back/forward misbehave.** Navigating back to the result re-triggers the POST or shows a document-expired page. 3. **The URL is not shareable.** The address bar shows the form-handling endpoint, not the created resource. Copying it gives someone a URL that does nothing useful. PRG removes all three by making the *last* navigation a GET. ## The exchange ``` POST /orders → 303 See Other, Location: /orders/1042 GET /orders/1042 → 200 OK (confirmation page) ``` The history entry the user ends on is the GET. Refresh re-runs the GET and re-renders the confirmation. The URL identifies the created order and is bookmarkable and shareable. The POST is gone from the top of the history stack. Historically many frameworks used 302 for this because pre-HTTP/1.1 clients did not understand 303, and 302's de-facto POST-to-GET behaviour produced the same result. That works, but 303 is the honest code: it states the intent rather than relying on a documented deviation. ## Carrying state across the redirect The redirect discards the response body, so anything the POST wanted to tell the user must survive some other way: - **Best:** put it in the resource. `/orders/1042` renders "Order placed" because the order exists and is new. - **Query parameter:** `Location: /orders/1042?created=1`. Simple, but user-controllable — never trust it for authorisation, and never reflect it into HTML unescaped. - **Flash message in the session:** store a one-shot message server-side, read and clear it on the GET. Costs a session write and needs care behind multiple app servers with sticky-session assumptions. Validation failures are the interesting counter-case. If the POST fails validation you often *want* to re-render the form with the submitted values and errors, which means returning `422` or `400` with the form HTML rather than redirecting. Redirecting on failure forces you to round-trip the user's input through the session. ## Other places 303 fits **Asynchronous work.** A POST that starts a long-running job returns `202 Accepted` with a status URL; when the job completes, polling that status URL can answer `303 See Other` pointing at the finished result. "Not here, over there, go GET it" is exactly 303's meaning. **Logout and session transitions.** After a POST that destroys a session, 303 to the public home page leaves the user on a clean GET. ## Practical cautions - **Do not redirect to a user-supplied URL.** Taking `Location` from a `next` or `returnUrl` parameter without validating it against an allow-list of paths or hosts is an open-redirect vulnerability — attackers use it to make phishing links appear to originate from your domain. Accept relative paths only, or match against a fixed allow-list. - **Cost:** PRG adds a round trip to every form submission. That is almost always worth it for browser flows, and almost never worth it for machine APIs, where returning `201 Created` with a `Location` header and the created representation in the body is better than forcing a second call. - **Do not use 303 to signal an error.** It means "your answer is elsewhere", not "something went wrong".
- Post/Redirect/Get throws away the response body of the POST. How do you still show the user a "your order was placed" message?Three options in order of robustness: render it from the resource itself, so /orders/1042 shows a confirmation because the order exists and is fresh; pass a query parameter such as ?created=1, remembering it is user-controllable and must be escaped and never trusted for authorisation; or store a one-shot flash message in the session that the GET reads and clears. The first is stateless and survives refresh and sharing best.
- When would you prefer 307 over 303 after a POST?When the request genuinely needs to be re-executed as a POST at a different location — for example an endpoint that has moved to another host or region and the write must still happen there. 303 is for the case where the write already succeeded and you are directing the client to fetch the result. Choosing 307 after a completed write would re-submit the body and risk a duplicate.
- Why is taking the redirect target from a request parameter dangerous?It creates an open redirect: an attacker crafts a link on your trusted domain whose parameter points at a site they control, and the victim is bounced there after appearing to visit you, which is highly effective for phishing and can also leak tokens in the referrer. The fix is to accept only relative paths, or to validate the target against a fixed allow-list of hosts, rejecting anything else rather than trying to sanitise it.
You hand a form to a clerk; instead of answering over the counter they say "your receipt is at window 4". You walk there and simply collect it — and you can come back to window 4 any time without re-filing the form.
saying these in an interview costs you the question
- Saying 303 preserves the original request method
- Believing 303 responses are cached like 301 responses
- Using 303 to report a validation failure instead of re-rendering the form with 400 or 422
- Redirecting to an unvalidated URL taken from a query parameter
- Thinking the browser's resubmission warning is adequate protection against duplicate submissions