http

HTTP 工具

一份可直接干活的 HTTP 排查参考:状态码与重定向语义、Cache-Control 各指令的作用,以及当响应头、URL 或分享卡片出问题时该拿哪个工具。

9 工具

§01 领域指南

你实际在排查的是什么

一次 HTTP 交换是四个可以分开看的东西,而排查中被浪费掉的时间大多来自把它们混为一 谈:URL(你要的是什么)、请求头(你声称自己是谁、能接受什么)、 状态行(判定结果),以及响应头(关于缓存、安全和内容类型的契约)。正文是 其中最不重要的部分——等它出错的时候,响应头早就把原因讲完了。

第二点,浏览器给你看的几乎没有一样是源站的原始输出。在你的服务器和 Network 面板 之间,隔着一个 CDN、也许还有代理、HTTP/2 或 /3(它们会把字段名小写化并去掉原因 短语)、一个 service worker,以及磁盘缓存。一行 200 OK (from disk cache) 是一次 响应的记忆,不是一次响应——所以当某个头看起来不见了,先问问你读的是谁的副本。

第三点,HTTP 在很大程度上是自述的:User-AgentRefererAccept-Language 以及 每一个自定义 X- 头,都只是客户端选择发送的声明,一行代码就能伪造。只有源站输出 的东西——状态码和响应头——才在你的掌控之内。

状态码分类速查

