Skip to main content
308

Permanent Redirect

Active
RFC 9110 §15.4.9Since 2015API versioning, domain migrations, permanent URL restructuring for non-GET resources

HTTP 308 Permanent Redirect indicates the target resource has been assigned a new permanent URI and the client MUST preserve the original request method. Defined in RFC 9110 §15.4.9. Unlike 301, which historically allowed method changes (POST→GET), 308 guarantees POST stays POST. The method-safe counterpart to 301.

Description

The 308 Permanent Redirect status code indicates that the target resource has been assigned a new permanent URI and any future references to this resource ought to use one of the enclosed URIs. The server SHOULD generate a Location header field containing the preferred URI reference for the new permanent URI. The user agent MAY use the Location field value for automatic redirection.

308 exists because 301 Moved Permanently has a critical historical flaw: browsers were allowed to change the request method from POST to GET during the redirect. This behavior was never mandated by the spec but became de facto standard across all major browsers. For GET-only resources this was harmless, but for APIs and form submissions relying on POST/PUT/PATCH semantics, it silently broke things — the server received a GET with no body instead of the original POST with its payload.

The relationship between redirect codes forms a clean 2×2 matrix. For temporary redirects: 302 (may change method) vs 307 (must preserve method). For permanent redirects: 301 (may change method) vs 308 (must preserve method). 308 is to 301 what 307 is to 302 — the method-preserving variant.

In practice, 308 is less common than 301 because the vast majority of redirects apply to GET requests (page moved, URL restructured) where method preservation is irrelevant. 308 becomes critical specifically when permanent URL changes affect non-GET endpoints: API versioning where POST /v1/orders permanently moves to POST /v2/orders, domain migrations for webhook receivers, or permanent restructuring of REST resource hierarchies that handle mutations.

