HTTP status code · Redirection (3xx)
303 See Other
303 See Other tells the client to fetch a different URL, given in Location, with a GET request, as the answer to what it just did. Its classic use is Post/Redirect/Get: after a form POST, the server redirects to a result page so refreshing it does not submit the form twice.
| Class | 3xx, Redirection |
|---|---|
| Defined in | RFC 9110 §15.4.4 |
| Cacheable by default | Only with explicit Cache-Control or Expires |
| Safe to retry | Do not repeat the original request; GET the URL in Location |
| Relevant headers |
|
What 303 means
RFC 9110, section 15.4.4, makes 303 the one redirect that always means GET (or HEAD) next, whatever method the original request used, and says it applies to any method. It exists because 302 was ambiguous in the 1990s: some browsers re-sent the POST, others switched to GET. With 303 there is nothing to guess.
The URL in Location is not a new address for the original resource; it is a separate resource describing the outcome, such as /orders/1042 after POST /orders. That result page can be bookmarked, shared and cached on its own terms, which a POST response cannot. The 303 itself is not stored by caches unless it has explicit freshness headers.
When to use it
- After a successful form POST (sign-up, checkout, comment) to show the result page with GET.
- After accepting a long-running job, pointing the client at the status or result resource.
- In Next.js, redirect() inside a Server Action already responds with 303, so you get Post/Redirect/Get without choosing a code.
Common causes
If you run the server
- In FastAPI or Starlette, returning RedirectResponse after a form POST without status_code: the default is 307, so the browser POSTs the form again to the result URL, which then answers 405 Method Not Allowed.
- Using 302 after a POST works in browsers but leaves other HTTP clients free to repeat the POST at the new URL.
How to fix it
If you run the server
- Return 303 explicitly after any state-changing POST that should end on a page, and make the target a plain GET route.
- Do the work before redirecting: if the POST fails, answer 4xx with the form and its errors instead of a 303.
How to send 303
app.post('/orders', async (req, res) => {
const order = await createOrder(req.body);
// The browser loads the receipt with GET; refresh will not re-submit
res.redirect(303, `/orders/${order.id}`);
});// app/orders/route.ts
export async function POST(request: Request) {
const order = await createOrder(await request.formData());
return Response.redirect(new URL(`/orders/${order.id}`, request.url), 303);
}
// In a Server Action, redirect() from next/navigation already sends 303.mux.HandleFunc("POST /orders", func(w http.ResponseWriter, r *http.Request) {
id, err := createOrder(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
http.Redirect(w, r, "/orders/"+id, http.StatusSeeOther) // 303
})from fastapi import FastAPI, Form
from fastapi.responses import RedirectResponse
app = FastAPI()
@app.post("/orders")
def create_order(product_id: int = Form(...)):
order = save_order(product_id)
# Without status_code=303 the default 307 would re-POST the form
return RedirectResponse(f"/orders/{order.id}", status_code=303)Commonly confused with
- 303 vs 302
- 302 only tolerates the switch to GET for historical reasons; 303 requires it, which makes the intent explicit to every client.
- 303 vs 307
- 307 is the opposite rule: repeat the same method and body. After a form POST that means submitting it again.
- 303 vs 201
- 201 Created answers a POST directly and points at the new resource in Location; 303 sends the client off to GET a page about the result.
Frequently asked questions
- When should I use 303 instead of 302?
- Whenever the redirect follows a POST, PUT or DELETE and the client should load a result page with GET. Browsers treat both the same in that case, but 303 states the intent in the protocol, so API clients and libraries do not have to guess.
- Should a REST API return 201 or 303 after a POST?
- Return 201 Created with a Location header and, usually, the new resource in the body. A 303 fits better when the result lives at an existing resource, such as a duplicate submission pointing to the record that already exists.
- Does a 303 also turn PUT and DELETE into GET?
- Yes. RFC 9110 says 303 applies to any method, and the Fetch standard followed by browsers changes the method to GET and drops the body for everything except HEAD.
Last reviewed by Arielton Oberek.