HTTP 308 is Permanent Redirect: the resource has moved for good to the URL in Location, and the client must repeat the request there with the same method and body. RFC 7538 introduced it in 2015, and RFC 9110 section 15.4.9 now defines it. It’s 301 without the loophole that lets a POST become a GET.
The exchange
POST /api/order HTTP/1.1
Content-Type: application/json
HTTP/1.1 308 Permanent Redirect
Location: /api/orders
The client sends the same POST, body and all, to /api/orders, and may remember the move for next time.
308 or 301
301 Moved Permanently permanent; browsers turn POST into GET
308 Permanent Redirect permanent; same method and body
For pages fetched with GET, the two behave the same. Use 308 when the URL also takes POST, PUT or DELETE: an API route, a webhook receiver, a form’s action. A 301 there turns a POST into a body-less GET that your new handler probably answers with 405.
Both are heuristically cacheable (RFC 9110 section 15.1), so browsers keep them with no cache headers at all. Test a new 308 with a short Cache-Control: max-age before you commit to it. Google treats 308 and 301 the same: a strong signal that the target should be canonical.
Older clients
RFC 9110 tells a client to handle a 3xx it doesn’t recognize as 300 Multiple Choices, which isn’t followed automatically. A client written before 308 existed may stop at it instead of following. Current browsers, curl and the mainstream HTTP libraries all follow 308; a 301 is the safer choice only for plain GET traffic from clients you can’t vouch for.
Who sends it
- Next.js redirects with
permanent: truesend 308, and 307 withpermanent: false. - ingress-nginx sends 308 for its HTTP-to-HTTPS redirect. The
http-redirect-codekey in its ConfigMap changes that to 301, 302 or 307.
In your stack
location = /api/order { return 308 /api/orders; }
res.redirect(308, '/api/orders'); // Express
// next.config.js
module.exports = {
async redirects() {
return [{ source: '/api/order', destination: '/api/orders', permanent: true }];
},
};
return redirect("/api/orders", code=308) # Flask
return RedirectResponse("/api/orders", status_code=308) # FastAPI
http.Redirect(w, r, "/api/orders", http.StatusPermanentRedirect)