Short answer: 307 and 308 are redirects that preserve the original HTTP method. A 301 or 302 lets the browser turn a POST into a GET when it follows the redirect. A 307 or 308 requires the browser to repeat the exact same request – same method, same body – at the new URL. Use 308 for permanent redirects on anything that handles POST/PUT/DELETE. Use 307 for temporary redirects on the same.
The rest of this guide covers why method preservation matters, when 307 beats 302, when 308 beats 301, how to implement them in Nginx and Apache, and the gotchas around caching, browsers, and SEO.
The full redirect family at a glance#
All four of these codes tell the browser “the resource is at a different URL, go there instead.” The differences are in two dimensions: permanent vs temporary, and whether the HTTP method is preserved.
| Code | Permanent? | Method preserved? | Typical use |
|---|---|---|---|
| 301 Moved Permanently | Permanent | No – POST becomes GET | URL change, domain move, HTTP to HTTPS |
| 302 Found | Temporary | No – POST becomes GET | Maintenance page, A/B test, geo redirect |
| 307 Temporary Redirect | Temporary | Yes – method and body preserved | API endpoints, form submissions, temporary relocations |
| 308 Permanent Redirect | Permanent | Yes – method and body preserved | Permanent API moves, permanent form endpoint changes |
301 and 302 are the originals, defined in HTTP/1.0 and HTTP/1.1. 307 and 308 were added later (307 in RFC 2616, 308 in RFC 7538, 2015) specifically to fix a long-standing ambiguity in how browsers handle non-GET requests after a redirect.
See HTTP 302 redirect: what it means and when it is used for the full picture on 301 vs 302. This article picks up where that left off.
Why 307 and 308 exist: method preservation#
The oldest and most confusing quirk of 301 and 302 is this: when a browser follows a 301 or 302 redirect for a POST request, most browsers silently change the method to GET. The body is discarded. The second request is a simple GET to the new URL.
This was never what the HTTP spec intended. RFC 1945 (HTTP/1.0) said browsers should not automatically redirect POST requests without user confirmation. In practice, every major browser ignored that and did the auto-redirect anyway – but changed POST to GET to avoid resubmitting form data unexpectedly. It became a de facto standard.
The problem is that GET is not POST. A POST to
/api/orders
with a JSON body that gets redirected to
/api/v2/orders
with a 301 arrives at the new endpoint as a GET with no body. The order is not placed. The client sees a 200 (or 405, or whatever the GET handler returns) and thinks the request succeeded. Data is silently lost.
307 and 308 were introduced to solve this. They explicitly say: the browser must use the same method as the original request, and must include the original body. A POST to a URL that returns 307 becomes another POST to the new URL, with the body intact.
The rule, simply
- 301 / 302 = method can change. Safe for GET redirects. Unsafe for POST, PUT, PATCH, DELETE.
- 307 / 308 = method never changes. Safe for any method.
If the endpoint you are redirecting only ever serves GET requests (a blog post, a static page, a category archive), 301 and 302 are fine. If the endpoint handles any non-GET method (an API, a form handler, a webhook), use 307 or 308 to avoid silent data loss.
307 vs 302: when to use 307#
307 is the method-preserving version of 302. Both are temporary. The choice between them depends on what the endpoint does.
Use 307 when:
- You are temporarily redirecting an API endpoint that accepts POST, PUT, PATCH, or DELETE.
- You are temporarily redirecting a form submission endpoint.
- You are temporarily redirecting a webhook receiver.
- You need to guarantee the client’s method and body survive the redirect.
Use 302 when:
- The endpoint only serves GET requests (a web page, an image, a static asset).
- You want the maximum compatibility – 302 has been understood by every HTTP client since 1996; 307 is widely supported but occasionally mishandled by very old tooling.
In practice, for a modern site or API, 307 is the safer default for any endpoint that is not purely read-only. The worst thing 307 does is behave identically to 302 on ancient clients; the worst thing 302 does is silently corrupt POST requests.
308 vs 301: when to use 308#
308 is the method-preserving version of 301. Both are permanent. Same logic applies.
Use 308 when:
- You permanently moved an API endpoint (
/api/v1/users→/api/v2/users). - You permanently moved a webhook endpoint.
- You permanently changed a form submission URL.
- The endpoint handles anything other than GET and you want browsers, bots, and cached clients to repeat the exact request at the new location.
Use 301 when:
- You are moving a GET-only resource – a blog post, a page URL, a domain migration, HTTP to HTTPS.
- You want the widest possible support. Every HTTP client, crawler, and tool handles 301 perfectly. 308 is well-supported in modern browsers and crawlers but occasional ancient clients still treat it as 301 and change POST to GET (the very bug 308 was designed to fix).
For most WordPress and static-site redirect scenarios, 301 is the right answer – those are GET-heavy. For APIs and form endpoints, 308 is the right answer.
Implementing 307 and 308 in Nginx#
Nginx uses the
return
directive with the status code and the target URL. The target URL can be absolute or use Nginx variables.
307 temporary redirect:
location /api/old-endpoint {
return 307 /api/new-endpoint;
}
308 permanent redirect:
location /api/v1/users {
return 308 /api/v2/users$is_args$args;
}
The
$is_args$args
preserves query strings. Without it,
?page=2&limit=10
gets dropped during the redirect.
Redirect an entire domain with 308 (permanent, method-preserving):
server {
listen 443 ssl;
server_name old.example.com;
return 308 https://new.example.com$request_uri;
}
$request_uri
includes the path and query string exactly as the client sent them, so a
POST https://old.example.com/api/orders
with a JSON body becomes a
POST https://new.example.com/api/orders
with the same JSON body.
For the general case of forcing HTTPS, see how to redirect HTTP to HTTPS in Nginx – HTTP-to-HTTPS is a GET-dominated redirect, so 301 is the conventional choice there.
Implementing 307 and 308 in Apache (.htaccess)#
Apache uses
Redirect
and
RedirectMatch
for simple cases, and
mod_rewrite
for pattern matching. The status code is the second argument.
307 with Redirect:
Redirect 307 /api/old-endpoint /api/new-endpoint
308 with Redirect:
Redirect 308 /api/v1/users /api/v2/users
308 with mod_rewrite (for pattern matching):
RewriteEngine On
RewriteRule ^api/v1/(.*)$ /api/v2/$1 [R=308,L]
The
R=308
flag sets the status code. The
L
flag says “last rule, stop processing.” Without
L
, subsequent rules might modify the response and break the redirect.
One Apache-specific gotcha: some older versions of Apache (before 2.4) did not fully support 308 in
Redirect
– you would get a “Redirect: 308 is not a valid HTTP/1.1 status code” error on startup. If you hit this on a legacy server, use
mod_rewrite
with
R=308
instead, which works in older versions.
Gotchas and edge cases#
Browser caching of 308
308, like 301, is cached aggressively by browsers. This is a feature – you do not want the browser to re-check the old URL on every request – but it is a trap if you deploy a 308 and then want to remove it.
If you published a 308 redirect and later need to undo it, every browser that hit the old URL has the redirect cached, usually for a long time. The only reliable fix is to:
- Change the redirect to a 302 or 307 pointing back to the original URL for a few weeks (to overwrite the cached 308).
- Then remove the redirect entirely.
This is the same problem 301 has. Never use 308 (or 301) for anything that might reasonably be temporary. Use 307 or 302 for “I am not sure if this is permanent.”
Browser back button behavior
Before 307/308 existed, the browser back button after a POST + 302 redirect would sometimes re-submit the form, showing the “Confirm form resubmission” dialog. With 307 and 308, browsers are supposed to preserve the method, so the back button still asks for confirmation when returning to a POST response – the UX is the same, but the underlying behavior is now predictable.
Search engine handling
Google treats 307 like 302 (temporary, old URL stays indexed) and 308 like 301 (permanent, signal flows to new URL) for ranking purposes. Other search engines mostly match Google’s behavior. So for SEO, the choice between 301 and 308 is neutral – pick based on method preservation, not ranking.
If you are redirecting a GET-heavy site during a URL restructure, 301 is still the standard choice and has the widest crawler support. 308 is fine for Google but occasionally produces weirdness with smaller crawlers.
Proxy and CDN caching
Some CDNs cache redirect responses. A misconfigured 308 at the edge can get stuck in a CDN cache for the configured TTL, and a retry from origin will keep returning the cached redirect even after you fix the origin. Check your CDN’s documentation – most (Cloudflare, Fastly, CloudFront) have cache rules specifically for redirect status codes, and you may need to purge them after a redirect change.
307 from a POST handler blocks method-downgrade attacks
One subtle security property: some older reverse proxies would accept a POST, forward it as a GET to the backend (due to 301/302 handling), and bypass CSRF checks that only fired on POST. A 307 or 308 cannot be used to downgrade the method, which closes that class of bug. Not a reason to use 307 on its own – but worth knowing if you are auditing redirect chains for security.
Too-many-redirects loops
307 and 308 can loop the same way 301 and 302 can – if the target of the redirect also returns a redirect back to the original URL. Browsers break the loop after ~20 hops and show an error. If you see it in WordPress, too many redirects error in WordPress covers the common causes (HTTPS plugin + site URL mismatch, canonical redirect fighting a caching layer, etc.).
Testing redirects with curl#
curl -I
shows the response headers without following the redirect, which is the cleanest way to verify which code you are actually returning:
curl -I https://example.com/api/v1/users
Look for the first line:
HTTP/2 308
location: https://example.com/api/v2/users
To verify method preservation, send an explicit POST and follow the redirect:
curl -L -X POST -d '{"hello":"world"}' -H "Content-Type: application/json" https://example.com/api/v1/users
With a 307 or 308, the second request curl makes will also be a POST with the same body. With a 301 or 302, curl (by default) will change the method to GET on follow – the same bug browsers have. Add
--post301
/
--post302
flags to force curl to preserve the method for those codes if you need it.
See curl command: how to make HTTP requests from the command line for the full curl reference.
A quick decision tree#
When you need to pick a redirect code:
- Is this a GET-only resource (web page, image, static asset)?
- Permanent move: 301
- Temporary move: 302
- Does this endpoint accept POST, PUT, PATCH, or DELETE?
- Permanent move: 308
- Temporary move: 307
- Not sure if the move is permanent? Always start with temporary (302 or 307) and upgrade to permanent (301 or 308) once you are confident. Downgrading a cached permanent redirect is painful.
That is the whole story. 307 and 308 exist to fix one specific bug in how browsers handle POST requests after a redirect. If your redirect target ever sees anything other than GET, reach for 307 and 308. If it does not, 301 and 302 are fine and have slightly broader tooling support.
If you hit a 405 after setting up a redirect on an API endpoint, 405 method not allowed covers the most common cause – the new endpoint accepts the method your redirect preserved, but a middleware or framework in front of it does not.