Vary
用法
同一个 URL 往往会根据客户端发送的内容产生不同的响应。服务器对一个客户端使用 gzip 压缩、对另一个客户端使用 Brotli 压缩,就为同一资源提供了两种不同的表示形式。Vary 头部记录了是哪些请求头导致服务器选择了某种特定的表示形式,从而为缓存提供存储和匹配正确副本所需的信息。
当缓存收到一个已有存储响应的 URL 请求时,缓存会比较新请求与已存储请求之间所列的 Vary 头部。如果值匹配,则缓存的响应有效。如果不同,缓存会从源站获取新的响应。
最常见的值是 Accept-Encoding(用于压缩协商)和 Accept(用于内容协商)。基于 User-Agent 字符串向移动端和桌面端客户端提供不同 HTML 的动态服务配置会添加 Vary: User-Agent 以示意这种差异。
通配符值 * 意味着每个请求都被视为唯一的,实际上会对该资源禁用缓存。这很少是恰当的做法,大多数缓存会将 Vary: * 响应视为不可缓存。
注意
Google 不使用 Vary 头部来理解用于搜索索引的移动端与桌面端关系。该头部是一种缓存机制,而非搜索信号。使用带 Vary: User-Agent 的动态服务的站点仍能从正确的缓存行为中受益,但该头部本身对 Google 如何发现或索引移动内容没有影响。尽管采用移动优先索引,Google 也建议不要将 m-dot URL 作为规范版本,而推荐响应式设计作为首选配置。
Accept-Encoding
表示响应会随 Accept-Encoding 请求头而变化。不同的压缩算法(gzip、br、zstd)会为同一资源产生不同的响应正文。这是使用最广泛的 Vary 值。
Accept
表示响应会随 Accept 请求头而变化。内容协商会根据客户端偏好从同一 URL 提供不同的媒体类型(HTML、JSON、XML)。
Accept-Language
表示响应会随 Accept-Language 请求头而变化。从同一 URL 提供本地化内容的多语言站点会包含此值。
User-Agent
表示响应会随 User-Agent 请求头而变化。动态服务配置会从同一 URL 向移动端和桌面端客户端提供不同的 HTML。
Cookie
Vary: Cookie 意味着响应会根据 Cookie 头部的值而不同。缓存会为每个唯一的 Cookie 字符串存储一个单独的变体,由于每个已认证用户都有不同的会话 cookie,这会导致严重的缓存碎片化。请将 Vary: Cookie 限制在真正依赖 cookie 值的响应上(个性化页面、仪表盘),并避免在公开的静态资源上设置该值。
* (wildcard)
通配符值示意响应会随请求头中未捕获的因素而变化。缓存会将其视为不可缓存。很少有意使用。
示例
服务器使用客户端所请求的编码来压缩响应。Vary: Accept-Encoding 头部告知中间缓存分别为 gzip 和 Brotli 请求存储各自的副本,而不是向支持 Brotli 的客户端提供 gzip 压缩的响应。
Vary: Accept-Encoding
内容协商配置会从同一端点提供 JSON 或 HTML。缓存会根据 Accept 头部的值存储各自的响应。
Vary: Accept
影响响应的多个头部以逗号分隔的值列出。这里响应同时取决于所接受的编码和首选语言。
Vary: Accept-Encoding, Accept-Language
动态服务配置会向移动端和桌面端用户代理提供不同的 HTML。缓存会以完整的 User-Agent 字符串为键存储各自的版本。
Vary: User-Agent
将 Vary 与 Cache-Control 搭配可同时控制什么会变化以及每个变体保持新鲜的时长。
Cache-Control: public, max-age=3600
Vary: Accept-Encoding
疑难解答
由缺失或过于宽泛的 Vary 头部引起的缓存问题,常常表现为用户收到错误的内容。
CDN 提供了错误的缓存变体。服务器基于某个请求头返回不同的内容,却省略了匹配的 Vary 值。CDN 会缓存第一个响应,并将同一副本提供给所有客户端,无论其请求头如何。请将相关的头部名称添加到 Vary 列表中。可通过 curl 发送带有不同头部值的请求,并比较 X-Cache 或 Age 响应头进行测试:curl -H “Accept-Encoding: gzip” -v https://cdn.example.re/style.css curl -H “Accept-Encoding: br” -v https://cdn.example.re/style.css
Vary: User-Agent 导致的缓存碎片化。User-Agent 字符串有成千上万种唯一值。在 Vary 头部中列出 User-Agent 几乎会为每个访客创建一个单独的缓存条目,破坏缓存命中率。像 Cloudflare 和 Fastly 这样的 CDN 会将 User-Agent 归一化为设备类别。在可能的情况下,请使用诸如 Sec-CH-UA-Mobile 之类的归一化头部或 CDN 特定的设备类型头部,而非原始的 User-Agent。
Vary: * 完全禁用缓存。通配符告知缓存响应会随任何请求头之外的因素而变化。大多数缓存会将其视为不可缓存,绝不会提供已存储的副本。除非意图是对该资源禁用所有缓存,否则请移除 Vary: *。如果某个特定的请求头驱动差异,请显式指定该头部。
缺失 Vary: Origin 导致 CORS 缓存投毒。服务器基于 Origin 请求头返回动态的 Access-Control-Allow-Origin 值,却未包含 Vary: Origin。CDN 会以一个源缓存该响应,并将同一个 CORS 头部提供给不同的源,导致浏览器阻止该响应。请为每个反映动态源值的响应添加 Vary: Origin。要进行诊断,可从不同的源发送两个 curl 请求,并检查缓存的 Access-Control-Allow-Origin 是否变化:curl -H “Origin: https://a.example.re” -v https://api.example.re/data curl -H “Origin: https://b.example.re” -v https://api.example.re/data
gzip 与 Brotli 之间的 Vary: Accept-Encoding 不一致。有些源服务器以 gzip 压缩,但 CDN 边缘会重新压缩为 Brotli,导致缓存的编码与 Vary 键之间不匹配。请确保源站与 CDN 在编码行为上保持一致。在 nginx 中,设置 gzip_vary on; 可自动添加 Vary: Accept-Encoding。在 gzip 之外使用 Brotli 时,请确认两个模块都添加相同的 Vary 值,以便缓存为每种编码存储各自的条目。