302 Found
Redirection response defined by RFC 9110 §15.4.3.
Last updated July 27, 2026
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.
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.