HTTP 308 Permanent Redirect

Permanent Redirect

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: true send 308, and 307 with permanent: false.
  • ingress-nginx sends 308 for its HTTP-to-HTTPS redirect. The http-redirect-code key 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)

Try it in the HTTP Status Codes

308 Permanent Redirect

  1. 308Permanent Redirect3xx Redirection

    The resource has moved for good, and the client must repeat the request at the new URL with the same method and the same body.

    When to use it

    An endpoint that has been renamed where the method matters, and a whole hostname moving without losing POSTs.

    Common mistakes

    Using it for a move you may undo, and counting on every client to follow it: one that predates 308 treats it as 300 Multiple Choices, as it must any 3xx it does not know, and may not follow it automatically.

    Cacheable by defaultRFC 9110

Every code, by class

Codes marked unofficial come from one server’s own convention — nginx, Cloudflare, cPanel, Microsoft — and are not in the IANA registry.

Runs in your browser. Nothing you enter is sent anywhere.