301 vs 302 vs 307 vs 308 redirects (tested)

All four codes send the client to a new URL. They differ in two ways: is the move permanent or temporary, and does a POST stay a POST. In our test, 301 and 302 turned a POST into a GET and lost the body. 307 and 308 kept the POST and the body.

1. The short answer

CodePermanent?POST stays POST?Use it for
301 Moved PermanentlyYesNo (in practice)Moving pages for good (GET traffic)
302 FoundNoNo (in practice)Temporary moves of GET pages
303 See OtherNoNo, always GETAfter a form POST, send the user to a result page
307 Temporary RedirectNoYesTemporary moves of APIs and forms
308 Permanent RedirectYesYesPermanent moves of APIs and forms

The spec says 301 and 302 should not change the method. Real clients have changed POST to GET for decades, and the spec now allows it. 307 and 308 were added to forbid the change.

2. Our test setup

A Node server answered /301, /302, /303, /307 and /308 with Location: /target. At /target it logged the method and body it got. Each client sent POST with the JSON body {"a":1}.

curl -s -L -d '{"a":1}' -H 'content-type: application/json' http://127.0.0.1:8993/301
{"method":"GET","body":""}

curl -s -L -d '{"a":1}' -H 'content-type: application/json' http://127.0.0.1:8993/308
{"method":"POST","body":"{\"a\":1}"}

3. Results: method that arrived at /target

Codecurl -L -dcurl -X POST -LNode fetchPython urllibChromium fetch()
301GET, no bodyPOST, empty bodyGET, no bodyGET, no bodyGET, no body
302GET, no bodyPOST, empty bodyGET, no bodyGET, no bodyGET, no body
303GET, no bodyPOST, empty bodyGET, no bodyGET, no bodyGET, no body
307POST + bodyPOST + bodyPOST + bodyNot followed (HTTPError 307)POST + body
308POST + bodyPOST + bodyPOST + bodyNot followed (HTTPError 308)POST + body

Python requests was not installed on the test box, so it is not in the table.

4. Three surprises

curl -X POST -L is a trap. It forces the word POST on every hop. But on 301, 302 and 303 the body was gone. The target got a POST with an empty body. If you want curl to resend the POST with its body, use --post301 --post302 --post303 instead of -X POST. With those flags, our target got POST plus the body for all three codes.

curl -s -L --post301 --post302 --post303 -d '{"a":1}' http://127.0.0.1:8993/301
{"method":"POST","body":"{\"a\":1}"}

Python urllib does not follow 307 or 308 for POST. It raised HTTPError with code 307 and 308. It did follow 301, 302 and 303, as a GET.

Chromium caches 301 and 308. We sent two GET fetches to each redirect URL, with no Cache-Control header on the redirect. The server saw /301 and /308 only once each. The second time, Chromium went straight to /target from cache. /302 and /307 were hit twice. Two page navigations gave the same result: /301 and /308 once, /302 twice.

5. SEO (from Google's docs, not measured)

Google treats 301 and 308 as permanent. They are a strong signal that the new URL should be shown in search. Google treats 302 and 307 as temporary. The old URL is more likely to stay in search. For a site move, use 301 or 308.

Be careful with permanent redirects. Browsers cache them, as we saw above. If you set a 301 by mistake, visitors who already saw it may keep going to the wrong place until their cache clears. Test with 302 first, then switch to 301 when you are sure.

6. Which one to pick

Page moved for good: 301. Page moved for now: 302. API or form endpoint moved, and POST must stay POST: 308 (permanent) or 307 (temporary). After a form submit, show a result page: 303.

To see every hop and its code, follow the redirect chain check. If a PHP redirect does nothing, see PHP header Location not working. If a 301 turned your POST into a GET and the target only accepts POST, you may see a 405 Method Not Allowed. Switch to 308.

Frequently asked questions

Does a 301 redirect change POST to GET?

In practice, yes. In our test curl -L, Node fetch, Python urllib and Chromium all turned a POST into a GET with no body after a 301. Use 308 if the POST must survive.

What is the difference between 307 and 308?

Both keep the method and body. 307 is temporary. 308 is permanent, and in our test Chromium cached the 308 and skipped the server on the second request.

Why does curl -X POST -L still lose my body?

-X only changes the method name. On 301, 302 and 303 our target got POST with an empty body. Use --post301 --post302 --post303 to resend the body.

Is 308 as good as 301 for SEO?

Google's docs say Google treats 301 and 308 the same way: as permanent redirects. We did not measure search ranking.

How do I undo a 301 that browsers cached?

You cannot clear other people's caches from the server. Visitors must clear their cache, or wait for it to expire. That is why it is safer to test with 302 first.

Written with AI assistance. Measured on 2026-10-02 with curl 8.14.1, Node.js 24.21.0 fetch, Python 3.13.5 urllib and headless Chromium 153 (Playwright 1.63): a small Node server on 127.0.0.1:8993 answered /301 /302 /303 /307 /308 with a redirect to /target and logged the method and body that arrived. The redirects sent no Cache-Control header. Python requests was not installed, so it was not tested. PlainHeaders is our extension; curl, Node.js, Python and Chromium are free.