All modern browsers support 308 correctly. Internet Explorer 11 did not, but IE is effectively dead. Server-side HTTP clients (curl, axios, fetch) all respect the method preservation semantics. HSTS preload lists use 307 for HTTP→HTTPS enforcement because the redirect is conceptually temporary (the resource didn't move — the scheme changed), though some argue 308 is more semantically correct for domains that will never serve HTTP again.

Examples

POST request to a permanently moved API endpoint
http
POST /api/v1/orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer token123

{"product_id": "prod_abc", "quantity": 2, "shipping": "express"}
308 response — method MUST be preserved
http
HTTP/1.1 308 Permanent Redirect
Location: https://api.example.com/api/v2/orders
Content-Type: application/json

{"message": "The /v1/orders endpoint has permanently moved to /v2/orders. Please update your integration.", "new_url": "https://api.example.com/api/v2/orders", "documentation": "https://docs.example.com/migration/v1-to-v2"}
PUT request during domain migration
http
PUT /users/42/profile HTTP/1.1
Host: old-api.example.com
Content-Type: application/json
If-Match: "etag-v7"

{"display_name": "Alice Smith", "bio": "Engineer"}
308 response — permanent domain move
http
HTTP/1.1 308 Permanent Redirect
Location: https://new-api.example.com/users/42/profile
Cache-Control: public, max-age=31536000

{"message": "This API has permanently moved to new-api.example.com. All endpoints and methods are preserved."}
Webhook receiver URL permanently changed
http
POST /webhooks/stripe HTTP/1.1
Host: hooks.old-domain.com
Content-Type: application/json
Stripe-Signature: t=1234,v1=sig...

{"type": "payment_intent.succeeded", "data": {"object": {"id": "pi_abc", "amount": 2000}}}
308 response — webhook endpoint permanently relocated
http
HTTP/1.1 308 Permanent Redirect
Location: https://hooks.new-domain.com/webhooks/stripe

{"error": "endpoint_moved", "message": "Webhook receiver has permanently moved. Update your Stripe dashboard to use https://hooks.new-domain.com/webhooks/stripe"}

Edge Cases

  • 308 REQUIRES the client to preserve the request method AND body. A client that strips the body on redirect is violating the spec — but some older HTTP libraries did exactly this. Always test your HTTP client's redirect behavior with POST bodies.
  • Caching: 308 is cacheable by default (it's permanent). Include Cache-Control headers to control how long clients and intermediaries cache the redirect. A misconfigured cache can make rollbacks impossible if you need to un-redirect.
  • Redirect loops: if /a 308→/b and /b 308→/a, the client is trapped. Servers must maintain a complete redirect map and validate no cycles exist before deploying permanent redirects.
  • Request body size: some proxies and CDNs have body buffering limits. A 308 redirect of a large file upload (POST with 100MB body) may fail at the infrastructure layer because the proxy didn't buffer the original body to replay it at the new location.
  • Authentication headers: per RFC 9110, clients SHOULD strip Authorization headers when following a redirect to a different origin. A 308 from api.example.com to api2.example.com may lose the auth token — the client needs to re-authenticate at the new location.
  • Browser form submissions: if a user submits a form via POST and receives a 308, the browser MUST re-send the POST to the new URL. Unlike 303 (which converts to GET for the redirect target), 308 will cause the form data to be re-submitted — potentially creating duplicate resources if the new endpoint isn't idempotent.
  • SEO impact: search engines treat 308 identically to 301 — link equity passes to the new URL. However, some crawlers may not follow 308 if they only understand the older RFC 2616 status codes. For public-facing pages, 301 is safer; reserve 308 for API endpoints where method preservation matters.

When You'll See This

  • API version sunset — POST /v1/resource permanently moves to POST /v2/resource
  • Domain migration where the old domain's API endpoints permanently redirect to the new domain, preserving PUT/PATCH/DELETE semantics
  • Webhook receiver URL change — third-party services POSTing to your old URL get redirected to the new one while preserving the payload
  • Permanent URL restructuring in a REST API — /users/42/orders moves to /orders?user=42 but must still accept POST for order creation
  • Corporate acquisition — acquired company's API permanently redirects to parent company's API while preserving all HTTP methods
  • Moving from path-based to subdomain-based API routing — POST api.example.com/v2/payments permanently replaces POST example.com/api/v2/payments
  • CDN origin change where mutable endpoints (file uploads via POST) need to follow the new origin without losing the request body

Implementation References

LanguageConstant
Gohttp.StatusPermanentRedirect
Rusthttp::StatusCode::PERMANENT_REDIRECT
Pythonhttp.HTTPStatus.PERMANENT_REDIRECT
Node.jshttp.STATUS_CODES[308]
.NETHttpStatusCode.PermanentRedirect
JavaHttpURLConnection (no built-in constant; use 308 literal)
PHPResponse::HTTP_PERMANENTLY_REDIRECT (Symfony)
Ruby:permanent_redirect (Rack)

History

Originally defined in RFC 7238 (June 2014) as an experimental status code, then elevated to Standards Track in RFC 7538 (April 2015). Now consolidated into RFC 9110 §15.4.9 (June 2022) as part of the HTTP Semantics reorganization. Created specifically to address the decades-old ambiguity in 301 where user agents changed POST to GET despite no spec requirement to do so.

Related Status Codes

Related Headers

FAQ

What is the difference between 301 and 308?

Both indicate a permanent move. The critical difference is method preservation. With 301, browsers historically changed POST requests to GET when following the redirect — this was never required by the spec but became universal browser behavior. 308 explicitly forbids this: the client MUST use the same method (POST stays POST, PUT stays PUT). For GET-only resources, 301 and 308 are functionally identical. For APIs or form endpoints that receive POST/PUT/PATCH/DELETE, 308 is the only safe choice for permanent redirects.

When should I use 308 instead of 301?

Use 308 when the resource permanently moved AND the endpoint receives non-GET requests that must be preserved. Common cases: API version migrations (POST /v1/users → POST /v2/users), domain moves for webhook receivers, permanent restructuring of REST endpoints that handle mutations. Use 301 when the resource only serves GET requests (web pages, static assets, public URLs) — it has wider legacy support and identical behavior for GET-only traffic.

Do all browsers and HTTP clients support 308?

All modern browsers (Chrome 36+, Firefox 25+, Safari 7.1+, Edge 12+) support 308 correctly. Internet Explorer 11 does not — it either ignores the redirect or changes the method. However, IE is effectively dead (Microsoft ended support in 2022). Server-side HTTP clients including curl, axios, node-fetch, Python requests, Go's net/http, and Rust's reqwest all handle 308 with proper method preservation. The only risk is very old or embedded HTTP stacks that predate RFC 7538.