用curl检查HTTPS接口,退出码是0,响应状态却是304,下载文件没有新内容。别重签证书,也别用空文件覆盖旧产物。回环实验分开TLS、HTTP与文件验收。浏览器缓存与curl下载不是一回事。
一、304没有正文,是协议行为
RFC9110第15.4.5节规定,304 Not Modified表示条件GET或HEAD原本会得到200,但条件求值阻止了传输。它指向已有表示;304在头部结束处终止,不能携带内容或尾部字段。没有正文不是下载中途断了。

图为条件GET检查关系,不是完整协议时序,也不是实测日志。配图由DM-Fox生成,经技术与可读性复核。
二、先留版本与三层证据
本次实测是Linux上的curl7.61.1、OpenSSL1.1.1k FIPS,临时CA与回环TLS1.2、HTTP/1.1服务。仅测试隔离环境,没有修改系统信任库,也不代表CDN、生产浏览器或HTTP/2的实际行为。先记录构建:
bash
curl --version
openssl version
| 证据 | 回答的问题 |
|---|---|
| curl退出码、TLS错误 | 这次传输有没有失败 |
| HTTP状态和响应头 | 服务器如何处理请求条件 |
| 本地文件、校验值与业务契约 | 能否采用这个结果 |
本例没有关闭证书验证。错误CA会让curl在HTTP请求前失败;信任与名称正确后,服务器仍可返回304、200或500。退出码不是HTTP状态,HTTP状态也不是业务完成证明。日志先脱敏。
三、找到是谁发出了条件请求
优先检查脚本里的If-None-Match、If-Modified-Since、-z,以及客户端配置。If-None-Match携带实体标签;ETag是服务器给表示的验证器,不保证等于内容哈希,也不能拿证书指纹替代。demo-v1仅为隔离样本:
bash
curl -q --noproxy '*' --http1.1 -sS --max-time 15 \
-H 'If-None-Match: "demo-v1"' \
-D conditional.headers -o conditional.body \
-w 'http=%{http_code} bytes=%{size_download}\n' \
https://example.com/data
rc=$?
printf 'curl_exit=%s\n' "$rc"
exit "$rc"
本次服务收到匹配的If-None-Match后返回304与同一ETag,curl退出0、下载字节0。-f也不会把304变成错误22,因为它不是400以上的HTTP错误。本轮304创建了零字节输出文件,不能当新正文。
-q放在第一个参数只用于跳过默认curlrc;它不会清除所有环境影响。--noproxy显式绕过代理,仅适合已授权可直连的检查环境。生产必须经代理时保留真实链路,不能为了通过检查绕过控制。
四、想取新正文,先去掉请求条件
同一URL,不发送条件头再做一次GET,能够区分"服务返回不变"与"资源确实有内容"。不要用-k换取成功。下面命令不手动发送条件字段:
bash
curl -q --noproxy '*' --http1.1 -sS --max-time 15 \
-D fresh.headers -o fresh.body \
-w 'http=%{http_code} bytes=%{size_download}\n' \
https://example.com/data
rc=$?
printf 'curl_exit=%s\n' "$rc"
exit "$rc"
隔离服务这时返回200、正文OK加换行,curl下载3字节。匹配标签为304,不匹配标签为200,错误CA为60且服务没有收到HTTP请求。这些是本机构建的观察,不是所有故障的固定退出码清单。
若无条件GET仍收到304,核对实际发出的头、代理改写和服务端实现;不要自动认定"本地缓存有效"。Cache-Control: no-cache表示希望缓存先重验证,不等于保证每次返回200,也不是删除所有请求条件的快捷键。
五、复用缓存必须绑定到同一份表示
只有持有与验证器对应的缓存正文,且满足请求目标、选择表示的字段以及缓存规则时,304才有复用基础。至少把URL、必要的Vary维度、验证器、正文和采样信息绑定保存;认证身份切换也不能随意复用另一个人的缓存。
较新curl的--etag-save与--etag-compare可保存和发送ETag,但它们不是完整缓存管理器;两者均在7.68.0加入。本次7.61.1不支持,正文因此采用显式头部对照,不声称实测了新选项。通用缓存还须遵循RFC9111。
六、下载脚本宁可拒绝,也别覆盖错文件
下面是Bash单请求示例,不复用缓存:只接受200和精确的OK加换行。保存为check.sh,在可写的独占工作目录执行bash check.sh;example.com/data是占位地址,实际使用须换成你获授权的接口,并替换内容契约。私有CA通过受控--cacert指定,不能信任对端随手发来的根。
bash
#!/usr/bin/env bash
set -eu
umask 077
work=$(mktemp -d ./download.XXXXXX) || exit 1
trap 'rm -rf -- "$work"' EXIT
if status=$(curl -q --noproxy '*' --http1.1 -sS -f \
--connect-timeout 5 --max-time 15 \
-o "$work/body" -w '%{http_code}' \
https://example.com/data); then
:
else
rc=$?
printf 'transfer_failed=%s\n' "$rc" >&2
exit "$rc"
fi
[ "$status" = 200 ] || { printf 'unexpected_http=%s\n' "$status" >&2; exit 1; }
printf 'OK\n' > "$work/expected"
cmp -s "$work/expected" "$work/body" || { printf 'content_failed\n' >&2; exit 1; }
mv -- "$work/body" ./result.txt
printf 'accepted\n'
临时目录建在目标同级,检查成功才移动文件;失败不动result.txt。这里只提供单写者演示,不承诺并发更新事务或断电持久性。二进制下载应比对经可信渠道取得的摘要或签名,不要把"文件非空"升级成完整性保证。
七、正反例决定脚本能否上线
| 本轮对照 | 应如何处理 |
|---|---|
| 匹配条件、304、无正文 | 诊断可退出0;单次下载脚本拒绝 |
| 200且内容正确 | 校验后替换目标 |
| 200但空体或内容错误 | 拒绝并保留旧目标 |
| 500或证书信任失败 | 保留非零退出,不用旧文件冒充成功 |
本文完整代码从最终HTML抽取后执行,条件命中、不命中、错误CA,以及包装脚本的200正确/错误内容、空正文、304和500均做了隔离对照。测试临时私钥与监听用完清理;未改真实站点。
- 保留curl构建、实际URL、脱敏请求条件和响应头。
- TLS验证保持开启,HTTP状态与退出码分别记录。
- 304复用前确认缓存正文和验证器归属;不明则停止。
- 只在状态及内容验收通过后更新产物,失败保留旧文件。
八、参考资料
参考实现不代替本地文件与业务验收。