Skip to main content
303

See Other

Active
RFC 9110 §15.4.4Since 1999Form submissions, PRG pattern, payment processing, REST APIs after writes

HTTP 303 See Other instructs the client to retrieve the result of the request at a different URI using GET, regardless of the original method. Defined in RFC 9110 §15.4.4. The canonical Post/Redirect/Get (PRG) pattern uses 303 to prevent duplicate form submissions on browser refresh.

Description

The 303 See Other status code indicates that the server is redirecting the client to a different resource, identified by a URI in the Location header field, which is intended to provide an indirect response to the original request. The client is expected to perform a GET request on that URI regardless of the original request method. This is the critical distinction from 307 and 302.

303 is NOT about a resource that has moved. It means: 'the ACTION you requested has been performed, and the RESULT is available at this other URI — go GET it.' The resource at the new URI is semantically different from the resource that was originally targeted. A POST creates an order; the 303 points to the order confirmation page. A POST processes a payment; the 303 points to the receipt.

The Post/Redirect/Get (PRG) pattern is the primary use case. Without PRG, refreshing a browser page after a POST re-submits the form. With PRG: the server processes the POST, responds with 303 + Location header, the browser follows with GET, and now the browser's address bar shows the result page. Refreshing that page just re-GETs the result — no duplicate submission.

The method change is MANDATORY. Unlike 302 (which historically was ambiguous about method preservation) and 307 (which explicitly preserves the method), 303 ALWAYS converts the follow-up request to GET. The original request body MUST NOT be retransmitted. This makes 303 safe for any method — POST, PUT, DELETE — that wants to redirect to a viewable result.

303 carries no SEO implications. Search engines do not transfer link equity through 303 redirects because they represent temporary, action-result relationships — not permanent resource relocation. The redirect is functional, not navigational.

Examples

POST form submission triggering PRG pattern
http
POST /checkout/complete HTTP/1.1
Host: shop.example.com
Content-Type: application/x-www-form-urlencoded
Cookie: session=abc123

card_token=tok_visa4242&amount=9999&currency=usd
303 response — redirect to order confirmation
http
HTTP/1.1 303 See Other
Location: /orders/ord_7xK9mP/confirmation
Cache-Control: no-store
Set-Cookie: session=abc123; Path=/; HttpOnly
DELETE request with redirect to collection
http
DELETE /api/documents/doc_42 HTTP/1.1
Host: api.example.com
Authorization: Bearer token_xyz
Accept: text/html
303 response — redirect to updated collection view
http
HTTP/1.1 303 See Other
Location: /documents?deleted=doc_42&status=success
Content-Length: 0
Long-running operation — redirect to status endpoint
http
POST /api/reports/generate HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer token_xyz

{"type": "annual", "year": 2024, "format": "pdf"}
303 response — redirect to async status resource
http
HTTP/1.1 303 See Other
Location: /api/reports/rpt_8nQ3vL/status
Retry-After: 30

Edge Cases

  • 303 ALWAYS changes the method to GET — even if the original was PUT or DELETE. The request body is dropped. This is by design, not a bug.
  • If the Location header is missing or malformed, clients should treat the response as a final response and not follow any redirect. Always include a valid absolute or relative URI.
  • Infinite redirect loops: a 303 pointing to another 303 pointing back to the original. Browsers cap at ~20 redirects. APIs should enforce a maximum redirect depth.
  • Fragment identifiers (#section) in the original URL are NOT carried over to the redirect target unless explicitly included in the Location header value.
  • CORS preflight complications: a 303 from a cross-origin POST requires the target to also have proper CORS headers. The browser will make a new preflight for the GET if the origin differs.
  • Caching: RFC 9110 says 303 responses are not cacheable by default. However, if explicit caching headers (Cache-Control, Expires) are present, a cache MAY store the 303 response itself — not the target resource.
  • AJAX/fetch requests follow 303 automatically but change the method to GET. If your client-side code expects to read the response body from the POST, intercepting the redirect (redirect: 'manual' in fetch) is necessary.

When You'll See This

  • Form submission followed by redirect to confirmation page (Post/Redirect/Get pattern)
  • Payment processing: POST payment, 303 to receipt page
  • User registration: POST credentials, 303 to welcome/dashboard page
  • File upload: POST multipart data, 303 to the uploaded file's view page
  • OAuth callback: POST authorization code exchange, 303 to the application's landing page
  • Long-running task submission: POST job, 303 to status/polling endpoint
  • Delete action in a web UI: DELETE resource, 303 back to the collection listing

Implementation References

LanguageConstant
Gohttp.StatusSeeOther
Rusthttp::StatusCode::SEE_OTHER
Pythonhttp.HTTPStatus.SEE_OTHER
Node.jshttp.STATUS_CODES[303]
.NETHttpStatusCode.SeeOther
JavaHttpURLConnection.HTTP_SEE_OTHER
PHPResponse::HTTP_SEE_OTHER (Symfony)
Ruby:see_other (Rack)

History

Introduced in HTTP/1.1 (RFC 2616, 1999) specifically to resolve the ambiguity of 302 Found, where browsers inconsistently changed POST to GET on redirect despite the spec saying they shouldn't. 303 was created to explicitly mandate the method change to GET. Retained and clarified in RFC 7231 (2014) and RFC 9110 (2022) §15.4.4 as the definitive 'redirect with GET' status code.

Related Status Codes

Related Headers

FAQ

What is the difference between 303 See Other and 307 Temporary Redirect?

303 ALWAYS changes the method to GET and drops the request body — the redirect target is a different resource representing the result of the action. 307 preserves the original method and body exactly — the same request should be repeated at the new location. Use 303 after a POST when you want the browser to display a result page. Use 307 when the resource itself has temporarily moved and the client should repeat the exact same request elsewhere.

Why was 303 created when 302 already existed?

302 Found was specified in HTTP/1.0 to mean 'repeat the request at this new URL with the same method.' But browsers universally changed POST to GET on 302 redirects, violating the spec. This created an ambiguity: does 302 preserve the method or not? HTTP/1.1 resolved this by splitting the semantics: 303 explicitly means 'follow with GET' (what browsers were already doing), and 307 explicitly means 'preserve the method' (what 302 was supposed to mean). 302 remains for backward compatibility but its behavior is technically undefined for non-GET methods.

How does the Post/Redirect/Get (PRG) pattern work with 303?

Step 1: The client submits a form via POST. Step 2: The server processes the submission (creates order, processes payment, etc.). Step 3: Instead of returning a 200 response body directly, the server returns 303 with a Location header pointing to the result page. Step 4: The browser automatically follows the redirect with a GET request. Step 5: The result page loads normally. Now the browser's last request was a GET — refreshing the page re-GETs the result instead of re-POSTing the form. This eliminates the 'Confirm Form Resubmission' dialog and prevents duplicate orders, payments, or registrations.