# HTTP 301 Moved Permanently

**Code:** `301` · **Category:** HTTP · **Seen on:** Nginx, Apache, Cloudflare, WordPress

The resource has a new permanent URL; clients and search engines should use the address in the Location header from now on.

## Meaning

A 301 is not an error by itself — it is the correct way to move a page. Problems start when redirects loop, chain through several hops, point to the wrong place, or turn a POST into a GET.

Search engines transfer ranking signals to the new URL, and browsers cache 301s aggressively, so a wrong 301 can stick around in users’ browsers even after you fix the server.

## Common causes

- Site moved to HTTPS or to www / non-www
- Page slug or URL structure changed
- Redirect loop: http→https and https→http rules fighting (common behind proxies/CDNs)
- Trailing-slash redirects added by the framework or web server
- Old cached 301 in the browser after the rule was removed
- POST requests become GET after a 301 (use 308 to keep the method)

## Quick fix

1. Inspect the chain: `curl -sIL https://example.com/page`
2. Redirect straight to the final URL — one hop, not several
3. Behind Cloudflare/a load balancer, base HTTPS redirects on `X-Forwarded-Proto` to avoid loops
4. Use 308 instead of 301 for API endpoints that receive POST/PUT
5. Clear browser cache or test in a private window after changing redirects

## Detailed fix by platform

### Nginx

1. Force HTTPS + non-www in one hop:

```nginx
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}
server {
    listen 443 ssl;
    server_name www.example.com;
    return 301 https://example.com$request_uri;
}
```


### Apache

1. .htaccess, proxy-aware HTTPS redirect:

```apache
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
```


### Cloudflare

1. "Too many redirects" with SSL mode Flexible + an origin HTTPS redirect is a loop — set SSL/TLS mode to Full (strict).

### WordPress

1. Check `siteurl` and `home` in Settings → General (or wp_options) match the canonical https URL.

## Code examples

### Trace a redirect chain

```bash
curl -sIL http://example.com/blog | grep -iE '^(HTTP|location)'
# HTTP/1.1 301 → https://example.com/blog
# HTTP/2 301   → https://example.com/blog/
# HTTP/2 200   ← two hops: make the first redirect point straight to /blog/
```

## How to diagnose

1. **Chain** — curl -sIL — how many hops, and where do they go?
2. **Loop** — Does any URL appear twice? (ERR_TOO_MANY_REDIRECTS)
3. **Layer** — Which layer issues each redirect: CDN, web server, app?
4. **Method** — Is a POST being redirected? Use 307/308
5. **Cache** — Retest in a private window — 301s are cached

---
Source: [WhatIsMyError](https://whatismyerror.com/errors/http/301-moved-permanently)