分类含义客户端该做什么启发式可缓存
1xx中间态(100101103继续等
2xx成功使用正文(204 没有正文)200203204206
3xx重定向 / 重新验证跟随 Location;收到 304 就复用缓存300301308
4xx请求本身就是错的改正它;不要盲目重试404405410414
5xx服务器把一个合法请求办失败了退避;尊重 Retry-After501

有两个码经常被处理错:429 应该带上 Retry-After,而 304 Not Modified 是成功—— 把它记成错误会掩盖一个正常工作的缓存。

大家总选错的重定向矩阵

永久性方法与请求体是否保留用于
301永久否——可能被改成 GET重命名的 URL、主机归并
302临时否——同样有改写风险历史默认值;对 POST 请避免
303临时否——强制变成 GETPOST 之后重定向到结果页
307临时API 端点临时迁移
308永久POST 端点永久迁移

浏览器还会把 301 缓存得很死,有时长达整个配置文件的生命周期,所以一个发错的 301 撤销起来代价高昂——先在预发环境验证。

Cache-Control 逐条指令

指令管的是什么备注
max-age=N新鲜期秒数,对任何缓存生效0 强制重新验证
s-maxage=N仅对共享缓存的新鲜期在 CDN 侧覆盖 max-age
no-cache可以存;复用前必须重新验证不等于 no-store
no-store禁止存储该响应需登录才能看的 HTML
private浏览器可存,共享缓存不可存个性化响应
must-revalidate出错时不得继续提供过期副本max-age 搭配
immutable新鲜期内绝不重新验证带内容哈希的资源
stale-while-revalidate=N先给过期副本,后台再刷新平滑 CDN 未命中

Vary 也属于这一节:如果响应会随 Accept-Language 或某个自定义头而不同,而你从 未声明它,共享缓存就会把错误的变体交给下一位访客。

什么问题用哪个工具

手上有一个 URL、想知道源站自己怎么回答,就用 HTTP 头信息检查工具:请求从 api.sitekits.dev 发出,所以路径 上没有扩展、没有缓存、没有 service worker,你拿到的是最终状态、最终 URL、跳数以及 每一个响应头。它只读头,从不读正文,其 SSRF 防护会拒绝 localhost 和私有地址段。

需要发出某个具体请求时——带 JSON 请求体的 PATCH、一个 Authorization: Bearer 头、一个裸的 OPTIONS 用来读预检——用 REST API 测试器。请求从你的浏览器直连目标,所以目标的 CORS 策略对它生效的方式,跟对你自己前端代码生效的方式完全一样——当 bug 就是 CORS 时 这正是你要的。如果怀疑对象是 token,把它粘进 JWT 令牌解码器exp 和各项声明;解码不是验证。

当出问题的单位是整个页面加载而不是单个请求,就导出一份 HAR 放进 HAR 文件查看器;计时相关的内容在 HAR 实战指南 里。查询串上的谜团交给 URL 解析和分析工具,它经由浏览器的 WHATWG 解析器列出百分号解码 后的参数;某个值被多转义了一次时再加上 URL 编码 / 解码UserAgent 解析器 把一条 UA 字符串归约为浏览器、操作系统、设备类别和一个 bot 启发式判断;不认识的状态码去 HTTP 状态码参考;文件本该渲染却被下载,通常是 MIME 类型检查器 关于 Content-Type 的问题。当响应本身健康、 只是分享卡片不对时,把页面的 HTML 粘进 OG 预览——解析在本地完成, 所以预发构建和需要登录的页面都能用。

落坑清单

挂在 HTTP 响应上的 Strict-Transport-Security 毫无作用

浏览器会忽略经明文 HTTP 送达的 HSTS,在那里设置的 Secure cookie 也会被丢掉。把 升级重定向放在 80 端口,把安全响应头放在 HTTPS 响应上——并且两跳都要检查,不是只看 最后一跳。

安全响应头是按响应生效的,不是按站点

你 HTML 上一条严格的 Content-Security-Policy,对你的 API 返回的 JSON、CDN 的错误 页、或者别处的上传存储桶什么都不代表。CSP 生成器 负责写出这个 头;至于哪些响应会带上它,由你的部署配置决定。

重定向链要花掉往返

http://example.comhttps://example.comhttps://www.example.com//home/ 会在任何 HTML 之前多花三个往返,而且每一跳都可能丢掉一个方法、cookie 或 查询串。在边缘把它压成一跳。

正文里塞错误的 200 会击穿基于状态码的监控

一边回 200 一边在载荷里写 {"error": ...},会让按状态码分类触发的告警看不见故障, 并抬高被测出来的可用性。如果 SLO 归你负责,就连载荷一起断言——参见为 SRE 工作 汇集的那批工具。

+%20 不能互换

在按 application/x-www-form-urlencoded 编码的查询串里,+ 解码为空格;而在路径段 里它就是一个字面的加号。双重编码是它的兄弟 bug:%20 变成 %2520,而故障看起来 像路由问题,不像转义问题。

FAQ
为什么我的 POST 在重定向之后变成了 GET?
因为 301 和 302 允许客户端把方法改写成 GET 并丢掉请求体,而浏览器就是这么干的。当方法和请求体必须原样穿过这一跳时,临时移动用 307,永久移动用 308。一个表单或 webhook 莫名其妙丢掉载荷,通常就是撞上了一条被配成 301 的 http 转 https 或者尾斜杠重定向。
为什么 DevTools 里看到的响应头跟我的服务器配置不一样?
DevTools 显示的是经过全部中间跳之后的响应。CDN、反向代理或 service worker 都可能添加、改写或剥掉响应头,HTTP/2 和 /3 会把所有字段名小写化并去掉原因短语,而标着 200(from disk cache)的那一行根本不是一次新鲜的响应。在服务端取回这个 URL 就排除了这些变量,直接看到源站为链条末端那个 URL 实际输出了什么。
Cache-Control 的 no-cache 能阻止浏览器存储我的页面吗?
不能。no-cache 允许存储,但要求在复用已存副本之前先向源站重新验证,通常经由 ETag 和 If-None-Match。真正禁止把响应写入磁盘的指令是 no-store。需要登录才能看的 HTML 一般应该用 private, no-store,而带内容哈希的静态资源应该用很长的 max-age 再加 immutable。
我的 API 用 curl 好使,在浏览器里却报 CORS 错误,为什么?
CORS 是由浏览器执行的,不是服务器,而 curl 完全不理它。只要不是简单请求,浏览器会先发一个 OPTIONS 预检,并且除非源站返回匹配的 Access-Control-Allow-Origin(涉及 cookie 时还要有 Access-Control-Allow-Credentials),它就拒绝把响应交给你。服务器往往好好地回了 200;只是你读不到而已。先去看那个 OPTIONS 响应。
为什么我的 Strict-Transport-Security 头被忽略了?
因为浏览器只在 HSTS 经由 HTTPS 到达时才认它。放在明文 HTTP 响应上的策略会被直接丢弃——在那里设置的 Secure cookie 也一样——所以 80 端口的响应应该只带升级重定向,别的什么都不带,安全相关的响应头挂在 HTTPS 响应上。要逐跳检查而不是只看最后那个 200:重定向链末端存在某个头,完全不能说明第一跳发了什么。还要记住,includeSubDomains 和 preload 会把每一个子域按你发布的 max-age 绑定到 HTTPS 上,而从 preload 列表里移除要花好几个月。