302 Found
Redirection response defined by RFC 9110 §15.4.3.
Written and maintained by Ben Ennis
Last reviewed July 27, 2026 · How we verify this
Common causes at a glance
- An unauthenticated request being bounced to a login page
- Post/redirect/get after a successful form submission
- A framework default: Flask redirect(), Django HttpResponseRedirect, Rails redirect_to
- A/B testing or geo routing sending a client to a variant URL
Reproduce it
curl -sSI https://example.com/dashboard | grep -iE '^(HTTP|location)'What 302 Found means
302 is the default redirect in almost every web framework: log in, get bounced to your dashboard; arrive unauthenticated, get bounced to the login form. Unlike 301 it makes no claim about the future and is not cacheable by default, so you can change where it points whenever you like.
The method rewriting is the catch. RFC 2616 said clients must not change the method,
but every browser already did, so the requirement was relaxed to match reality. A 302
answering a POST produces a GET with the body dropped. For
post/redirect/get that is what you want. For an API redirecting a write to a new
endpoint, it silently turns an update into a read.
What the spec says
RFC 9110 §15.4.3 carries the same historical note as
301: a user agent MAY change the method from POST to GET. If
you need the method preserved, the spec points at 307; if you want
the rewrite guaranteed rather than conventional, that is 303. 302
sits between the two, which is why both were added. Unlike 301 and
308, a 302 is not heuristically cacheable under
RFC 9111 §4.2.2, so it is stored only when you send explicit
freshness information.
What actually causes it
The failure worth planning for is the method rewrite biting an API
client. A service answering POST /v1/orders with a 302 to
/v2/orders sees the client issue GET /v2/orders with no body,
and the caller reports that the write silently did nothing. The other classic is a
redirect loop, usually a session cookie the login handler sets on a different host or
with a Secure flag the client will not store, so the app bounces to login
forever. Watch too for 302s where the target never changes: those want a 301, since
search engines do not consolidate signals onto a temporary redirect.
How to debug it
Check the raw redirect and the method your client actually used on the second request:
curl -sSv -X POST -d 'id=1' -L https://example.com/orders 2>&1 | grep -E '^[<>] (POST|GET|HTTP|location)'
If the second request line says GET, the rewrite happened. Clients differ:
browsers and curl’s -L rewrite, --post302 keeps the method.
Better to change the server to 307 so every client behaves the
same without a flag.
For loops, --max-redirs 5 makes curl fail fast, and the useful signal is
usually Set-Cookie on the 302 rather than Location: compare
the cookie’s domain, path, and Secure attribute against the redirect
target with the header inspector.
Why a cross-origin CORS preflight cannot follow a 302
A CORS preflight is an
OPTIONS request the browser sends before certain cross-origin requests, and
the Fetch Standard requires the
CORS check to run only after the preflight response has an ok status, meaning a
2xx. A 302 is not a 2xx, so a preflight that gets redirected fails outright with a network
error, before the browser even looks at the Location header. This is
different from a normal navigation or a same-origin fetch, both of which follow a 302
without complaint.
The practical trap is an API gateway or load balancer that redirects a bare path to a
versioned one, for example POST /login to POST /v1/login. A
same-origin form submission or a server-to-server call follows the 302 transparently. A
cross-origin fetch() with a custom header or a non-simple content type
triggers a preflight first, and that preflight cannot follow the redirect, so the request
fails in the browser console with a CORS error that never mentions the word “redirect.”
Rewriting the path server-side, or pointing the client at the final URL directly, are the
only fixes; the browser will not chase the 302 for you.
Requests that do not trigger a preflight, plain GET or a simple
POST with no custom headers, follow the redirect exactly like a normal
fetch, since only the preflight OPTIONS request is restricted this way. If a
cross-origin call fails only when you add an auth header, check whether the endpoint is
redirecting before checking your CORS configuration.
Working examples
curl
# See the raw 302 and its Set-Cookie before following anything.
curl -sSI https://example.com/dashboard | grep -iE '^(HTTP|location|set-cookie)'
# Watch the method change across the redirect:
curl -sSv -X POST -d 'id=1' -L https://example.com/orders 2>&1 \
| grep -E '^[<>] (POST|GET|HTTP|location)'
# Keep POST across a 302 (non-default, and non-standard behaviour):
curl -sS -X POST -d 'id=1' -L --post302 https://example.com/orders
Python (requests)
import requests
# requests rewrites POST to GET on 302, matching browsers.
resp = requests.post("https://example.com/orders", data={"id": 1}, timeout=10)
for hop in resp.history:
print(hop.status_code, hop.request.method, hop.url)
print("final:", resp.status_code, resp.request.method, resp.url)
# To keep the method, follow the redirect yourself.
first = requests.post(
"https://example.com/orders", data={"id": 1},
allow_redirects=False, timeout=10,
)
if first.status_code == 302:
requests.post(first.headers["Location"], data={"id": 1}, timeout=10)
Node (fetch)
// fetch() follows a 302 on POST as GET, per the Fetch spec.
const resp = await fetch("https://example.com/orders", {
method: "POST",
body: new URLSearchParams({ id: "1" }),
});
console.log(resp.status, resp.url); // resp.url is the final URL
// Preserve the method by handling the hop manually.
const first = await fetch("https://example.com/orders", {
method: "POST",
body: new URLSearchParams({ id: "1" }),
redirect: "manual",
});
if (first.status === 302) {
const target = new URL(first.headers.get("location"), first.url);
await fetch(target, { method: "POST", body: new URLSearchParams({ id: "1" }) });
}Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.
Related reading
- Every HTTP status code, and when it shows up
The wider map: which codes you actually meet in production, and the pairs everyone confuses.