访问网站时浏览器突然报 ERR_TOO_MANY_REDIRECTS,页面直接打不开——这是典型的 Nginx 重定向循环。根因几乎都是「强制跳 HTTPS」与「反向代理协议头」配合不当,导致请求在 HTTP 与 HTTPS 之间无限互跳。它常出现在「CDN/负载均衡 + 反向代理 + 后端程序」的多层架构里,任何一层多做了一次跳转都会引爆。本文用 curl 把重定向链一层层扒出来,并给出可复制的根治配置,让你下次遇到 Too many redirects 不再靠猜。
一、重定向循环到底是什么
浏览器对单次请求的重定向次数有上限(Chrome 约 20 次)。当服务器回的 301/302 指向的地址,又把自己跳回来,就形成「A→B→A→B…」的死循环。超过上限后浏览器中断并报 ERR_TOO_MANY_REDIRECTS(中文常显示「该网页有重定向循环」)。它和 404、502 不同:服务其实活着,只是跳转规则自相矛盾。
二、五个最常见的成因
2.1 强制跳 HTTPS,但判断条件永远为假
最常见的一种:前面挂了 CDN/SLB 终止 TLS,请求到 Nginx 时 $scheme 永远是 http。你用 if ($scheme != https) 强制跳 HTTPS,于是每次都被跳;浏览器到了 HTTPS 又被 CDN 转回 HTTP,Nginx 又跳——死循环。本质是「判断真实协议」的头没传下去。
2.2 反向代理没传 X-Forwarded-Proto
反代后端的程序(如 WordPress、Spring Boot)看到的是 http://127.0.0.1,于是它生成的 canonical/登录回调是 http 地址,前端再跳回 https,又开始循环。后端根本不知道用户实际用的是 https,这是反向代理最常踩的坑。正确做法是在 Nginx 把真实协议透传给后端。
2.3 多层代理各自加一次 301
CDN 一层、Nginx 一层、应用框架一层,每层都「贴心」地补一次 redirect,叠加起来就超额。正确原则是:只在最外层(面向公网那一层)做一次跳转,内层一律透传、不再二次跳转。
2.4 WordPress 站点地址与访问协议不一致
当 WP_HOME/WP_SITEURL 配成 http://,而用户用 https:// 访问时,WordPress 会不断做 canonical 重定向把自己跳回 http,再被外层跳回 https。站点地址必须与访问协议严格一致。
2.5 浏览器缓存了错误的 301
301 是「永久重定向」,浏览器会缓存很久。有时服务端已修好,你本地仍报循环——这是旧缓存在捣鬼。排错时务必用无痕窗口或 curl,避免被本地缓存误导。
三、用 curl 把重定向链扒出来
别靠浏览器猜,用命令行最直观。下面三条命令能让你看清每一跳的 Location 到底在 http 与 https 之间怎么横跳:
# 看单次响应的 Location
curl -sI https://example.com
# 自动跟随并打印最终生效地址与下一跳
curl -s -o /dev/null -w "effective=%{url_effective} next=%{redirect_url}\n" https://example.com
# 逐跳追踪,最多 30 次,打印每一跳的状态与 Location
curl -sIL --max-redirs 30 https://example.com | grep -iE "^HTTP|^location|^location:"
如果输出里 Location 在 http:// 与 https:// 之间反复出现,基本可以断定是协议头(X-Forwarded-Proto)没传对,或有多层重复跳转。配合 Nginx 反向代理完整配置:负载均衡 + HTTPS + 缓存优化 一文里的反代骨架对照看,能更快定位是哪一层在重复跳。
四、根治配置:可复制
核心就两条:外层只跳一次 HTTPS;反代把真实协议透传给后端,且后端不再二次跳转。下面是一个干净的反代示例:
server {
listen 80;
server_name example.com;
# 只在外层(面向公网)做一次跳转,内层不要再判断 scheme
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
# 关键:把真实协议告诉后端,避免后端误以为自己是 http
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 注意:这里不要再写 if ($scheme != https) 跳转,否则又循环
}
}
若后端是 WordPress,需要在 wp-config.php 里让它信任并识别这个协议头,否则 WP 仍会生成 http 的链接:
// wp-config.php:识别反代传来的真实协议
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
// 站点地址必须与访问协议严格一致(都用 https)
define('WP_HOME', 'https://example.com');
define('WP_SITEURL', 'https://example.com');
安全提醒:X-Forwarded-Proto 只能由可信代理设置。若 Nginx 直接暴露在公网,要在 set_real_ip_from 里限定可信来源,否则外部可伪造该头。证书本身建议用 Let’s Encrypt 免费 SSL 证书自动续期 的 certbot/宝塔方案,避免证书过期又引发新一轮跳转与混合内容告警。
五、上线前自查清单
| 检查项 | 命令 / 位置 | 期望结果 |
| 外层 301 只存在一处 | grep “return 301” 配置 | 仅公网层一次 |
| 后端收到的协议 | 打印 $_SERVER[‘HTTPS’] | 应为 on |
| 无重复 Location | curl -sIL –max-redirs 30 | 只跟一次即 200 |
| CDN/SLB 不额外跳 | 控制台重定向规则 | 关闭多余 redirect |
| 站点地址一致 | WP 设置 / 配置文件 | 统一 https |
六、预防:在 CI 里加一条重定向自检
把检查写进上线流水线,比事后救火省心得多。下面这段脚本可作为 K8s Ingress NGINX 实战 或任意 CI 的一环:发布后探测一次,若仍存在多余重定向就直接失败拦截。
#!/usr/bin/env bash
url="https://example.com"
loc=$(curl -s -o /dev/null -w "%{redirect_url}" "$url")
if [ -n "$loc" ] && [ "$loc" != "$url" ]; then
echo "WARN: 仍存在重定向 -> $loc"
exit 1
fi
echo "OK: 无重定向循环"
七、真实案例:一次线上循环的定位全过程
某次上线后,运维群里陆续有人反馈后台打不开,报错正是 ERR_TOO_MANY_REDIRECTS,但用手机流量访问却正常。按前文思路,先用无痕窗口排除本地 301 缓存,再 curl -sIL --max-redirs 30:第一跳是 SLB 回的 301 到 HTTPS,第二跳 Nginx 又 301 到 HTTPS(重复),第三跳到了 WordPress,WP 却回 302 跳回 HTTP 的 wp-admin——于是又回到 SLB 的 HTTPS 跳转,形成闭环。
定位结论很清晰:SLB 已经做了 HTTPS 终止并加了一次跳转,Nginx 的 if ($scheme != https) 又加了一次(多余),而 WordPress 的 WP_SITEURL 还是 http,于是它往回跳。三处各加一次,远超浏览器上限。修复只做三件事:删掉 Nginx 里多余的 scheme 判断、反代补上 proxy_set_header X-Forwarded-Proto $scheme、把 WP 站点地址改成 https。改完再次用上面的 CI 自检脚本跑一遍,输出 OK: 无重定向循环,群里告警随即消失。这个案例也说明:多层架构里「跳转职责」必须收敛到唯一一层。
八、小结
Nginx 重定向循环(ERR_TOO_MANY_REDIRECTS)九成以上是「协议头没透传 + 多层重复跳转」导致的。记住三句话:只在外层跳一次 HTTPS、反代必须传 X-Forwarded-Proto、站点地址与访问协议一致。排错时先 curl -sIL 看 Location 链,再用上面的清单逐项核对,基本都能在十分钟内根治。如果还伴随证书相关告警,可顺带参考 HTTPS 混合内容与证书链故障排查实战 一起处理。




