入门指南
线上还是旧的,这次让每一层自己交代:Cache-Status 的顺序、八个参数与默认不出现的原因
Age 只能说明存了多久,说不出是谁存的、为什么回源、有没有存下来。RFC 9211 的 Cache-Status 让每层缓存按离源站由近到远各留一条记录:命中还是回源、回源的原因、剩余 ttl、有没有存、有没有被合并。讲清八个参数怎么读,以及它为什么默认可能根本不出现。
顺序是源站到用户;hit 与 fwd 互斥;没这个头不等于没缓存。

先看答案
- 适合谁
- 自己发静态站、开了 CDN,改完只能靠猜是哪一层没更新的人
- 会用 Age 和 CF-Cache-Status,但换一家缓存就完全看不见的人
- 把 Cache-Status 当成「缓存一定命中/一定没命中」信号的人
- 你会获得
- 它记的是每层做了什么,不是存了多久。
- 发布时间
- 实质更新
- 发布
- AOPC 编辑部
60 秒结论
一分钟看懂这篇文章
顺序是源站到用户;hit 与 fwd 互斥;没这个头不等于没缓存。
人负责
- 决定要不要打开这个头
- 读出是哪一层在骗你
AI 负责
- 按离源站由近到远读成员
- 核对 ttl 与回源原因
- 打开调试
- 取响应头
- 逐层读成员
- 按层修
直接答案
「部署成功了,源站也改了,线上还是旧的。」
以前遇到这句话,手里只有 Age:它告诉你这份响应从源站生成到现在过了多久,但说不出是谁存了它、为什么这次回源、回源之后有没有存下来。换一家 CDN、换一个反向代理,能看的头就换一套,CF-Cache-Status 只在 Cloudflare 那边有意义。
RFC 9211 定义了一个标准的替代品:每一层缓存往响应里追加一个成员,说明自己是怎么处理这次请求的。这就是 Cache-Status。
它最容易被读反的一点,是成员顺序:
The first member represents the cache closest to the origin server, and the last member represents the cache closest to the user (possibly including the user agent’s cache itself if it appends a value).
第一个成员离源站最近,最后一个离用户最近。 读反了,两层各自的 ttl 就会张冠李戴,进而得出「边缘还有 20 分钟、浏览器已经过期」这种正好相反的结论。
四条硬规则,先摆在这里:
| # | 规则 | 依据 | 读错的后果 |
|---|---|---|---|
| ① | 成员顺序是源站 → 用户 | §2 原文 | 两层的 ttl 对调 |
| ② | hit 与 fwd 互斥 |
§2.1 原文 | 把同一层读成「命中又回源」 |
| ③ | 这个头默认可能根本不出现 | §2 原文 | 看不到就以为没缓存 |
| ④ | ttl 是剩余寿命,可以是负数 |
§2.4 原文 | 当成总寿命,算错过期时间 |
本文只讲这个标准头本身:它由谁写、八个参数各回答什么问题、多层时怎么读,以及它在什么情况下不会出现。每家 CDN、反向代理和浏览器有没有实现、默认开不开、怎么开,需要各自的官方文档——本文没有取证,也不猜。
一、Age 回答「多久」,它回答「谁、做了什么」
先把三个头的分工摆清楚。它们经常同时出现在一个响应里,回答的却是三个不同的问题:
| 头 | 回答的问题 | 谁写 |
|---|---|---|
Cache-Control / Expires |
这份响应允许被存多久、谁能存 | 源站(也可能被中间层改写) |
Age |
从源站生成或验证到现在,过了多久 | 每一层缓存各自改写 |
Cache-Status |
这一层做了什么:命中、回源、为什么回源 | 每一层缓存各自追加 |
Age 的语义边界在 RFC 9111 §5.1 写得很清楚:
The presence of an Age header field implies that the response was not generated or validated by the origin server for this request. However, lack of an Age header field does not imply the origin was contacted.
有 Age 说明这次不是源站现做的;没有 Age 不等于回源了。所以「看 Age 是不是 0」既证不了命中,也证不了回源。而 Age 的数值本身是各层停留时间之和(§4.2.3):
The Age value is therefore the sum of the time that the response has been resident in each of the caches along the path from the origin server, plus the time it has been in transit along network paths.
一句话:Age 是计时器,Cache-Status 是工作记录。要知道「谁没干活」,只能看后者。
二、把顺序背下来:第一个最靠近源站
RFC 9211 §2 先定义了形状:
Its value is a List. Each member of the List represents a cache that has handled the request.
然后才是顺序那句话。两层的情形,规范给的例子是:
Cache-Status: OriginCache; hit; ttl=1100,
"CDN Company Here"; hit; ttl=545
读法:离源站最近的那层(OriginCache)命中了,还剩 1100 秒;离用户更近的那层("CDN Company Here")也命中了,但它那份只剩 545 秒。越靠用户,剩余寿命越短——这正是顺序应该带来的直觉。反过来读,就会以为边缘比源站更新鲜。
三层时,规范把三个成员写成三行:
Cache-Status: ReverseProxyCache; hit
Cache-Status: ForwardProxyCache; fwd=uri-miss; collapsed; stored
Cache-Status: BrowserCache; fwd=uri-miss
三行仍然是一个 List。RFC 8941 §4.2 规定了解析时的合并方式:
When generating input_bytes, parsers MUST combine all field lines in the same section (header or trailer) that case-insensitively match the field name into one comma-separated field-value
也就是说:拆成几行不影响顺序,拼接后仍按书写顺序读。看到三行不要以为是三个不同的头。
两个细节值得单独记:
- 成员标识必须是 String 或 Token。 §2 原文:
Each List member identifies the cache that inserted it, and this identifier MUST be a String or Token.带空格的标识要加引号(例子里就是"CDN Company Here");不加引号的 Token 不允许有空格。 - 最后一个成员可能就是你自己的浏览器缓存。 原文那句括号里写着
possibly including the user agent's cache itself if it appends a value。所以「三层」不一定是「边缘 + 代理 + 源站」,也可能把 UA 自己算进去。判断某一层是谁,看标识字符串,不要按位置猜。
三、八个参数,分成三组
§2 开头说了参数的定位:
Each member of the list can have parameters that describe that cache’s handling of the request. While these parameters are OPTIONAL, caches are encouraged to provide as much information as possible.
参数全部可选,所以最小形态是合法的:
Cache-Status: ExampleCache; hit
但这种写法几乎没有信息量——没有 ttl 就不知道还剩多久,没有 fwd 就不知道有没有回源。八个标准参数按「回答什么」可以分三组(类型与「是否只在 fwd 存在时有意义」都取自 §2.1–§2.8,并可在 IANA 注册表逐行核对):
| 参数 | 类型 | 只在 fwd 存在时有意义 |
回答的问题 |
|---|---|---|---|
hit |
Boolean | 否 | 这次用的是存下来的那份吗 |
fwd |
Token | 否 | 如果回源了,为什么 |
fwd-status |
Integer | 是 | 回源拿回来的是哪个状态码 |
ttl |
Integer | 否 | 这份响应还剩多少新鲜寿命(可为负) |
stored |
Boolean | 是 | 回源之后存下来了吗 |
collapsed |
Boolean | 是 | 这次请求有没有和别的请求合并 |
key |
String | 否 | 用的是哪个缓存键(实现相关) |
detail |
String 或 Token | 否 | 这一层自己的补充信息 |
第三列是读表时最容易忽略的一列:fwd-status、stored、collapsed 三个只在 fwd 出现时才有意义。在一层写着 hit 的成员上找 stored,是在问一个没有答案的问题。
八条全部登记在 IANA 的「HTTP Cache-Status」注册表里,Reference 一栏都是 RFC 9211——这意味着它们不是某家厂商的自造字段;厂商要加自己的参数,走的是 §4 的扩展注册流程,那里还有一条取向:
Generic parameters are preferred over vendor-specific, application-specific, or deployment-specific values.
看到不认识的参数名,先想到「这是某个实现的扩展」,而不是「RFC 又加了一条我没跟上」。
四、hit 不等于 200
这是第二个高频误读。§2.1 的定义先给了 hit 的本义:
The value of “hit” is a Boolean that, when true, indicates that the request was satisfied by the cache; that is, it was not forwarded, and the response was obtained from the cache.
关键词是 not forwarded——判据是「有没有往前送」,不是「状态码是不是 200」。紧接着规范把三种容易搞混的情形都点了名:
A response that was originally produced by the origin but was modified by the cache (for example, a 304 or 206 status code) is still considered a hit, as long as it did not go forward (e.g., for validation).
304 和 206 可以是 hit。 这跟直觉相反:304 明明是一次「问过源站」的结果。区别在于问的是谁——如果源站产生过这份响应、缓存只是把它改写成 304 交给客户端,而自己没有再往前送,就算 hit。
A response that was in cache but not able to be used without going forward (e.g., because it was stale or partial) is not considered a hit.
存了但必须回源才能用(过期了、或者只有一部分),不算 hit。
Note that a stale response that is used without going forward (e.g., because the origin server is not available) can be considered a hit.
源站不可用时把陈旧的那份直接用了,算 hit,这时 ttl 通常是负数(见第七节)。
再看互斥规则:
“hit” and “fwd” are exclusive; only one of them should appear on each list member.
注意原文这里是小写的 should,不是 BCP 14 的全大写 SHOULD,所以它描述的是一种使用约定,而不是一条可被一致性测试卡住的强制要求。工程上的处理是:同一个成员上同时出现 hit 和 fwd,按异常数据对待,先怀疑中间层拼接或改写出了问题,别据此下结论(编辑判断)。
五、fwd 的八个原因:由具体到笼统
fwd 是「这次请求往前送了」,值是回源的原因:
“fwd”, when present, indicates that the request went forward towards the origin; its value is a Token that indicates why.
八个标准值按「从最具体到最笼统」排列(§2.2):
| 值 | 含义 | 对应的现场 |
|---|---|---|
bypass |
这层被配置成不处理该请求 | 规则里排除了这条路径 |
method |
请求方法的语义要求必须转发 | POST、PUT 之类 |
uri-miss |
缓存里没有任何匹配这个 URI 的响应 | 第一次请求、刚被清过、换了查询串 |
vary-miss |
有这个 URI 的响应,但按 Vary 选不中 |
语言、编码、UA 变了 |
miss |
有响应但没有能用的(实现分不清上面两种时用) | 实现粒度较粗 |
request |
有新鲜的可选,但请求语义不允许用它 | 请求带了 no-cache、max-age=0 |
stale |
选到了,但已经过期 | 要回源验证或重新取 |
partial |
选到的是不完整的响应,凑不齐请求的区间 | 断点续传、分段请求 |
规范对「用哪个值」有明确取向:
The most specific reason known to the cache SHOULD be used, to the extent that it is possible to implement.
两个判读价值最高的区分:
① uri-miss 与 vary-miss 指向完全不同的修法。 前者是「根本没有」,后者是「有,但挨不上」——后者几乎总能追到 Vary 列的头和你这次请求的头不一致。如果看到 vary-miss 却去查缓存键里有没有查询串,方向就错了。
② request 与 stale 都会让「明明有新鲜的」变成回源。 request 是你的请求不让用(例如为了核对内容而带上 Cache-Control: no-cache),stale 是它自己过期了。前者你换个请求就好,后者只能等或清。把这两者混为一谈,就会得出「缓存坏了」的错误结论。
六、只在 fwd 出现时有意义的那三个
fwd-status:回源拿到的状态码。
The fwd-status parameter is only meaningful when fwd is present. If fwd-status is not present but the fwd parameter is, it defaults to the status code sent in the response.
它存在的原因是,只看客户端收到的状态码分不清两种 304:一种是缓存自己改写出来的,一种是下一跳真的回了 304。§2.3 把用途写得很直白:
This parameter is useful to distinguish cases when the next-hop server sends a 304 (Not Modified) response to a conditional request or a 206 (Partial Content) response because of a range request.
所以 fwd=stale; fwd-status=304 的正确读法是:缓存拿旧的去问了源站,源站说「没变」,于是继续用旧的。内容确实是旧的,但源站认为它没变——这两件事同时成立,不矛盾。
stored:回源之后有没有存。
The stored parameter is only meaningful when fwd is present.
fwd=miss; stored 说明这次既回了源、又把结果存下来了,下一个请求有机会命中。反过来,fwd=miss 而没有 stored,说明这层不打算存它——可能是 no-store、可能是 private、也可能是配置没让它存。这一条是「为什么每次都回源」最直接的证据。
collapsed:有没有和别的请求合并。
If true, the response was successfully reused; if not, a new request had to be made. If not present, the request was not collapsed with others.
「合并」(collapse)在 RFC 9111 §4 里的定义是把多个进来的请求合并成一次回源。规范给了两个例子,差别很细:
Cache-Status: ExampleCache; fwd=uri-miss; collapsed
Cache-Status: ExampleCache; fwd=uri-miss; collapsed=?0
第一条是「合并了,而且这次响应被成功复用」;第二条原文的注解是尝试合并但没成功。而完全不写 collapsed,含义是「压根没有合并这回事」。
这里要借 RFC 8941 的一条序列化规则才读得准:
Parameters whose value is Boolean (see Section 3.3.6) true MUST omit that value when serialized.
也就是说 collapsed 等价于 collapsed=?1,但序列化时必须省掉 ?1(§3.3.6 也复述了这一点:Boolean true is indicated by omitting the value)。所以线上真看到 collapsed=?1,说明这个实现在序列化上不合规——解析器仍然必须能吃下它,但你可以据此判断「这层的头不是标准库生成的」(编辑判断)。
七、ttl 是剩余寿命,而且可能是负数
定义有两句,都要读:
The value of “ttl” is an Integer that indicates the response’s remaining freshness lifetime (see [HTTP-CACHING], Section 4.2.1) as calculated by the cache, as an integer number of seconds, measured as closely as possible to when the response header section is sent by the cache.
This includes freshness assigned by the cache through, for example, heuristics (see [HTTP-CACHING], Section 4.2.2), local configuration, or other factors. It may be negative, to indicate staleness.
三个要点:
- 是「剩余」,不是「总寿命」。 一份
max-age=3600的响应,在边缘已经躺了 3000 秒,ttl就是 600 左右,不是 3600。 - 它包含启发式寿命。 源站没写
max-age/Expires时,缓存可以自己估一个(RFC 9111 §4.2.2 明确does not provide specific algorithms,只给最坏情况的约束),估出来的值也会体现在ttl里。 - 可以是负数。 规范自己的例子就是
Cache-Status: ExampleCache; hit; ttl=-412,注释是「a stale hit just has negative freshness」。看到负数不要当成 bug,它描述的正是第四节里那种「源站不可用、直接把陈旧的用了」的命中。
再补一层容易算错的:两层的 ttl 不能相加,也不是同一份预算。 每层都按自己的规则算「我这份还剩多少」(§2.4 的 as calculated by the cache),而 Age 才是各层停留时间之和(RFC 9111 §4.2.3)。所以「边缘 ttl=545、浏览器 ttl=-10」是完全可能同时出现的——那意味着浏览器手里那份已经过期,而边缘手里那份还新鲜(编辑推论:两层的新鲜寿命起点不同,谁先存谁先老)。
八、key 与 detail:一个可能帮到攻击者,一个只对本层成立
key 是可选参数,暴露的是缓存键:
The value of “key” is a String that conveys a representation of the cache key (see [HTTP-CACHING], Section 2) used for the response. Note that this may be implementation specific.
may be implementation specific 这句话的分量在于:它只是「某个表示」,不是「完整键」。所以拿两层的 key 直接做字符串比较、据此判断「是不是同一个对象」,是不成立的(编辑判断)。RFC 9111 §2 对缓存键的定义也只给了下限:
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
「至少」包含方法与目标 URI,其余(Vary 列的头、实现自己加的材料)都可以并进去。这也是为什么「加了查询串就一定 miss」这种经验有时不灵——查询串在不在键里,取决于这一层的实现(编辑判断)。
更要紧的是 §6 的安全考量,它直接决定了这个头该不该开:
Attackers can use the information in Cache-Status to probe the behavior of the cache (and other components) and infer the activity of those using the cache.
For example, knowing if a cache has stored a response can help an attacker execute a timing attack on sensitive data.
Additionally, exposing the cache key can help an attacker understand modifications to the cache key, which may assist cache poisoning attacks.
Note that merely obfuscating the key does not mitigate this risk.
「把 key 混淆一下」不算缓解,规范把这句话单独写出来了。可用的处置在 §6 末尾:
To avoid assisting such attacks, the Cache-Status header field can be omitted, only sent when the client is authorized to receive it, or sent with sensitive information (e.g., the key parameter) only when the client is authorized.
三条路:不发、只发给已授权客户端、只在客户端已授权时才带敏感参数。对一个人的静态站,最常见的组合是「只在带调试开关的请求上发」——但这需要实现支持,本文不给具体产品配置。
detail 则是完全相反的东西:它不承担跨层语义。
The semantics of a detail parameter are always specific to the cache that sent it; even if a details parameter from another cache shares the same value, it might not mean the same thing.
规范自己的例子是 Cache-Status: ExampleCache; hit; detail=MEMORY。两层都写 detail=MEMORY 也不代表同一个意思——一层可能指内存缓存,另一层可能指「内存命中模式」。跨层比较 detail 是没有意义的。
九、为什么你可能根本看不到这个头
这是本文最该先讲、却最容易在排查时忘掉的一条。§2 写得很清楚:
Caches determine when it is appropriate to add the Cache-Status header field to a response. Some might add it to all responses, whereas others might only do so when specifically configured to, or when the request contains a header field that activates a debugging mode. See Section 6 for related security considerations.
三种常见形态:全都加、配置后才加、请求带调试开关才加。所以「curl -I 没看到 Cache-Status」的合法解释至少有四个:
- 这一层不支持这个头;
- 这一层支持,但没开;
- 这一层只在特定请求上发(调试开关、已授权客户端);
- 这一层发了,被中间某一层删掉了。
还有一条专门解释「为什么 403、429 上没有它」:
An intermediary SHOULD NOT append a Cache-Status member to responses that it generates locally, even if that intermediary contains a cache, unless the generated response is based upon a stored response (e.g., 304 (Not Modified) and 206 (Partial Content) are both based upon a stored response).
自己在边缘生成的响应(限流、拦截、错误页)默认不写这个成员——因为那不是「缓存处理请求」的结果。规范紧接着举了例子:代理因为请求格式错误而生成的 400,不会带 Cache-Status。所以「被 WAF 拦下来的那个 403 没有缓存状态头」不是异常。
最后一条是「链路为什么只剩一层」:
When adding a value to the Cache-Status header field, caches SHOULD preserve the existing field value, to allow debugging of the entire chain of caches handling the request.
SHOULD preserve——是建议,不是强制。某一层整体重写这个头,链路就断了。看到只有一个成员时,先问「上游到底发了没有」,而不是直接认定「只有一层缓存」。
十、出事了,按这个顺序读
把上面的规则落成一张分诊表。用法:从最右(离用户最近)往左读,先找那个「本该命中却没命中」的成员。
| 现象 | 先看哪个成员 | 结论 | 下一步 |
|---|---|---|---|
页面旧,最后一层是浏览器缓存且 hit; ttl 还很大 |
最右 | 你本机那份还没过期 | 换无痕窗口或硬刷新,别急着 purge |
最后一层 hit,但 ttl 是负数 |
最右 | 陈旧兜底:源站当时不可用或明确允许 | 查源站可用性与 stale-* 策略 |
fwd=request |
该层 | 你的请求头不让用缓存(no-cache、max-age=0) |
去掉请求头复测,这通常不是故障 |
fwd=vary-miss |
该层 | 存了,但按 Vary 选不中 |
查 Vary 列的头与你请求的头 |
fwd=uri-miss |
该层 | 这层没有这个 URI 的响应 | 查是否刚被清、是否换了查询串 |
fwd=stale; fwd-status=304 |
该层 | 回源验证过,源站说「没变」 | 内容确实是旧的,但源站认为没变——查发布是否真的改了源站 |
fwd=miss 且没有 stored |
该层 | 回了源,但这层不打算存 | 查 no-store/private/配置 |
fwd=miss; stored |
该层 | 回了源并存下了 | 下一个请求应该命中;不命中就看键与 Vary |
| 三层只剩一层 | 最左 | 上游没发,或被删了 | 直接对源站发一次请求,看它发不发 |
同一成员上同时有 hit 与 fwd |
该成员 | 数据异常(第四节) | 怀疑拼接或改写,不要据此下结论 |
十一、十分钟核对:四组命令
先说边界:curl 每次都是全新请求,没有本地缓存。它能告诉你「这一层的响应里写了什么」,不能告诉你「你的浏览器会怎么处理」。浏览器那一层要在开发者工具的网络面板里看,或者看 Cache-Status 最后一个成员。
第一组:这层到底发不发这个头
curl -sS -o /dev/null -D - https://example.com/ \
| grep -iE '^(HTTP/|cache-status:|age:|cache-control:)'
-D - 把响应头打到标准输出。没有 cache-status: 这一行,本身就是信息(第九节的四种解释),不要跳到「没缓存」这个结论。
第二组:连取三次,看 ttl 与 Age 怎么动
for i in 1 2 3; do
echo "== $i"
curl -sS -o /dev/null -D - https://example.com/pricing \
| grep -iE '^(age|cache-status):'
sleep 5
done
判读:ttl 每次减少约 5,说明这一层真的存着并在倒计时;ttl 一直是同一个值、Age 也不动,说明每次都回源或者根本没存;ttl 突然变大,说明中途被重新取过(或者你打到了另一台边缘节点——这一点本文不做推断)。
第三组:故意让缓存别用它
curl -sS -o /dev/null -D - \
-H 'Cache-Control: no-cache' https://example.com/pricing \
| grep -iE '^(HTTP/|cache-status:|age:)'
如果这一层尊重请求指令,应该看到 fwd=request(或者 fwd=stale,取决于它选择验证还是重取)。这一步的价值是给你一个已知原因的对照样本:知道「回源长什么样」,才能认出「不该回源却回源了」。
第四组:换一个键,看是不是 uri-miss
curl -sS -o /dev/null -D - \
"https://example.com/pricing?cb=$(date +%s)" \
| grep -iE '^(HTTP/|cache-status:)'
第一次带随机串的请求,绝大多数实现会给出 fwd=uri-miss(查询串进了键)。如果它仍然 hit,说明这一层的键不含查询串——那么「加个参数绕过缓存」这条经验在这层就不成立(编辑判断)。
读的时候守住三条:① 顺序是源站 → 用户;② hit 与 fwd 不会同时出现在一个成员上;③ ttl 是剩余寿命,负数代表陈旧。
十二、上线前十二项清单
- 这个头是谁发的?先对源站本身发一次请求,确认源头有没有。
- 每一层的标识字符串分别是什么?能不能对应到具体产品?
- 成员顺序对不对?第一个是不是最靠近源站的那层?
- 有没有哪一层把上游的值整体重写了(
SHOULD preserve只是建议)? - 打开它会不会把
key暴露给所有人?按 §6 决定「不发/只发已授权/只带非敏感参数」。 - 生产环境要不要常开?还是只在带调试开关的请求上开?
- 你们读它的脚本,是不是把「没有这个头」当成了「没有缓存」?
- 分诊表里
fwd=request与fwd=stale是不是分开处理了? - 看到
fwd=stale; fwd-status=304时,团队会不会误判成「缓存没更新」? ttl为负时,监控会不会报警?它是正常状态还是故障,先定义清楚。- 两层
ttl会不会被拿去相加?Age才是各层之和。 - 换 CDN 时,这套读法还能不能用?依赖的是标准头还是某家的私有头?
十三、六种误读,与本周能做的五件事
| 误读 | 原文怎么说 |
|---|---|
| 「所有缓存都会发这个头」 | Caches determine when it is appropriate to add;可能默认不发、只在调试模式下发 |
| 「按响应里的顺序就是从用户到源站」 | 第一个成员 closest to the origin server,最后一个 closest to the user |
「hit 就是 200」 |
304/206 被缓存改写也算 hit;源站不可用时的陈旧兜底也算 hit |
「ttl 是总寿命」 |
remaining freshness lifetime,而且 may be negative |
「fwd 和 hit 可以一起读」 |
"hit" and "fwd" are exclusive,同时出现按异常数据处理 |
「detail 两层一样就代表同一个意思」 |
always specific to the cache that sent it;同名不同义是正常的 |
还有一条不在这张表里、但同样常被说错:「加了它就能看到缓存键」。key 是可选参数、只是「某个表示」(may be implementation specific),而且 §6 明确它可能帮助缓存投毒。没有 key 是常态,不是配置错了。
本文不写:各家 CDN、反向代理与浏览器对这个头的支持情况、默认是否开启、开启方式与字段长度上限;
ttl的具体算法与各实现的启发式比例;任何命中率、延迟与事故时长数字。这些都没有一手来源,写出来就是猜。
本周能做的五件事:
- 对源站直接发一次请求,确认这个头到底存不存在——先分清「不支持」和「没开」。
- 把你现在依赖的私有状态头(例如某家的
CF-Cache-Status)和Cache-Status并排记一次,看哪些结论是标准头也能得出的。 - 把第十节的分诊表贴进发布检查单,特别是
fwd=request与fwd=stale要分开写。 - 检查读这个头的脚本里,有没有把「头不存在」当成「未命中缓存」的分支。
- 如果打算在生产常开,先按 §6 决定
key要不要发给所有人。
边界说明
- 本文只引用 RFC 9211、RFC 9111、RFC 8941 与两份 IANA 注册表的原文;每处英文引用都可在对应页面检索到。标注为「编辑判断」「编辑推论」的段落是从原文推出来的工程结论,不是规范原文。
- 不讨论任何具体 CDN、反向代理、浏览器或框架对这个头的实现差异、支持比例、默认开关与配置项名称。这些需要各家官方文档,本文没有取证。
- 不讨论
ttl的具体算法、启发式的具体比例,也不给任何命中率、延迟或事故时长数字。 - 文中的
example.com、各段命令与示例头部均为占位示例,不代表任何真实站点的配置。
来源
- RFC 9211: The Cache-Status HTTP Response Header Field — https://www.rfc-editor.org/rfc/rfc9211.txt核查于 2026/11/3(2026-11-03 访问;§1.1 术语与 Structured Fields 的引用、§2 列表形状/顺序/标识类型/何时添加/本地生成响应不追加/保留上游值、§2.1
hit与三种情形及互斥、§2.2 八个fwd原因与「最具体优先」、§2.3fwd-status的默认值与用途、§2.4ttl为剩余寿命且可为负、§2.5stored、§2.6collapsed与两个示例、§2.7key、§2.8detail、§3 全部示例、§4 扩展注册与「通用优先」、§5 IANA 登记、§6 安全考量与三条缓解) - RFC 9111: HTTP Caching — https://www.rfc-editor.org/rfc/rfc9111.txt核查于 2026/11/3(2026-11-03 访问;§2 缓存键的构成下限、§3 可缓存性、§4 复用条件与「合并请求」、§4.2.1 寿命计算顺序、§4.2.2 启发式不给具体算法、§4.2.3
Age是各层停留时间之和、§5.1Age的语义与「缺失不代表回源」) - IANA: Hypertext Transfer Protocol (HTTP) Field Name Registry — https://www.iana.org/assignments/http-fields/核查于 2026/11/3(2026-11-03 访问;
Cache-Status为permanent,类型List,Reference「RFC 9211: The Cache-Status HTTP Response Header Field」) - IANA: Hypertext Transfer Protocol (HTTP) Cache-Status — https://www.iana.org/assignments/http-cache-status核查于 2026/11/3(2026-11-03 访问;
hit/fwd/fwd-status/ttl/stored/collapsed/key/detail八行,类型分别为 Boolean/Token/Integer/Integer/Boolean/Boolean/String/String or Token,Reference 均为 RFC 9211;注册程序为 Expert Review) - RFC 8941: Structured Field Values for HTTP — https://www.rfc-editor.org/rfc/rfc8941.txt核查于 2026/11/3(2026-11-03 访问;§3.1.2 参数与「Boolean true 序列化时省略」、§3.3.6 Boolean 的
?1/?0与省略规则、§4.2 解析前必须把同名多行合并成一个逗号分隔的字段值)