访问 HTTPS 下载入口,curl 加上 -L 后反而报"curl: (47) Maximum (2) redirects followed"。这时先别换证书,更别加 -k。47 指向重定向次数上限:可能是入口来回兜圈,也可能只是正常链条超过了你设置的预算。下面用隔离实验,把跳转、TLS 验证和最终文件验收拆开。
一、先分清 302、47 和证书错误
HTTP 302 是响应状态;curl 47 是客户端退出码,两者不是同一套编号。单次请求收到 302,curl 默认不跟随,传输可以正常结束且退出 0。启用 -L 后才沿 Location 继续;达到 --max-redirs 允许的次数,再需要跳转时会失败。
退出 0 不代表拿到文件,47 也不证明循环。三次正常跳转遇到上限 2 同样失败:问路超预算,不等于被指回原地。
| 现象 | 先查什么 | 不能据此断言 |
|---|---|---|
| 302,curl 退出 0 | 是否启用 -L,Location 是否合理 | 正文是目标文件 |
| curl 退出 47 | 跳转链、次数、重复 URL | 一定循环或证书过期 |
| curl 退出 60 | 失败目标的证书、信任与名称 | 增加跳转次数可以修好 |
二、固定环境,先看第一跳
本次回环实验使用 curl 7.61.1、OpenSSL 1.1.1k FIPS,限定 TLS 1.2、HTTP/1.1;临时证书私钥用完销毁,未改生产配置。先记录版本,不照搬新版默认值。
bash
curl -V
openssl version
下面保留默认"不跟随",保存第一跳响应头和正文。example.com 是文档占位,并不保证提供这些路径;需要替换成有权限访问的真实入口。实验执行器则通过回环映射和临时授权 CA 重放同样命令。
bash
curl -q --noproxy '*' --http1.1 \
--connect-timeout 3 --max-time 10 \
-sS -D first.headers -o first.body \
-w 'http=%{http_code} redirects=%{num_redirects}\n' \
https://example.com/start
-q 必须作为 curl 的首个参数,它跳过默认配置文件,但不会清空环境变量。这里用 --noproxy '*' 明确直连;企业网络若必须走代理,应改成自己的授权配置,不能照搬绕过。命令未关闭证书验证,生产私有 CA 仅添加经管理员确认的 --cacert 文件。
三、跟随跳转,同时限制协议和次数
先设小预算诊断,不要一开始就无限跟随。两项协议限制分别约束初始 URL 和重定向目标;仅限制第一跳,不能代替检查 Location。实验入口 /start 经 /middle 到 /final,共两次跳转,预算 2 恰好成功。
bash
curl -q --noproxy '*' --http1.1 \
--proto '=https' --proto-redir '=https' \
-L --max-redirs 2 --connect-timeout 3 --max-time 10 \
-sS -D chain.headers -o candidate.body \
-w 'http=%{http_code} redirects=%{num_redirects}\n' \
https://example.com/start
-D 保存跳转链上的多组响应头,不能只看最后一组。-w 打印最终观察到的 HTTP 码与已跟随次数;出错时 HTTP 码可能只是最近一跳,不代表最终文件成功。candidate.body 只是候选输出,绝不能直接覆盖正式产物。

图为独立概念卡,非时序;次数须给数值,如上例 2。HTTPS 验证保持开启。DM-Fox 辅助生成,已复核。
四、怎样确认是循环,而不只是链太长
按顺序整理"当前 URL → Location"。相对路径基于当前 URL 解析,再比较协议、主机、端口、路径和查询参数。重复终点或反复转换规则才是循环证据,两次 302 还不足以定性。
常见原因是应用误读代理信息生成另一种协议地址,或两层规则反复增删末尾斜杠。修复实际生成 Location 的那层;转发头只接受可信代理,不能信任任意客户端头。
无 Cookie 也可能反复回登录页,先确认认证契约。公开 Location、Cookie、Authorization 和签名参数前须脱敏,原始证据按权限保管。
五、回环正反例说明了什么
实验记录请求路径、退出码和响应头,分别测试长链、循环、降级及错误 CA;不凭一行 verify 0 放行。
| 隔离样本 | 实际结果 | 边界 |
|---|---|---|
| 两跳,预算 2 | 退出 0,最终 200,正文 OK | 仍需业务验收 |
| 三跳,预算 2;或自循环 | 均退出 47 | 同码不能区分二者 |
| 预算 0,入口要跳转 | 退出 47 | 0 不是无限制 |
| 跳向 HTTP,限定 HTTPS | 本机构建退出 1 | 不要移除协议限制当修复 |
| 目标 HTTPS 不受信任 | 退出 60 | 不是跳转次数问题 |
这是回环实测,未覆盖浏览器、全部 TLS 后端或生产单点登录。跨主机须重审授权,不盲加 --location-trusted 转发凭据。
六、成功文件最后再替换
下面只接受最终 200 和精确字节 OK 加换行,适合演示"失败保留旧文件"。真实下载须换成独立可信来源的摘要、签名或业务解析规则,不是机械寻找 OK 字样。临时文件与目标在同一目录,验证完成后再 mv。
bash
set -eu
url='https://example.com/start'
tmp=$(mktemp './download.XXXXXX')
trap 'rm -f "$tmp"' EXIT
if code=$(curl -q --noproxy '*' --http1.1 \
--proto '=https' --proto-redir '=https' \
-L --max-redirs 2 --connect-timeout 3 --max-time 10 \
-sS -f -o "$tmp" -w '%{http_code}' "$url"); then
[ "$code" = 200 ] || exit 1
python3 - "$tmp" <<'PY'
import sys
from pathlib import Path
if Path(sys.argv[1]).read_bytes() != b'OK\n':
raise SystemExit(1)
PY
mv -- "$tmp" result.txt
else
rc=$?
exit "$rc"
fi
我重放了正常链、循环、错误正文、空正文、HTTP 500 和错误 CA;成功时替换结果,其他情况保持旧文件,并清除临时下载。-f 负责部分 HTTP 错误,不负责证明 200 的内容正确;跳转失败也可能已经写入部分数据,所以必须检查 curl 退出码。
七、哪些做法不算修复
单纯把次数改大只能处理经确认合理的长链,不能修循环。--max-redirs -1 表示不限制次数,诊断时不建议使用;--max-time 限制总时间,但时间预算不替代次数预算。也不要加 -k,让真正的目标证书问题藏到下一层。
HEAD 与 GET 可能走不同规则,不能代替下载验收;调试输出须脱敏。修复后原入口重测,直达最终地址不证明入口恢复。
八、收尾验收清单与参考
- 记录 curl/TLS 后端版本,明确代理、CA、入口和协议预算。
- 保存并脱敏每一跳 Location,区分长链与重复循环。
- 每个 HTTPS 目标保持证书验证,禁止无条件转发凭据。
- 确认最终状态、内容、退出码;失败不覆盖旧文件。
- 修复后从原入口重测,回收实验监听、临时证书和私钥。
参考资料: