Skip to content
Control Plane Labs

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.

Other 3xx codes