入门指南
同一个 URL 两种语言:Vary 少写一个头,缓存就把中文页发给英文读者
Vary 列出的是「这次响应按哪些请求头选出来」,它扩的是缓存键,不是内容。匹配只在归一化后相等时成立,请求里缺这个头就只能匹配同样缺的请求,Vary: * 永不匹配,而默认响应漏写它会让别的变体被顶掉。讲清它与 ETag 的两条独立线,以及出事后的分诊顺序。
同一个 URL 可以有多个变体,Vary 决定缓存怎么分辨它们。

先看答案
- 适合谁
- 自己发静态站、开 CDN,做了多语言或多编码版本的人
- 遇到「我这边正常、用户那边是另一种语言」却查不出原因的人
- 把 Vary 当成「缓存开关」或「安全设置」的人
- 你会获得
- 先分清哪个变体该发给谁,再谈缓存。
- 发布时间
- 实质更新
- 发布
- AOPC 编辑部
60 秒结论
一分钟看懂这篇文章
同一个 URL 可以有多个变体,Vary 决定缓存怎么分辨它们。
人负责
- 判断该不该在响应里加 Vary
- 分清是选错了内容还是发错了人
AI 负责
- 读 Vary 列出的 selecting header fields
- 比较两个请求的归一化结果
- 确认响应有多个变体
- 列出参与选择的请求头
- 核对归一化与缺省
- 按变体分诊
直接答案
「我这边打开是英文,客户那边打开是中文。」
这句话里混着两件事:源站按请求选错了内容,和源站选对了、但缓存把另一份发给了这个人。前者是内容协商的逻辑问题,后者才是 Vary 的问题。
RFC 9110 对 Vary 的第一句定义就把它的范围划清了:
The “Vary” header field in a response describes what parts of a request message, aside from the method and target URI, might have influenced the origin server’s process for selecting the content of this response.
注意两个限定:方法(method)与目标 URI 不在其中——它们本来就在缓存键里;Vary 补的是「除此之外,还有哪些请求头参与了这个 URL 的内容选择」。
它不改变内容,也不承诺内容。它只做一件事:告诉缓存「这个响应不是这个 URL 的唯一答案」。
| 响应头 | 缓存的理解 | 常见后果 |
|---|---|---|
没有 Vary |
这个 URL 只有一份内容 | 先存下的那份服务所有后来的请求 |
Vary: Accept-Language |
按语言拆成不同条目 | 语言对了,但条目数随语言数增长 |
Vary: Accept-Language, Accept-Encoding |
两个维度都参与匹配 | 对,但漏掉任何一个客户端会发的头,就少一批命中 |
Vary: * |
任何请求都无法确认匹配 | 每次使用前都要回源确认 |
三句话记住:
Vary扩的是缓存键,不是内容;- 「相等」是归一化之后的相等,不是字符串一模一样;
- 请求里没有这个头,只能匹配同样没有它的请求。
下面按这三点拆开:它到底在描述什么、匹配怎么算、* 与漏写各自会发生什么,以及它和 ETag 为什么是两条独立的线。
一、Vary 描述的是「上一次选内容用了哪些头」
定义的第二句给出了字段名的正式叫法:
A Vary field value is either the wildcard member “*” or a list of request field names, known as the selecting header fields, that might have had a role in selecting the representation for this response.
selecting header fields——「参与选择的头字段」。这个措辞是整篇的关键:它描述的是已经发生过的选择,不是你对缓存的请求。
规范紧接着补了一句,堵住了「只列标准里的头」这个误解:
Potential selecting header fields are not limited to fields defined by this specification.
也就是说,只要你的源站真的按某个请求头选过内容,它就有资格出现在 Vary 里,哪怕那个头是你自己发明的。
而规范给的例子恰好是最常见的一对:
Vary: accept-encoding, accept-language
原文对这个例子的解释里有一个容易漏掉的括号:
indicates that the origin server might have used the request’s Accept-Encoding and Accept-Language header fields (or lack thereof) as determining factors while choosing the content for this response.
or lack thereof——「或者它们不存在」也算一种选择依据。这一点会在第四节变成一个具体的事故。
Vary 还有第二个读者,不只是缓存:
To inform user agent recipients that this response was subject to content negotiation (Section 12) and a different representation might be sent in a subsequent request if other values are provided in the listed header fields (proactive negotiation).
所以它同时是一句对客户端的告知:同一个 URL,换个请求头可能给你另一份。这也是为什么第五节说它不只影响浏览器缓存。
二、它扩的是缓存键,不是内容本身
RFC 9111 先定义了缓存键的最小构成:
The “cache key” is the information a cache uses to choose a response and is composed from, at a minimum, the request method and target URI used to retrieve the stored response; the method determines under which circumstances that response can be used to satisfy a subsequent request. However, many HTTP caches in common use today only cache GET responses and therefore only use the URI as the cache key.
这段话把现实说得很直白:很多缓存只拿 URI 当键。在那种缓存里,/index.html 只有一格。
Vary 就是往这一格里加维度:
A cache might store multiple responses for a request target that is subject to content negotiation. Caches differentiate these responses by incorporating some of the original request’s header fields into the cache key as well, using information in the Vary response header field, as per Section 4.1.
RFC 9110 用一句话总结了同一个机制:
In other words, Vary expands the cache key required to match a new request to the stored cache entry.
「扩展缓存键」这四个字,比「让缓存更聪明」准确得多。它带来的是分格,不是判断力:原来一格装一份,现在一格装多份,每份各自带一套「原始请求的头字段值」。
没有 Vary |
有 Vary |
|
|---|---|---|
| 缓存里有几格 | 一个 URI 一格 | 一个 URI 多格,按列出的头分 |
| 新请求怎么找 | 只看 URI | URI 相同,再比列出的头 |
| 找不到匹配时 | 几乎总能拿到那一份 | 回源(或按规则验证) |
| 谁来决定分几格 | 缓存实现 | 你的响应头 |
这里有一个方向性的误解要先纠正:Vary 不会让缓存更愿意缓存,也不会提高命中率。规范自己把代价写在了明处:
Vary might be elided when an origin server considers variance in content selection to be less significant than Vary’s performance impact on caching, particularly when reuse is already limited by cache response directives (Section 5.2 of [CACHING]).
Vary's performance impact on caching——它是有成本的。RFC 9110 在讲主动协商的缺点时也列了同一条:
It limits the reusability of responses for shared caching.
(这两句都不构成「所以别写」的建议:漏写 Vary 的代价是发错内容,比少几次命中严重得多。规范允许省略的前提是「这点差异不重要」,而不是「你懒得写」。)
三、「相等」是归一化之后的相等
这是最容易出错的一节。很多人以为「两个请求都带 Accept-Encoding: gzip 就算匹配」,规范给的是变换后等价:
The header fields from two requests are defined to match if and only if those in the first request can be transformed to those in the second request by applying any of the following:
- adding or removing whitespace, where allowed in the header field’s syntax
- combining multiple header field lines with the same field name (see Section 5.2 of [HTTP])
- normalizing both header field values in a way that is known to have identical semantics, according to the header field’s specification (e.g., reordering field values when order is not significant; case-normalization, where values are defined to be case-insensitive)
三种变换,逐条落到实际请求上:
| 变换 | 会匹配的例子 | 不会匹配的例子 |
|---|---|---|
| 增删空白 | gzip, br 与 gzip,br |
— |
| 合并同名行 | 两行 Accept-Language 与一行逗号分隔 |
— |
| 按字段语义归一化 | Accept-Encoding: gzip, br 与 br, gzip(顺序无意义) |
Accept-Language: en-US 与 en-US,en;q=0.9(后者多了一个偏好项,不是同一组值) |
第三行右边那条要展开说:归一化只做「规范认定语义相同」的变换,不做「你的源站认为等价」的变换。你的代码可能把 en-US,en;q=0.9 和 en-US 都解析成英文,但在缓存眼里这是两个不同的值,于是分成两格。多出来的那一格不会报错,只会让命中率悄悄下降。
反过来也成立:缓存不会替你补一个缺失的值。原文把这一条单独写了一句:
If (after any normalization that might take place) a header field is absent from a request, it can only match another request if it is also absent there.
「缺席只能匹配缺席」。没有 Accept-Encoding 的请求,匹配不到任何带 Accept-Encoding 的条目——哪怕服务端最后给两边的内容一模一样。
当多份都匹配时,规范给了一个次要的择一规则:
If multiple stored responses match, the cache will need to choose one to use. When a nominated request header field has a known mechanism for ranking preference (e.g., qvalues on Accept and similar request header fields), that mechanism MAY be used to choose a preferred response. If such a mechanism is not available, or leads to equally preferred responses, the most recent response (as determined by the Date header field) is chosen, as per Section 4.
注意是 MAY:这是允许的优化,不是必须。所以「反正有多份,缓存会挑最合适的」不能当成前提。
四、请求里没有这个头,只能匹配同样没有的请求
把上一节最后那条规则单独拎出来,因为它是「我这边正常」最常见的来源。
RFC 9111 在解释「默认响应」时,顺手给了一个定义:
Some resources mistakenly omit the Vary header field from their default response (i.e., the one sent when the request does not express any preferences), with the effect of choosing it for subsequent requests to that resource even when more preferable responses are available.
括号里那句 the one sent when the request does not express any preferences 值得抄下来:「默认响应」= 请求没有表达任何偏好时的那一份。它不是「最重要的那一份」,也不是「兜底的那一份」,只是「没人提要求时给的那一份」。
于是有两种完全不同的「没有这个头」:
| 情形 | 缓存里有几份 | 谁会拿到它 |
|---|---|---|
客户端没发这个头,响应也没 Vary |
一份 | 所有请求 |
客户端没发这个头,响应有 Vary |
至少两份 | 只有同样没发的请求 |
第二种是正确的:没发偏好的请求拿到默认那份,发了 Accept-Language: en 的请求拿到英文那份,互不干扰。第一种才是事故:用 curl(默认不带 Accept-Language,也不带浏览器的 Accept-Encoding)存下来的那一份,会被拿去服务所有浏览器。
这也解释了为什么「本地测是好的」不能证明线上没问题——本地那一发请求,本身就是「没有表达偏好」的那一类。
编辑判断:用
curl复现线上问题时,先把你怀疑的请求头补齐,再比较。缺头的那一发请求,走的是另一条分支,不是「同一个请求的简化版」。
五、Vary: * 不是「不缓存」,是「永不匹配」
先看 * 在语义上意味着什么:
A list containing the member “” signals that other aspects of the request might have played a role in selecting the response representation, possibly including aspects outside the message syntax (e.g., the client’s network address). A recipient will not be able to determine whether this response is appropriate for a later request without forwarding the request to the origin server. A proxy MUST NOT generate “” in a Vary field value.
两个要点:
*等于「我也说不清是哪些方面」——甚至包括报文语法之外的东西,比如客户端网络地址;- 接收方无法自己判断这份响应适不适合后来的请求,只能转发回源站。
RFC 9111 把它落成了匹配规则:
A stored response with a Vary header field value containing a member “*” always fails to match.
「永不匹配」和「不存储」是两件事。规范没有说带 Vary: * 的响应不能被存下来;它说的是存下来也用不上,每次都得回源确认。所以:
| 写法 | 还能不能存 | 后续请求 |
|---|---|---|
Vary: * |
规范未禁止存储 | 永远不匹配,必须回源确认 |
Cache-Control: no-store |
禁止存储 | 不适用 |
Cache-Control: private |
只能存在私有缓存 | 共享缓存不得使用 |
Vary: * 常被当成「一招禁用缓存」来用。它确实能让共享缓存不敢直接复用,但它不是 no-store:存储仍然可能发生,只是每次使用前都要回源。别把它当隐私开关——真正不想让中间缓存碰的内容,该用 private 或 no-store。
还有一条容易被忽略的约束:MUST NOT generate "*" 只约束 代理。源站生成 * 是合法的(虽然通常意味着「我没法枚举清楚」)。这也解释了为什么同一份配置在不同链路上表现不同——你前面那一层是不是代理,决定了它有没有资格写这个 *。
六、漏写 Vary 的默认响应,会顶掉别的变体
这是最常见的一种,规范用 mistakenly(错误地)来描述:
Some resources mistakenly omit the Vary header field from their default response (i.e., the one sent when the request does not express any preferences), with the effect of choosing it for subsequent requests to that resource even when more preferable responses are available.
效果写得很清楚:默认那份被选给后续所有请求,哪怕有更合适的响应已经存在。
规范给了一个补救,但它只是 SHOULD:
When a cache has multiple stored responses for a target URI and one or more omits the Vary header field, the cache SHOULD choose the most recent (see Section 4.2.3) stored response with a valid Vary field value.
两点限定决定了它靠不住:
- 这是
SHOULD,不是MUST——缓存可以不这么做; - 前提是「已经有多份存储响应」。如果漏写
Vary的那一份先到,缓存里只有它,就无所谓「多份」可选。
所以正确的顺序是:先保证每一份都带 Vary,再谈命中率。指望缓存在缺头时替你挑,是把正确性押在一个可选行为上。
三种典型漏写:
| 漏写的位置 | 谁会看到错的那份 | 表现 |
|---|---|---|
| 默认响应(无偏好请求) | 后来所有请求 | 全站回到默认语言/未压缩版本 |
| 错误页/404 页 | 同 URL 的成功响应 | 修好之后仍看到旧错误页 |
| 重定向响应 | 目标变体 | 跳到错误语言的分支 |
第二、三行不是规范原文的分类,是编辑判断:它们之所以也会中招,是因为这三类响应同样参与内容选择,也同样可能被缓存。
七、Vary 与 ETag 是两条独立的线
这两者经常被混为一谈:一个管「哪一份」,一个管「这份还能不能用」。
RFC 9110 给了一个内容编码的例子,把两条线并排放在一起:
GET /index HTTP/1.1
Host: www.example.com
Accept-Encoding: gzip
未压缩的那份:
HTTP/1.1 200 OK
Date: Fri, 26 Mar 2010 00:05:00 GMT
ETag: "123-a"
Content-Length: 70
Vary: Accept-Encoding
Content-Type: text/plain
压缩的那份:
HTTP/1.1 200 OK
Date: Fri, 26 Mar 2010 00:05:00 GMT
ETag: "123-b"
Content-Length: 43
Vary: Accept-Encoding
Content-Type: text/plain
Content-Encoding: gzip
同一份原文,两个 ETag("123-a" 与 "123-b"),同一个 Vary。规范在例子后面的注记里给了原因:
Content codings are a property of the representation data, so a strong entity tag for a content-encoded representation has to be distinct from the entity tag of an unencoded representation to prevent potential conflicts during caching or validation.
强校验器必须区分内容编码。这正好说明两条线各管一段:
| 改哪个 | 解决什么 | 没解决什么 |
|---|---|---|
只加 Vary |
两个变体不再共用一个缓存格 | 共用 ETag 时,条件请求仍可能把 A 的校验结果用在 B 上 |
| 只改 ETag | 条件请求不会把两份当成同一份 | 缓存仍只有一个格,后到的变体覆盖先到的 |
| 两个都对 | 分格 + 分校验器 | 命中率仍受客户端请求头一致性影响 |
它们还会在验证阶段交汇。RFC 9111 说,缓存自己发起验证请求时,会照着 Vary 把请求头补齐:
When generating a conditional request for validation, a cache either starts with a request it is attempting to satisfy or – if it is initiating the request independently – synthesizes a request using a stored response by copying the method, target URI, and request header fields identified by the Vary header field (Section 4.1).
也就是说:Vary 列错了,连回源验证时带什么头都会跟着错。这一层比「缓存命中」更深,也是「修了缓存还在错」的一种解释。
八、304 也要带 Vary,而且它不会替你补上缺的那条
RFC 9110 把 Vary 列进了 304 的必带清单:
The server generating a 304 response MUST generate any of the following header fields that would have been sent in a 200 (OK) response to the same request:
- Content-Location, Date, ETag, and Vary
- Cache-Control and Expires (see [CACHING])
注意条件是「如果 200 会带」。200 从来没带 Vary 的资源,304 也没有义务凭空补一个——错误会被原样继承下来。
那 304 到达缓存之后,具体改了什么?RFC 9111 §3.2 的规则是「加进去、覆盖已有值」:
When doing so, the cache MUST add each header field in the provided response to the stored response, replacing field values that are already present, with the following exceptions: …
这里没有「删除 304 里缺失的字段」这一条。也就是说:
- 304 带了
Vary→ 覆盖存储条目里的旧值(应当与 200 一致); - 304 没带
Vary→ 存储条目里的旧值保持原样,不会被清掉。
编辑判断:上一条是从 §3.2「只加与替换、不删」推出来的,规范没有用一句话直说「缺失即保留」。实践含义是:一次漏写会跟着条目活到它被替换为止,不会因为一次 304 自动修好。
更麻烦的是 §4.3.4 的更新范围。缓存拿到 304 后,先按 §4.1 筛出「本来可能被选中的那些条目」,再用强校验器精确定位:
If the new response contains one or more “strong validators” … then each of those strong validators identifies a selected representation for update. All the stored responses in the initial set with one of those same strong validators are identified for update. If none of the initial set contains at least one of the same strong validators, then the cache MUST NOT use the new response to update any stored responses.
把两段连起来看,会得到一个不太直观的结论:Vary 与 ETag 两处都松,才会串。
Vary缺失 → 两个变体进同一个初始集合;- ETag 又相同 → 这个 304 会更新集合里所有带同一强校验器的条目。
只松一处,另一处还能兜住一半。这也是为什么排障时不能只看一个头。
九、十分钟核对
下面的命令用 https://example.com/ 占位,换成你自己的 URL。默认用 GET 拿响应头(-D - 把头部打到标准输出),因为有些源站对 HEAD 与 GET 的处理不同;只有在确认两者一致时才用 -I。
第一组:这份响应到底声明了什么
curl -sS -o /dev/null -D - https://example.com/ \
| grep -iE '^(HTTP/|vary:|etag:|content-language:|content-encoding:|cache-control:)'
第二组:三个不同的请求,比一比
for h in "zh-CN" "en" "fr"; do
echo "== Accept-Language: $h"
curl -sS -o /dev/null -D - -H "Accept-Language: $h" https://example.com/ \
| grep -iE '^(HTTP/|vary:|etag:|content-language:|content-length:)'
done
如果三行的 Vary 都为空、Content-Language 却在变,那就是「同一格装了三种内容」——第六节描述的情况。
第三组:304 有没有带 Vary
etag=$(curl -sS -o /dev/null -D - https://example.com/ \
| grep -i '^etag:' | tr -d '\r' | cut -d' ' -f2-)
curl -sS -o /dev/null -D - -H "If-None-Match: $etag" https://example.com/ \
| grep -iE '^(HTTP/|vary:|etag:)'
第四组:有没有写成 *
curl -sS -o /dev/null -D - https://example.com/ | grep -i '^vary:' | grep -F '*'
判读:
| 看到什么 | 说明 | 下一步 |
|---|---|---|
有 Vary,且列的头正是你在用的选择依据 |
声明与实现一致 | 再核 304 是否同样带 Vary |
有 Vary,但少了某个你确实在用的头 |
漏写 | 补上,并按变体重验 |
没有 Vary,但响应内容随请求头变化 |
典型漏写 | 先补 Vary,再查缓存里的旧条目 |
Vary: * |
每次都要回源确认 | 如果本意是「不缓存」,改用 Cache-Control |
两个变体 ETag 相同 |
校验器没有区分变体 | 按表示分别生成强校验器 |
十、上线前十二项清单
- 这个 URL 在当前配置下会不会返回不同内容?不会,就别加
Vary。 - 列出的每一个头,都是源站真的用来选内容的吗?
- 有没有「按
Accept-Encoding压缩」却没写Vary: Accept-Encoding? - 有没有按
Accept-Language选语言,却只写了Vary: Accept-Encoding? - 默认响应(无偏好请求)也带
Vary吗? - 错误页、404、重定向响应也带吗?
- 是否误把
Authorization列进去?(规范说没有必要) - 是否用了
Vary: *?如果本意是禁止缓存,换成Cache-Control。 - 两个变体的强
ETag是否不同? - 304 响应里的
Vary、ETag是否与 200 一致? - 压缩层(CDN、反向代理、应用中间件)有没有改写或丢弃
Vary? - 上线后有没有用带偏好的请求复核,而不只是
curl裸请求?
六种误读,与本周能做的五件事
| 误读 | 原文怎么说 |
|---|---|
「Vary 能提高缓存命中率」 |
原文把 Vary's performance impact on caching 写成省略它的理由——它是成本,不是收益 |
「缺 Vary 的响应不能被缓存」 |
原文只说这种情况下缓存 SHOULD 优先选最近一条带合法 Vary 的存储响应 |
「写了 Vary 就一定不串内容」 |
匹配依赖客户端真的发了一致的值;缺字段只能匹配缺字段 |
「Vary: * 等于不缓存」 |
原文是 always fails to match——仍需回源验证,不是禁止存储 |
「Vary 只影响浏览器缓存」 |
定义里第二个目的是告知 user agent,第一个目的约束的是所有 cache recipient |
「Vary 是安全设置」 |
它只决定「哪份能发给谁」,不决定「谁能拿到」;访问控制另有机制 |
本文不写:各家 CDN/反向代理对
Vary的支持差异、字段条数上限、归一化的具体实现、缓存命中率数字,以及任何浏览器的默认请求头组合。这些都没有一手来源,写出来就是猜。
本周能做的五件事:
- 挑一个确实会变的页面,把它的响应头抄下来,对照第 2 条清单逐项核。
- 用第二组命令跑三种语言,确认
Vary与Content-Language同时存在。 - 用第三组命令验证 304 是否带
Vary。 - 如果站点做过压缩,确认压缩层没有把
Vary: Accept-Encoding吃掉。 - 在监控里加一条:主页面响应缺
Vary但内容随请求头变化时告警。
边界说明
- 本文只引用 RFC 9110 与 RFC 9111 的原文;每处引用都可在
rfc-editor.org的纯文本里检索到。 - 标为「编辑判断」「编辑推论」的段落是从原文推出来的工程结论,不是规范原文。
- 不讨论具体 CDN 产品的配置项名称、默认行为与生效时间;这些需要各家的官方文档,本文没有取证。
- 不讨论
Vary对 SEO 的影响;没有一手来源支持具体结论。
来源
- RFC 9110: HTTP Semantics — https://www.rfc-editor.org/rfc/rfc9110.txt核查于 2026/11/1(2026-11-01 访问;§12.5.5 定义与两个目的、§12.4.3 通配符、§12.1 主动协商的代价、§15.4.5 304 必带字段、§8.8.3.3 内容编码与 ETag 的例子)
- RFC 9111: HTTP Caching — https://www.rfc-editor.org/rfc/rfc9111.txt核查于 2026/11/1(2026-11-01 访问;§2 缓存键、§3.2 头部更新、§4.1
Vary匹配规则、§4.3.1 验证请求、§4.3.4 304 更新范围)