HTTP

308 - Permanent Redirect

Getting a 308 Permanent Redirect means the resource permanently moved to a new URL—follow the Location header using the same HTTP method (POST stays POST, PUT stays PUT) and update all references permanently. This client-side redirect (3xx) is like 301 but preserves the request method. Most common when APIs permanently migrate endpoints and method must be preserved, but also appears when domains permanently change, URL structures are permanently restructured, or permanent redirects with method preservation are needed.

#Common Causes

  • Frontend: Old URL still in use. Bookmarks point to deprecated endpoint. Application code uses outdated API URL. Client hasn't updated to new endpoint.
  • Backend: Resource permanently moved. API endpoint permanently migrated. Domain permanently changed. URL structure permanently restructured.
  • Infrastructure: Domain permanently migrated. CDN permanently redirects. Load balancer permanently routes. Reverse proxy permanent redirect.

Solutions

  1. 1Step 1: Diagnose - Check DevTools Network tab Location header—308 responses show the new permanent URL. Review redirect chain. Check if redirect is actually permanent.
  2. 2Step 2: Diagnose - Server logs show redirect configuration. Review permanent redirect rules. Check if resource actually moved. Verify new URL is correct.
  3. 3Step 3: Fix - Client-side: Update all references to new URL permanently. Update bookmarks and saved URLs. Change API base URL in code. Update client configuration.
  4. 4Step 4: Fix - Server-side: Configure 308 redirect with correct Location header. Ensure new URL is accessible. Set proper cache headers. Update documentation.
  5. 5Step 5: Fix - Infrastructure: Update DNS if domain changed. Configure CDN permanent redirects. Update load balancer routing. Set reverse proxy redirects.

</>Code Examples

Fetch API: Handle 308 Permanent Redirects
1// Client-side: Handle 308 with method preservation and permanent update
2async function fetchWithPermanentRedirect(url, method = 'GET', body = null) {
3  let response = await fetch(url, {
4    method,
5    headers: body ? { 'Content-Type': 'application/json' } : {},
6    body: body ? JSON.stringify(body) : null,
7    redirect: 'manual', // Handle redirect manually
8  });
9  
10  if (response.status === 308) {
11    const newUrl = response.headers.get('Location');
12    console.log('Permanent redirect to:', newUrl);
13    
14    // Update stored URL permanently
15    updateApiBaseUrl(url, newUrl);
16    
17    // 308 preserves original method (POST stays POST, PUT stays PUT)
18    response = await fetch(newUrl, {
19      method, // Same method as original
20      headers: body ? { 'Content-Type': 'application/json' } : {},
21      body: body ? JSON.stringify(body) : null,
22    });
23  }
24  
25  return response.json();
26}
27
28// Update API base URL permanently
29function updateApiBaseUrl(oldUrl, newUrl) {
30  // Update in-memory config
31  API_BASE_URL = newUrl;
32  
33  // Update localStorage permanently
34  localStorage.setItem('api-base-url', newUrl);
35  
36  // Update all cached URLs
37  const cachedUrls = JSON.parse(localStorage.getItem('cached-urls') || '{}');
38  cachedUrls[oldUrl] = newUrl;
39  localStorage.setItem('cached-urls', JSON.stringify(cachedUrls));
40  
41  // Update any stored endpoints
42  const endpoints = JSON.parse(localStorage.getItem('endpoints') || '{}');
43  Object.keys(endpoints).forEach(key => {
44    if (endpoints[key] === oldUrl) {
45      endpoints[key] = newUrl;
46    }
47  });
48  localStorage.setItem('endpoints', JSON.stringify(endpoints));
49}
50
51// POST request with 308 redirect
52async function postWithPermanentRedirect(url, data) {
53  let response = await fetch(url, {
54    method: 'POST',
55    headers: { 'Content-Type': 'application/json' },
56    body: JSON.stringify(data),
57    redirect: 'manual',
58  });
59  
60  if (response.status === 308) {
61    const newUrl = response.headers.get('Location');
62    updateApiBaseUrl(url, newUrl);
63    
64    // POST method is preserved
65    response = await fetch(newUrl, {
66      method: 'POST', // Still POST, not GET
67      headers: { 'Content-Type': 'application/json' },
68      body: JSON.stringify(data),
69    });
70  }
71  
72  return response.json();
73}
74
75// Usage
76fetchWithPermanentRedirect('/api/data', 'POST', { key: 'value' });
Express.js: Configure 308 Permanent Redirects
1// Server-side: Return 308 for permanent redirects with method preservation
2const express = require('express');
3const app = express();
4
5// Permanent redirect with method preservation
6app.all('/api/v1/endpoint', (req, res) => {
7  // 308 preserves the HTTP method and is permanent
8  res.redirect(308, '/api/v2/endpoint');
9});
10
11// POST endpoint with permanent redirect
12app.post('/api/v1/submit', (req, res) => {
13  // Permanently redirect POST to new location
14  // Method will be preserved (POST stays POST)
15  res.redirect(308, '/api/v2/submit');
16});
17
18// API version migration
19app.all('/api/v1/*', (req, res) => {
20  const newPath = req.path.replace('/v1', '/v2');
21  res.redirect(308, newPath);
22});
23
24// Domain migration
25app.all('*', (req, res, next) => {
26  const host = req.headers.host;
27  
28  // Permanently redirect old domain to new domain
29  if (host === 'old-domain.com') {
30    return res.redirect(308, `https://new-domain.com${req.url}`);
31  }
32  
33  next();
34});
35
36// URL structure change
37app.all('/api/old-structure/:id', (req, res) => {
38  const newUrl = `/api/new-structure/${req.params.id}`;
39  res.redirect(308, newUrl);
40});
41
42// Permanent redirect with cache headers
43app.all('/api/legacy', (req, res) => {
44  res.status(308)
45    .set('Location', '/api/modern')
46    .set('Cache-Control', 'public, max-age=31536000') // Cache for 1 year
47    .end();
48});
Nginx: Configure 308 Permanent Redirects
1# Nginx: Configure 308 permanent redirects with method preservation
2server {
3    listen 80;
4    server_name old-domain.com;
5    
6    # Permanent redirect entire domain (preserves method)
7    return 308 https://new-domain.com$request_uri;
8}
9
10server {
11    listen 80;
12    server_name api.example.com;
13    
14    # Permanent redirect with method preservation
15    location /api/v1/endpoint {
16        return 308 /api/v2/endpoint$is_args$args;
17    }
18    
19    # POST endpoint permanent redirect
20    location = /api/v1/submit {
21        return 308 /api/v2/submit$is_args$args;
22    }
23    
24    # API version migration
25    location ~ ^/api/v1/(.+)$ {
26        return 308 /api/v2/$1$is_args$args;
27    }
28    
29    # URL structure change
30    location ~ ^/api/old-structure/(.+)$ {
31        return 308 /api/new-structure/$1$is_args$args;
32    }
33    
34    # Permanent redirect with cache headers
35    location /api/legacy {
36        return 308 /api/modern$is_args$args;
37        add_header Cache-Control "public, max-age=31536000" always;
38    }
39    
40    # Or use rewrite for permanent redirect
41    location /api/old {
42        rewrite ^/api/old(.*)$ /api/new$1 permanent;
43    }
44    
45    # Note: 308 preserves method, unlike 301 which may change POST to GET

Related Errors

Provider Information

This error code is specific to HTTP services. For more information, refer to the official HTTP documentation.

308 - Permanent Redirect | HTTP Error Reference | Error Code Reference