301 Redirect Loop (ERR_TOO_MANY_REDIRECTS)
症状
- Browser shows `ERR_TOO_MANY_REDIRECTS` and the page never loads
- `curl -L` loops indefinitely or outputs the same Location header repeatedly
- Redirect chain: `http://example.com → https://example.com → http://example.com → ...`
- Problem often appears immediately after enabling HTTPS, adding a CDN, or changing Nginx config
- Different browsers may cache old redirect responses, making the issue seem intermittent
- `curl -L` loops indefinitely or outputs the same Location header repeatedly
- Redirect chain: `http://example.com → https://example.com → http://example.com → ...`
- Problem often appears immediately after enabling HTTPS, adding a CDN, or changing Nginx config
- Different browsers may cache old redirect responses, making the issue seem intermittent
根本原因
- Cloudflare SSL mode set to 'Flexible' while Nginx also redirects HTTP→HTTPS, causing Cloudflare to always talk HTTP to origin and be redirected back
- Django's `SECURE_SSL_REDIRECT = True` running behind a load balancer that strips the X-Forwarded-Proto header, causing every request to appear as HTTP
- Nginx configuration redirecting non-www to www and www to non-www simultaneously
- HSTS (Strict-Transport-Security) cached in the browser forcing HTTPS while the server redirects HTTPS back to HTTP
- Multiple redirect rules in .htaccess or server config that conflict with each other
诊断
1. **Trace the full redirect chain** without following redirects:
```bash
curl -sI --max-redirs 0 http://example.com
curl -sI --max-redirs 0 https://example.com
# Compare Location headers — do they point back to each other?
```
2. **Bypass the CDN** by hitting the origin IP directly to isolate where the loop starts:
```bash
curl -sI --resolve example.com:80:YOUR_ORIGIN_IP http://example.com/
curl -sI --resolve example.com:443:YOUR_ORIGIN_IP https://example.com/
```
3. **Check Cloudflare SSL mode** — if set to Flexible and your origin redirects HTTP→HTTPS:
Log in to Cloudflare → SSL/TLS → Overview → change from Flexible to Full (strict)
4. **Verify Django's X-Forwarded-Proto trust** in settings:
```python
# settings/production.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
```
Then confirm your proxy actually sets the header:
```bash
curl -sI -H 'X-Forwarded-Proto: https' http://localhost:8000/
# Should NOT redirect — if it does, the setting is missing
```
5. **Clear browser HSTS cache** — in Chrome: `chrome://net-internals/#hsts` → Delete domain. In Firefox: clear history including cached content.
```bash
curl -sI --max-redirs 0 http://example.com
curl -sI --max-redirs 0 https://example.com
# Compare Location headers — do they point back to each other?
```
2. **Bypass the CDN** by hitting the origin IP directly to isolate where the loop starts:
```bash
curl -sI --resolve example.com:80:YOUR_ORIGIN_IP http://example.com/
curl -sI --resolve example.com:443:YOUR_ORIGIN_IP https://example.com/
```
3. **Check Cloudflare SSL mode** — if set to Flexible and your origin redirects HTTP→HTTPS:
Log in to Cloudflare → SSL/TLS → Overview → change from Flexible to Full (strict)
4. **Verify Django's X-Forwarded-Proto trust** in settings:
```python
# settings/production.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
```
Then confirm your proxy actually sets the header:
```bash
curl -sI -H 'X-Forwarded-Proto: https' http://localhost:8000/
# Should NOT redirect — if it does, the setting is missing
```
5. **Clear browser HSTS cache** — in Chrome: `chrome://net-internals/#hsts` → Delete domain. In Firefox: clear history including cached content.
解决
**Fix 1: Cloudflare + origin HTTPS redirect loop** — change Cloudflare to Full (strict):
```
Cloudflare Dashboard → SSL/TLS → Overview → Full (strict)
```
Or disable HTTP→HTTPS redirect on the origin and let Cloudflare handle it with an Edge Certificate + Always Use HTTPS rule.
**Fix 2: Django behind a reverse proxy** — add the proxy header setting:
```python
# config/settings/production.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
```
```nginx
# Nginx: forward the scheme to Django
proxy_set_header X-Forwarded-Proto $scheme;
```
**Fix 3: Conflicting Nginx server blocks** — keep redirect logic in one place:
```nginx
# Correct: one block redirects HTTP → HTTPS
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
# One HTTPS block handles www → non-www
server {
listen 443 ssl;
server_name www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
# actual app config
}
```
```
Cloudflare Dashboard → SSL/TLS → Overview → Full (strict)
```
Or disable HTTP→HTTPS redirect on the origin and let Cloudflare handle it with an Edge Certificate + Always Use HTTPS rule.
**Fix 2: Django behind a reverse proxy** — add the proxy header setting:
```python
# config/settings/production.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
```
```nginx
# Nginx: forward the scheme to Django
proxy_set_header X-Forwarded-Proto $scheme;
```
**Fix 3: Conflicting Nginx server blocks** — keep redirect logic in one place:
```nginx
# Correct: one block redirects HTTP → HTTPS
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
# One HTTPS block handles www → non-www
server {
listen 443 ssl;
server_name www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
# actual app config
}
```
预防
- **Use Cloudflare Full (strict) SSL mode** whenever your origin serves HTTPS — Flexible mode is the single most common cause of redirect loops with CDNs
- **Always set `SECURE_PROXY_SSL_HEADER`** in Django before enabling `SECURE_SSL_REDIRECT` if the app runs behind Nginx, a load balancer, or any reverse proxy
- **Test redirects with curl** (`curl -sIL`) immediately after any Nginx, CDN, or SSL config change, before going live
- **Avoid HSTS preload** until you are confident your redirect chain is stable — a bad HSTS preload can lock users out for months
- **Always set `SECURE_PROXY_SSL_HEADER`** in Django before enabling `SECURE_SSL_REDIRECT` if the app runs behind Nginx, a load balancer, or any reverse proxy
- **Test redirects with curl** (`curl -sIL`) immediately after any Nginx, CDN, or SSL config change, before going live
- **Avoid HSTS preload** until you are confident your redirect chain is stable — a bad HSTS preload can lock users out for months