Skip to content
Control Plane Labs

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.

Other 3xx codes