HTTP 302 is Found: the resource is temporarily at the URL in Location, and the client should keep using the original URL for future requests. HTTP/1.0 called it Moved Temporarily; RFC 9110 section 15.4.3 defines it now. It’s the default redirect almost everywhere, which is why it’s the one most often misused.
The POST problem
RFC 9110 lets a user agent change POST into GET when following a 302, “for historical reasons”, and browsers do: the Fetch standard turns a POST into a GET with no body after a 301 or 302. So a 302 doesn’t say what happens to the method, and two later codes do:
302 Found temporary; POST may come back as GET
303 See Other temporary; always GET the new URL
307 Temporary Redirect temporary; same method, same body
The post/redirect/get pattern, where a form submission redirects to a result page so that reloading doesn’t submit twice, is what 303 is for. A temporary move of an endpoint that takes POST is 307. A move for good is 301 or 308.
Who sends it by default
res.redirect() in Express, redirect() in Flask and Django, nginx’s rewrite ... redirect, and PHP’s header('Location: ...') all send 302 unless told otherwise. Login flows run on it: Django’s @login_required answers an anonymous visitor with a 302 to the login page rather than a 401, and the OAuth 2.0 examples in RFC 6749 send the browser back to the app with a 302.
Caching
302 isn’t heuristically cacheable: with no Cache-Control or Expires, caches don’t keep it, so every request goes to the original URL first. That’s the point of a temporary redirect. If you want it reused for a while, send Cache-Control: max-age=... with it.
Search engines
Google follows 302 and 307 and treats them as a weak signal that the target should be canonical, against the strong signal of 301 and 308. If the move is permanent, say so with a 301; a redirect that has stayed “temporary” for years is a permanent one with the wrong code.
In your stack
res.redirect('/login'); // Express: 302
res.redirect(303, `/orders/${order.id}`); // after a form POST
location = /sale { return 302 /spring-sale; }
return redirect(url_for("login")) # Flask: 302
return redirect(url_for("order", id=order.id), code=303)
http.Redirect(w, r, "/login", http.StatusFound)
http.Redirect(w, r, "/orders/4182", http.StatusSeeOther) // after a POST
Search 301 302 307 308 in the tool below to compare the four redirects side by side.