侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 265 篇文章
  • 累计收到 7 条评论

If-None-Match 命中后,304 不该带响应正文

2026-11-11 / 0 评论 / 9 阅读

If-None-Match 命中后,304 不该带响应正文

浏览器带着缓存里的 ETag 再请求资源,服务器回 304 Not Modified,页面仍能正常显示。容易误会的是,304 看上去像一份没有内容的 200 响应;实际它告诉客户端复用已有表示,响应本身不应再传正文。

ETag 是表示的验证器。客户端收到带 ETag 的 200 后,可以在后续请求中把它放进 If-None-Match。如果当前表示仍匹配,服务端返回 304;客户端将本地缓存的正文与这次响应的元数据组合起来使用。若资源已经改变,服务端回 200 和新正文、新验证器。RFC 9110 第 15.4.5 节把 304 定义为条件 GET 或 HEAD 请求的响应,并明确说明服务端无需再次传输已有的表示。RFC 9110 §15.4.5

304 本身由响应头结束,不能带内容,也不能带 trailers。服务端仍要返回适用的缓存元数据,例如 ETag、Vary、Date;缓存要用它们更新已有条目。若只记住「304 没正文」,忘了这些头部的更新义务,客户端可能一直保留旧验证信息。

这套协商能减少重复传输,但它不会自动证明缓存一定新鲜。缓存策略还涉及 Cache-Control、Vary 和验证器生成方式。比如压缩前后内容不同,ETag 是否区分编码版本需要与缓存键设计一起考虑;单独一个 ETag 并不能替代所有缓存控制头。

304 与 204 不是一回事

204 No Content 表示请求成功处理,但没有表示内容返回。304 则用于条件请求的缓存验证,它告诉缓存复用已存的表示。两者响应都没有正文,语义却不同。把 304 改成 204,浏览器就失去了「本地那份仍有效」的明确判断依据。

自动化测试也要小心:某些客户端库会把无正文响应规范化,若只检查 response.text() 为空,就无法区分 204、304 或错误地返回空 200。状态码和关键头字段都应一起断言。反过来,如果测试客户端对 304 有特殊处理,最好再从服务端日志确认客户端实际发出的条件头。

比较器不能只看字符串长得像

ETag 可以是强验证器,也可以以 W/ 开头表示弱验证器。强比较要求字节级等价,弱比较关注语义等价;选择哪一种取决于缓存用途。若把版本号拼进字符串,却在内容变化时忘记更新版本,304 仍会让用户继续使用旧表示,错误不在 HTTP,而在验证器维护。

我会让 ETag 从内容或明确版本状态生成,并在测试里覆盖匹配与不匹配两条路径。命中时检查 304 和零字节正文;未命中时检查 200、新 ETag 和完整正文。只有状态码没有正文断言,或只有正文断言没有状态码,都不足以验证条件请求。

缓存中间层还会按请求头选择不同表示。服务器若根据 Accept-Encoding 返回压缩版本,响应需要配合 Vary: Accept-Encoding,否则代理缓存可能把一种编码的结果交给不兼容的请求。ETag 的强弱标记也要与生成规则一致,别因为字符串相同就假定两个表示逐字节相同。

客户端测试可分别发一次无条件 GET、一次带旧 ETag 的条件 GET,再用一个不存在的 ETag 触发完整响应。服务端也应记录实际收到的条件头,避免测试框架自动带缓存导致请求路径和预想不同。这样能区分「缓存命中」与「应用恰好返回空正文」,排查时少绕一圈。

    🤞 分享