HTTP status code · Redirection (3xx)
302 Found
302 Found means the resource is temporarily at the URL in the Location header, and the client should keep using the original URL in the future. It is what most frameworks send for a redirect by default, most often to send a visitor to a login page and back.
| Class | 3xx, Redirection |
|---|---|
| Also known as | Moved Temporarily |
| Defined in | RFC 9110 §15.4.3 |
| Cacheable by default | Only with explicit Cache-Control or Expires |
| Safe to retry | Follow Location now, but keep using the original URL next time |
| Relevant headers |
|
What 302 means
HTTP/1.0 called this code Moved Temporarily; since RFC 2616 it has been named Found, and RFC 9110 defines it in section 15.4.3. Because the move is temporary, browsers do not store a 302 unless it carries explicit Cache-Control or Expires headers, and Google documents it as only a weak signal, so the original URL stays in search results.
Many tools pick 302 when you give no status: Express res.redirect(url), PHP header("Location: ...") and the Fetch API Response.redirect(url) all send it. That default suits logins and short detours but is wrong for permanent moves, and it is a frequent reason a migrated site keeps showing old URLs in Google.
A 302 also lets the client turn a POST into a GET for the follow-up request, which is what browsers do. That is harmless after a form submit but breaks API calls that expect the same POST to reach the new location; send 307 for those.
When to use it
- Sending a signed-out visitor to /login and back to the page they wanted afterwards.
- Short-lived detours: a maintenance notice, an A/B test bucket, a landing page chosen per request by language or region.
- Seasonal or rotating URLs such as /sale that point somewhere different over time.
Common causes
If you are visiting the site
- A login loop: the site sends you to /login, the login page never sees a valid session cookie (blocked cookies, a strict privacy extension, a wrong system clock) and sends you back again.
- Captive portals on hotel, airport or café Wi-Fi answer any plain HTTP request with a 302 to their sign-in page.
If you run the server
- The framework default redirect used for a permanent URL change, so search engines keep indexing the old address.
- A session cookie that is set but never sent back, because its Domain, Path, Secure or SameSite attributes do not match the redirected request, so auth middleware redirects every request to login.
- An API endpoint answering POST with 302: the client repeats the call as GET without a body, and the request fails further down with 405 or a validation error.
- A 302 that lands on a URL which redirects again, adding hops to every visit.
How to fix it
If you are visiting the site
- Allow cookies for the site, delete its existing ones and log in again; check that your device clock is correct.
- On public Wi-Fi, open a plain http:// page such as http://neverssl.com to bring up the sign-in portal, then try again.
If you run the server
- If the move is permanent, change the status to 301 for GET pages or 308 for anything that receives other methods.
- Inspect the Set-Cookie header on the login response with curl -i and check that the cookie comes back on the redirected request.
- For APIs, answer with 307 when the client must repeat the same request, or 303 when it should fetch a result with GET.
- Collapse chains so each 302 points directly at a URL that answers 200.
How to send 302
app.get('/account', (req, res) => {
if (!req.session.user) {
// res.redirect(url) alone also sends 302
return res.redirect(302, '/login?next=' + encodeURIComponent(req.originalUrl));
}
res.render('account');
});// app/account/route.ts
import { cookies } from 'next/headers';
export async function GET(request: Request) {
const session = (await cookies()).get('session');
if (!session) {
return Response.redirect(new URL('/login', request.url), 302);
}
return Response.json({ ok: true });
}mux.HandleFunc("GET /account", func(w http.ResponseWriter, r *http.Request) {
if _, err := r.Cookie("session"); err != nil {
next := url.QueryEscape(r.URL.RequestURI())
http.Redirect(w, r, "/login?next="+next, http.StatusFound) // 302
return
}
// ...render the account page
})from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse
app = FastAPI()
@app.get("/account")
def account(request: Request):
if "session" not in request.cookies:
return RedirectResponse("/login", status_code=302)
return {"ok": True}# A campaign URL that points somewhere different each season
location = /sale {
return 302 /summer-sale-2026;
}Commonly confused with
- 302 vs 301
- 301 is permanent: browsers cache it and search engines move rankings to the target. 302 is temporary and changes neither.
- 302 vs 307
- 307 is the strict version of 302: the client must repeat the request with the same method and body instead of switching to GET.
- 302 vs 303
- 303 says explicitly that the next request must be a GET for a different resource; 302 only tolerates that switch for historical reasons.
Frequently asked questions
- Is a 302 redirect bad for SEO?
- Not when the move really is temporary; that is what it is for. Google treats a 302 as a weak signal and keeps the original URL in results. It becomes a problem only when a permanent move is made with 302, so use 301 or 308 for those.
- Is 302 Found the same as 302 Moved Temporarily?
- Yes. It is the same code: HTTP/1.0 (RFC 1945) called it Moved Temporarily, and HTTP/1.1 renamed it Found. The reason phrase is informational only; clients act on the number.
- Why does curl not follow a 302 redirect?
- curl only follows redirects with -L (--location). When it does, a POST sent with -d is re-sent as GET after a 301, 302 or 303; add --post302 to keep the POST, or use -X POST knowing it forces the method on every hop.
- Does a 302 keep the form data from a POST?
- In browsers, no: the follow-up request is a GET with no body. If the data must reach the new URL, the server has to send 307 instead, which obliges the browser to repeat the POST.
Last reviewed by Arielton Oberek.