响应头 Vary 被称为 HTTP 最丑未改进部分,有文称其互操作极差的糟糕机制,工程师多退避。但丑陋不无用。同一网址可有多个正确响应,如服务器按浏览器发不同图格式。缓存忽略 Vary 易返错字节,若认每头值不同则少数请求变上千难复用项。Vary 只告缓存哪些字段可能影响响应,不指明差异何者重要。现缓存规则(Cache Rules)全套餐支持 Vary,你定克拉夫拉(Cloudflare)如何处理各头。
正文
响应头 Vary 曾被称为“HTTP 中尚未改进的最丑陋部分”,也被形容为“可怕、拼凑的机制”,中间缓存之间的互操作性“相当糟糕”。这听起来不像推荐,但丑不代表没用。
一个 URL 可以有不止一个正确响应。服务器可能向不同浏览器返回不同图片格式。缓存若忽略 Vary,就可能把错误字节发给请求;若把每个原始头值都当成不同,少量相似请求又会膨胀成几千个几乎无法复用的缓存条目。Vary 告诉缓存哪些请求字段可能影响响应,却不告诉它哪些差异真正重要。
Vary 支持现已在所有计划的 Cache Rules 中可用。源站仍然指定可能影响响应的请求头,但由你决定 Cloudflare 如何处理每个头:规范化已知协商头、在精确值重要时透传,或在变化太不可预测时绕过缓存。源站声明什么可能变化,你决定多少变化对缓存才有意义。
Vary 如何工作
Vary 是标准 HTTP 响应头,告诉 Cloudflare 这样的中间缓存:哪些请求字段可能影响源站返回的响应。站点用它从同一 URL 提供不同语言、图片格式、压缩方式或区域内容。
例如同一 URL 有两种有效表示。浏览器请求 GET /catalog,Accept: text/html。源站返回 HTML,并声明 Vary: Accept。API 客户端用 Accept: application/json 请求同一 URL,正确响应是 JSON。Vary: Accept 告诉缓存,只看 URL 不够,还要看请求的 Accept 值。
没有 Vary,先进入缓存的那个响应可能被发给双方。HTML 先到,API 客户端拿到标记文本,JSON 解析失败;JSON 先到,浏览器拿到 API 响应。
Vary 防止缓存发错响应,但带来更难的问题:两个请求头值不同时,它们是否真的需要不同响应?
当正确的缓存变得无用
Vary 能告诉缓存哪些请求字段可能影响响应,却不能告诉缓存响应代表什么。比如源站只提供英语、法语、德语内容。一个客户端发送 Accept-Language: en-US, fr;q=0.8,另一个发送 fr;q=0.8, en-GB。两个请求都偏好英语,源站可能返回完全相同的英语响应。但缓存比较原始值,不能安全假设它们等价:顺序和语言标签不同,而源站并不区分。于是缓存可能把它们存成不同变体,即使响应体字节相同。
这是 Vary 的核心问题。应用往往从海量请求值中产生一小撮有限表示,源站知道几千种语言偏好会收敛到三种支持语言,缓存通常不知道。多个字段同时变化时更糟:一个字段 10 个值产生 10 个变体,三个字段各 10 个值可产生 1000 种组合。真实请求头基数更大:User-Agent 值很多,Cookie 可能每个访客唯一,偏好头在顺序、格式(空格和制表符都算)、质量值上都能不同。
结果就是缓存可能完全正确,却几乎永远冷:相同响应散落在流量太少、无法保持热的条目里,占用容量、互相驱逐、拉低命中率、增加回源。驱逐能删掉冷条目,却不能因为响应相同就合并它们。
对近 5 万个热门站点、超过 1.2 亿个响应的分析发现,近 3000 个站点在四个或更多字段上变化,有些在 10 个、23 个甚至 47 个字段上变化。需要让客户在合适时使用 Vary,但又不至于造出无用的缓存。
有些高基数变化是有意的。CDN 或反向代理可能注入地理区域等值,以可预测地划分内容。这只有在可能值受控、各组件对其含义一致时才行。否则缓存会碎成可能永远不再复用的变体。支持 Vary 的设计难题就在这里:保留足够变化来返回正确响应,又不让请求之间的偶然差异毁掉缓存效率。
Cache Rules 如何控制 Vary
Cloudflare 客户已有几种处理类似 Vary 的协商内容的方法:绕过缓存交给源站,在自定义缓存键或其他规则里复现源站协商逻辑,用 Worker,或使用 Vary for images 等功能。这些仍然有用,但要么放弃缓存,要么复制应用逻辑,要么要写额外代码,要么只覆盖较窄场景。Cache Rules 中的 Vary 填补中间空白,把支持拆成两个决定:
- 源站用
Vary标识可能影响响应的请求头。 - Cache Rule 决定 Cloudflare 如何处理每个头的值。
Cache Rule 不会强制每个响应都 vary。如果源站不返回 Vary,Cloudflare 照常缓存响应,不过规则仍可能在转发前重写 Accept 和 Accept-Language。当源站返回 Vary,Cloudflare 对其中每个头使用配置的动作;没有单独设置的头使用规则默认动作。三种动作是:
- normalize(规范化):选择缓存变体前规范化请求头,让等价请求共享缓存响应。对
Accept、Accept-Language、Accept-Encoding应用特定规则;对其他头,修剪可选空白,按原顺序合并重复头行,保留大小写和内部空白。适合许多请求值映射到少量响应的协商头,是推荐起点。 - passthrough(透传):用请求头的原始字节做缓存匹配,保留大小写、空白、顺序和重复值。如果头出现在多行,Cloudflare 按顺序用逗号合并这些行来做缓存匹配;出站头行不变。禁用 Respect Strong ETags 时仍可重写
Accept-Encoding。适合值受控、精确值会改变响应的头。 - bypass(绕过):当源站在
Vary中命名该头时,不存储响应。现有缓存条目不会被删除,要清除就 purge。适合个性化、高基数或意外头,如Cookie、User-Agent。
推荐默认用 normalize。对带个人或无界值的个别头用 bypass。精确值改变响应时用 passthrough。
例如透传会保留大小写、空白、顺序和重复值的区别,即使源站把它们视为等价。Vary: X-View 加透传时,X-View: compact,full、X-View: Compact,full、X-View: compact, full 会产生三个不同缓存键。足够多的偶然变化会把一个可复用响应变成许多一次性变体。
无论配置什么动作,Vary: * 总是绕过缓存。它表示请求的任何方面,甚至 HTTP 消息之外的信息(如客户端 IP),都可能影响源站选择哪个响应。因此 Cloudflare 不联系源站就无法复用该响应。
响应如何经过缓存
跟随前面的 /catalog 请求。第一次请求时,Cloudflare 没有该资源的已存 Vary 数据,缓存未命中。匹配的 Cache Rule 可以在联系源站前规范化配置字段。这可能发生在 Cloudflare 知道响应是否含 Vary 之前。规则定义允许的规范化;响应稍后决定这些字段是否成为缓存变体的一部分。
顺序很重要。如果 Cloudflare 把多个原始值归到一个规范化缓存键,但源站仍收到原始值,源站可能产生不同响应,而缓存之后会把它们当作可互换。转发规范化值,能让源站选择和缓存匹配保持一致。
源站返回 Vary: Accept, Accept-Language。Cloudflare 记录这些头名,并按 Cache Rule 处理后的头值把响应存为一个缓存变体,用来和同一资源的其他变体区分。
另一个 /catalog 请求到来时,Cloudflare 从资源的基缓存键开始,通常是 URL 加其他配置的键字段;然后读取已存 Vary 字段,把 Cache Rule 应用到新请求的这些头上,找出匹配的缓存变体。假设它们规范化为 Accept: text/html、Accept-Language: en,fr,Cloudflare 直接用这些值查找匹配变体,而不是逐个比对所有已存变体。存在且新鲜就是命中;否则发给源站,并可能把结果存成另一个变体。
源站响应完成闭环。对 Vary 中每个头,Cloudflare 使用该头配置的动作,或规则的默认动作:
- 响应不含
Vary,正常缓存。 - 每个命名头都解析为 normalize 或 passthrough,可以存为缓存变体。
- 任何命名字段使用 bypass,不存储响应。
- 响应包含
Vary: *,不存储。
这给源站一项重要责任:每个可能因请求字段不同而不同的可缓存响应,都必须一致地返回适当的 Vary,包括错误和回退响应。只要有一个响应漏掉,Cloudflare 就可能缓存它,却没有隔离所需的变化。
图中的缓存键是概念性的;后续请求假设缓存响应仍新鲜。
针对某个缓存资源的 purge 会覆盖它的所有 Vary 变体,现有对自定义缓存键的 purge 要求仍然适用。更改 Vary 配置不会自动清除已有内容。新策略可能产生不同缓存键:请求可能未命中并在新键下重新填充,旧条目则留到期满或被清除。
规范化让等价请求共享缓存
回到前面英语和法语的请求:Accept-Language: en-US, fr;q=0.8 与 Accept-Language: fr;q=0.8, en-GB。两者都偏好英语,但透传会把它们当成不同变体。如果 Cache Rule 允许 en、fr、de,规范化会把两者都归为 en,fr,让它们共享缓存响应。
为此,Cloudflare 会把 Accept、Accept-Language、Accept-Encoding 的值转成小写,按质量值从高到低排序,质量值相同时按字母序。客户端顺序不会影响缓存键。排序后,Cloudflare 去掉非零质量值条目的参数。在缩短语言标签或过滤到配置的格式和语言时,也可能丢掉 q=0(“不可接受”),例如 en-US;q=0 可能变成 en。如果源站需要看到这些排除,对 Accept 或 Accept-Language 使用透传。
还可以配置规则,只保留 Accept 和 Accept-Language 中指定的媒体类型或语言。en-US 这类区域语言标签会归到基础语言 en,除非配置了完整标签。这样可以让规范化与源站实际提供的格式和语言对齐。
为让源站选择与缓存匹配一致,Cloudflare 会把规范化后的 Accept 和 Accept-Language 转发给源站;启用 Respect Strong ETags 时也转发规范化后的 Accept-Encoding。其他头只为缓存匹配而规范化。
在 Cache Rules 中配置 Vary
在 Cloudflare 仪表板进入 Caching > Cache Rules,创建或编辑规则,让响应有资格缓存,然后添加 Vary 设置。先设默认行为,再添加源站预计会命名的头。
同样配置可通过 Rulesets API 的 http_request_cache_settings 阶段完成。默认设置会为源站在 Vary 中命名、但你没有单独配置的头选择后备动作。
示例中,规则匹配 example.com 的 /catalog,开启缓存,默认动作是 normalize;Accept 规范化到 text/html、application/json,Accept-Language 规范化到 en、fr、de。默认 normalize 也作用于 Vary 中其他头。这是对 http_request_cache_settings 阶段入口点执行 PUT 的完整请求体。PUT 会替换该入口点中的所有规则。如果已有 Cache Rules,要把它们包含在规则数组里,或改用单条规则创建/更新操作。
如果源站为每个媒体类型和语言对提供一种表示,就有六种内容组合。这不会把缓存限制为六个键:偏好顺序、缺失头、规范化后为空的值都可能产生更多键。保持支持集小,并清楚定义规则边界。上线后,用不同但应归一化为同一缓存变体的头值测试同一 URL。从同一客户端发送测试请求,确认返回的格式和语言符合预期,并检查 CF-Cache-Status。缓存填充后应看到命中;持续未命中或意外绕过要调查。限制、更多示例和 Terraform 用法见 Vary 文档。
为什么不用自定义缓存键
一个自然的问题是:为什么不把 Accept 和 Accept-Language 加到自定义缓存键里?当这些字段始终是资源身份的一部分时,这可行。但自定义缓存键会把配置的维度加到规则覆盖的每个响应上,不管源站是否使用了它们。Vary 是响应驱动的,不过同一基缓存键下的可缓存响应需要一致的 Vary 字段。请求属性始终定义资源时用自定义缓存键;源站在可缓存响应中声明同一组请求字段时用 Vary。避免把同一个头同时放进两者,除非这种重复是有意且经过测试的。
今天就用 Cache Rules 中的 Vary
Vary 帮助解决一个明显问题:一个 URL 可以有多个正确响应。但它给缓存一个更难的问题:哪些请求差异才重要。源站知道自己能提供哪些响应,缓存需要知道哪些请求可以复用每个响应。
Cache Rules 中的 Vary 把这两种视角连起来。源站标识可能影响响应的请求字段,你决定是规范化值、为精确差异使用透传,还是让响应不进入缓存。
Vary 从未丑到无用。但手动配置支持的格式和语言未必适合每个应用。Cloudflare 正在评估已过期的 Availability Hints 草案中的想法,能否让源站直接描述所提供的表示,从而减少这项工作。
Cache Rules 中的 Vary 现已在 Free、Pro、Business、Enterprise 计划通过 Cloudflare 仪表板、Rulesets API 和 Terraform 提供。
评论
Hacker News 上几乎一致欢迎 Cloudflare 终于支持 Vary 响应头,气氛是「等了太多年」的释然;分歧在于这算补齐一项迟到的缓存基础设施,还是又一次证明基于请求头的内容协商本身就不可靠、变体应该干脆写进 URL。
认为这是迟到多年、早该有的基础能力
- 有人表示等这个功能等了好几年,甚至一度以为永远不会上线,感慨真正的 HTTP 内容协商到 2026 年才落地(高赞)
- 有开发者当年默认 Cloudflare 已支持 Vary,结果给自家 SaaS 应用埋了一个严重 bug(较多人)
- 冷门视角:如果不是这次讨论,很多人不会意识到 Vary 的实现细节有多混乱,甚至惊讶整个 Web 还能正常运转(较多人) > 我一直想要 Cloudflare 做这个,想要了好几年。(@simonw)
认为不该依赖内容协商,把变体直接写进 URL 更可靠
- 与其让同一路径按 Accept 头返回 HTML 或 JSON,不如给 JSON 单独加 .json 后缀,让 URL 行为可预测(较多人)
- 语言选择最终普遍演化成 /en-US/ 这类路径前缀,而不是 Vary: Accept-Language,这本身就说明问题(较多人)
- 在 Cloudflare 缓存下,这类模式过去长期无法部署,因为非图片响应会被忽略 Vary,导致把 JSON 缓存后回给期待 HTML 的用户(较多人) > 与 Cloudflare 这个功能无关,我最终决定永远不用那种模式,因为我更喜欢 URL 能明确返回 HTML 还是 JSON。(@simonw)
认为 Vary 过于聪明,痛苦大于回报
- 二十多年前给 Apache mod_cache 各子模块实现 Vary 支持的经验是:暴露大量用户代理 bug 和诡异后端,收益完全对不起投入(较多人)
- 有人要求按非字面虚拟头(如地理位置)变体,甚至提出按 Date 头变体,足见这套机制容易被误用(较多人) > 整件事就是聪明过头了。语言选择最后落到 URL 里而不是 Vary: Accept-Lang,一点都不奇怪。(@colmmacc)
担心缺少 Vary 头的响应会污染带 Vary 的缓存条目
- 按文档,源站响应不带 Vary 时 Cloudflare 会照常缓存,那这个不带变体的对象是否会抢先命中、压过按 Vary 分段的缓存对象(少数人)
- 如果是这样,值得加一条规则,强制每个响应都带 Vary 头(少数人)
认为这能解锁按语言变化的动态页面缓存
- 有站点按 Accept-Language 返回动态语言内容,部分页面跳转到 /en/page 这类路径;过去这类动态页面根本无法缓存,现在或许可行(少数人)
有价值的补充
- Cloudflare 默认不缓存 HTML,这是很多人没察觉该缺陷长期存在的原因(@badlibrarian)
- 调侃性的一条:既然这个功能终于做完了,也许团队终于有时间修好营销邮件里那个根本不能用的退订链接(@rgbrenner)