The grid
| Method may change to GET | Method must be preserved | |
|---|---|---|
| Permanent | 301 Moved Permanently | 308 Permanent Redirect |
| Temporary | 302 Found | 307 Temporary Redirect |
That is the whole thing. 307 and 308 exist because 301 and 302 were specified as method-preserving but every browser ever built turned a POST into a GET when following them, so the RFCs gave up and blessed the behaviour, then added two new codes that mean what the old ones were supposed to mean.
Permanent or temporary?
Permanent (301, 308) tells clients the old URL is dead: update your bookmarks, your links, your index. Browsers cache a permanent redirect aggressively (Chrome effectively forever unless Cache-Control says otherwise), so the next visit skips the request to the old URL entirely. Search engines transfer the old URL's ranking signals to the new one and drop the old one from results over time.
Temporary (302, 307) says the resource is elsewhere for now, keep using the original URL. Browsers do not cache it by default. Search engines keep the old URL indexed and, if the redirect persists for long enough, eventually start treating it as permanent anyway.
The trap with permanent redirects is that they are permanent in the client too. If you 301 /pricing to /plans and later change your mind, visitors whose browser cached the 301 will keep landing on /plans with no request ever reaching your server for you to fix it. Add Cache-Control: max-age=... to a 301 if you want a way back, or use a 302 until you are sure.
Does the method matter?
For a GET request, no: all four behave the same. It matters when the redirected request is a POST, PUT, PATCH or DELETE.
- After a 301 or 302, browsers and most HTTP libraries re-issue the request as a GET with no body. A form submission that hits a redirect from
http://tohttps://arrives at the HTTPS endpoint as an empty GET. Your handler sees no data and no error. - After a 307 or 308, the client must repeat the request with the same method and body. The POST arrives as a POST.
This is why API endpoints should never redirect with 301 or 302, and why HTTP to HTTPS redirects on anything that accepts POST should be 308 (or 307). It is also why the 302-after-POST pattern (submit a form, get redirected to a confirmation page) works: it deliberately turns the POST into a GET so that refreshing does not resubmit.
Which one to use
| Situation | Code | Why |
|---|---|---|
| HTTP to HTTPS, whole site | 301 | Permanent; every client should learn the HTTPS URL. Use 308 if the site accepts POSTs over HTTP (it should not, but legacy). Add HSTS on the HTTPS response so browsers stop asking. |
| www to non-www (or the reverse) | 301 | Canonical host, permanent. Search engines consolidate on the target. |
| Old URL structure to new after a rebuild | 301 | Transfers ranking, updates bookmarks. Keep the redirects in place for years; inbound links do not update themselves. |
| Trailing slash normalisation | 301 | Pick one form and redirect the other. Permanent. |
| After a successful form POST | 302 or 303 | Send the browser to a GET page. 303 See Other is the precise code for this; 302 is what everyone uses and behaves identically here. |
| Maintenance page, A/B test, geo redirect | 302 or 307 | Temporary. The original URL stays canonical. 307 if the request might be a POST. |
| API endpoint moved | 308 | Permanent and method-preserving. Clients that ignore 308 (rare now) get a clear failure rather than a silent GET. |
| Login required, send to sign-in | 302 | Temporary by nature; the user will come back. |
Chains and loops
Each redirect is a full round trip: DNS, connect, TLS, request, response. http://example.com/page to https://example.com/page to https://www.example.com/page to https://www.example.com/page/ is three redirects before any content, easily half a second on a mobile connection. Collapse chains so every old URL goes to the final destination in one hop. Search engines follow up to about five hops and then give up, and each hop leaks a little of the signal you were trying to transfer.
The redirect checker shows every hop with its status, Location header and timing, which makes chains and the 302-that-should-be-a-301 obvious. It also shows a 3xx with no Location header, which is a misconfiguration that browsers render as a blank page.
Details that bite
- Relative Location headers are allowed since RFC 7231 but some old clients want absolute URLs. Absolute is safer.
- Redirecting a HEAD should give the same status as the GET would. Some frameworks 405 it.
- Query strings are not automatically carried across. If
/old?id=5should reach/new?id=5, the redirect rule has to say so. - Cached 301s in your own browser make testing confusing. Use a private window or curl.
- 304 Not Modified is not a redirect despite the number; it belongs to caching.
- Meta refresh and JavaScript redirects are not HTTP redirects. Search engines treat them as weaker signals and curl does not follow them at all.