← 中文资源

简体中文版本

HTTP — ETag 再验证返回陈旧内容或永不清缓存

调试 ETag 与 If-None-Match 行为:返回已变内容的 304、永不存储的缓存、以及弱/强验证器不匹配。

阅读英文原文

症状

  两个失败方向共用一件仪器。方向一——陈旧的 304:内容变了、部署发生了,ETag 却纹丝不动,因为它在进程启动时计算、与响应体无关,If-None-Match 命中旧验证器,客户端拿到“未修改”的裁决继续用旧内容。方向二——永不清缓存:ETag 嵌入请求时间或随机盐,每次都是新值,If-None-Match 永不匹配,每次都 200。两者都是派生 bug,两跳 curl 即可暴露:内容未变必须 304、内容已变必须 200 加新 ETag。第三个变体是缺 Vary——一个存储的变体满足所有请求,语言或凭证维度的差异被同一个 ETag 抹平。

诊断思路

  先用 If-None-Match 直接测验证器两个方向;再查 ETag 的派生源——mtime、启动时常量、随机盐都是错误来源,正确的是响应体或其内容依赖的哈希;最后检查可缓存性阻断项:Set-Cookie 出现在共享响应上、缺 Vary、意外的 no-store,都会让最好的验证器失效。

根因

  条件式 304 的正确性完全取决于验证器。从不变量派生的 ETag 在内容变化时不变,服务陈旧 304;不稳定的验证器永不匹配,缓存形同虚设;缺 Vary 让一个存储变体去满足正确响应本会不同的请求。

修复与验证

  验证器从响应体的真实内容派生:体哈希加所有 Vary 维度(如 Accept-Language)参与计算,配强验证器(不带 W/)与 must-revalidate;静态资源交给构建——内容哈希文件名(app.a91f.js)让验证器问题变得平凡,immutable 永久缓存;清掉阻断项:中间件往共享响应注入 Set-Cookie 会把它变成不可缓存,no-store 会覆盖一切。验证时未变内容返回 304 且验证器相同、已变内容返回 200 且带新验证器、两个语言变体在正确的 Vary 下各自独立缓存。弱验证器允许语义相等但会破坏字节范围与严格中间件——按字节变化的内容用强验证器。

代码示例

$ curl -sI https://app.example.com/data
ETag: "v1-boot-8a01"        <- computed at process start

# content changes, deploy happens:
$ curl -sI https://app.example.com/data
ETag: "v1-boot-8a01"        <- unchanged: the validator ignores the body
const hash = crypto.createHash('sha1')
  .update(JSON.stringify(data))
  .update(acceptLanguage)     // every Vary dimension participates
  .digest('base64');

res.set({
  'ETag': `"${hash}"`,        // strong validator
  'Vary': 'Accept-Language',  // the changing dimension
  'Cache-Control': 'public, max-age=0, must-revalidate',
});