入门指南
两个脚本同时改同一份配置,两边都回 200:写请求要先问「还是我读到的那个版本吗」
两份配置被两个脚本同时写,后到的覆盖了先到的,日志里两次都是 200——写请求没带前置条件。RFC 9110 把 If-Match 的用途写成防止 lost update:比较必须用强校验器,条件为假时 MUST NOT 执行,但回 412 只是 MAY。讲清 If-None-Match 的 * 形态、三条 MUST ignore 与五步判定顺序。
200 只说明这次请求被受理,不说明你的改动叠在了你读到的版本上。

先看答案
- 适合谁
- 用两个脚本或两个定时任务写同一份配置的人
- 在 API 里手写 PUT、又自己算了个哈希当 If-Match 的人
- 看到 412 就原样重试,然后开始怀疑人生的人
- 你会获得
- 写请求要带的是「我读到的那个版本」,不是一个新算出来的哈希。
- 发布时间
- 实质更新
- 发布
- AOPC 编辑部
60 秒结论
一分钟看懂这篇文章
200 只说明这次请求被受理,不说明你的改动叠在了你读到的版本上。
人负责
- 分清「请求成功」与「改动生效」
- 判断该用 If-Match 还是 If-None-Match
AI 负责
- 从响应头取 ETag 并原样回写
- 把 412 与「已生效的 2xx」分开处理
- 确认是不是丢更新
- 查有没有前置条件
- 核对校验器强弱
- 改成可判定的写
直接答案
「两个脚本都会改 config.yaml。昨晚两个都跑了,日志里都是 200 OK,可早上打开文件,只剩后一个脚本写的内容。」
这不是「后一个脚本写错了」。它写的是它读到的那份加上自己的改动——问题是它读完之后、写之前,另一个脚本已经改过一次了。两边谁都没问一句:我手里这个版本还是当前版本吗?
HTTP 为这句话准备了前置条件。RFC 9110 在条件请求的引言里就把用途写明:
Conditionals can also be applied to state-changing methods, such as PUT and DELETE, to prevent the “lost update” problem: one client accidentally overwriting the work of another client that has been acting in parallel.
If-Match 那一节说得更直接:
If-Match is most often used with state-changing methods (e.g., POST, PUT, DELETE) to prevent accidental overwrites when multiple user agents might be acting in parallel on the same resource (i.e., to prevent the “lost update” problem).
这里有个容易滑过去的词:prevent accidental overwrites。它防的是「覆盖」,不是「写失败」。所以它的失败形态不是报错,而是没有报错——一句话都不说,你的改动就没了。
| 你发的东西 | 源站该做什么 | 冲突时你看到什么 |
|---|---|---|
| 什么都不带 | 直接执行方法 | 200,两个都 200,后到的赢 |
If-Match: "v1" |
先比当前校验器,再决定执不执行 | 通常 412;也可能是 2xx(见第三节) |
If-None-Match: * |
只在资源当前没有表示时执行 | 已存在则 412 |
If-Unmodified-Since: <日期> |
按修改时间判定,且可能被忽略 | 412 或照常执行 |
三句话记住:
If-Match比的是「实体标签」,不是你算出来的内容哈希——原文叫它opaque validator;- 比较必须用强比较,所以当前表示只要挂着弱 ETag(
W/"..."),If-Match就永远不成立; - 条件为假时源站
MUST NOT执行方法,但回不回 412 只是MAY——所以「没收到 412」不等于「我的改动生效了」。
最后一节给一组能自己跑的核对命令。先说清它们能证明什么、不能证明什么。
一、If-Match 问的不是「内容一样吗」
它的定义是这么写的:请求方法以「源站有当前表示」为条件——当字段值是 * 时;或者以「当前表示的实体标签匹配列表里某一个」为条件。
换成操作语言:
If-Match: "v1"问的是:目标资源此刻的表示,其 ETag 是不是"v1";- 它不问「内容有没有被我改过」,也不问「文件大小对不对」;
If-Match: *问的是另一件事:目标资源现在有没有任何当前表示。有就通过,没有就不通过。
最后这条常被读反。* 不是「任意版本都行」,而是「得先存在」。所以对一个还不存在的路径发 PUT 加 If-Match: *,条件为假——按 §13.1.1 的求值步骤第 3 条,其余情况一律为假。它挡住的是「我以为在改,其实在建」这种事故。
反过来,创建场景要用 If-None-Match: *,含义是「我相信它还不存在」,第六节讲。
再看一眼语法,有个容易被生成器写出来的非法形态:
Note that an If-Match header field with a list value containing “” and other values (including other instances of “”) is syntactically invalid (therefore not allowed to be generated) and furthermore is unlikely to be interoperable.
If-Match: *, "v1" 不是「更宽松的匹配」,它根本不该被生成。
二、比较必须是强校验器
这是 If-Match 与缓存校验最大的分野。原文没有留余地:
An origin server MUST use the strong comparison function when comparing entity tags for If-Match (Section 8.8.3.2), since the client intends this precondition to prevent the method from being applied if there have been any changes to the representation data.
而强比较的定义,恰恰把弱标签排除在外:
“Strong comparison”: two entity tags are equivalent if both are not weak and their opaque-tags match character-by-character.
规范自己给了一张四行对照表(§8.8.3.2 的 Table 3):
| 你手里的 ETag | 当前的 ETag | 强比较 | 弱比较 |
|---|---|---|---|
W/"1" |
W/"1" |
不匹配 | 匹配 |
W/"1" |
W/"2" |
不匹配 | 不匹配 |
W/"1" |
"1" |
不匹配 | 匹配 |
"1" |
"1" |
匹配 | 匹配 |
从这张表能直接读出一个事故形态:如果源站对所有表示都只发弱 ETag,那么任何 If-Match 都不可能成立。不是因为你的值写错了,而是强比较的第一条就要求「both are not weak」。
这不是规范漏洞,是有意的取舍。§8.8.1 说得很清楚:弱校验器「easy to generate but are far less useful for comparisons」,而强校验器「ideal for comparisons but can be very difficult (and occasionally impossible) to generate efficiently」。规范选择了不在写路径上妥协:
A strong validator is unique across all versions of all representations associated with a particular resource over time.
注意这句话的范围:同一个资源、所有版本、所有表示、跨时间唯一。它没说跨资源唯一——原文紧接着补了一句,同一个强校验器可以同时用在多个资源的表示上,且不意味着那些表示等价。
编辑推论:如果你的资源只能生成弱 ETag(比如按修改时间秒级截断、或按内容协商分组),那么在这条资源上做 compare-and-swap 是做不到的。可选的路只有两条——换成能生成强校验器的方案,或者在应用层用带版本号的字段自己做判定。规范没有为这种情况提供「弱一点的 If-Match」。
三、条件为假:不许执行,但 412 不是唯一出口
这一段是全文最容易被读成「带了就安全」的地方。原文先给了硬约束:
An origin server that evaluates an If-Match condition MUST NOT perform the requested method if the condition evaluates to false.
紧接着是出口,用的是 MAY:
Instead, the origin server MAY indicate that the conditional request failed by responding with a 412 (Precondition Failed) status code.
Alternatively, if the request is a state-changing operation that appears to have already been applied to the selected representation, the origin server MAY respond with a 2xx (Successful) status code
括号里的解释值得抄完:客户端可能不知道改动其实已经成功,也许因为上一次响应丢了,也许因为等价改动已经被别的客户端做过。
于是同一个「条件为假」,线上可能长成三种样子:
| 你看到的 | 源站做了什么 | 你的改动生效了吗 |
|---|---|---|
412 |
判定为假,且选择用 412 表示失败 | 没有 |
2xx |
判定为假,但认为改动「看起来已经生效」 | 说不清——生效的可能是别人的等价改动 |
2xx |
条件为真,正常执行 | 是 |
这就是「两次都是 200 也可能丢更新」的第二条来源:即使你带了 If-Match,一个 2xx 也不能证明「我的改动叠在了我读到的那版上」。
同一份规范里还有一处措辞强度不同,值得单独标出来。§13.2.2 的五步顺序里,第一步写的是:
if false, respond 412 (Precondition Failed) unless it can be determined that the state-changing request has already succeeded (see Section 13.1.1)
respond 412 是祈使句,unless 只留了「已生效」一个例外;而 §13.1.1 用的是 MAY indicate。两处并不冲突——§13.2.2 规定的是求值顺序与分支,§13.1.1 规定的是响应选择——但强度读起来不一样。工程上稳妥的读法是:把 412 当「确定没执行」,把 2xx 当「不确定」,别把「没收到 412」当成功凭据。
编辑判断:这也是为什么「重试」不能写成「再发一次同样的请求」。同一条
If-Match重发,如果第一次其实已经生效,第二次会拿到 412(条件为假),你会误判成「没成功」;如果中间有人做了等价改动,第二次可能拿到 2xx,你又误判成「成功了」。两种误判方向相反,靠状态码本身分不开。
四、两次 200 也能是丢更新
把前面几条拼起来,同一个「后到的覆盖了先到的」至少有四种成因,而它们在你日志里的样子可能完全一样:
| 成因 | 日志长相 | 判定线索 |
|---|---|---|
| 写请求没带任何前置条件 | 两次 200 |
请求头里没有 If-Match |
| 带了,但服务器不实现/忽略 | 两次 200 |
换一个明显错的 ETag 去试,仍 200 |
| 带了,第二次落在「已生效」的 2xx 分支 | 两次 200 |
响应里可能缺 ETag(见下) |
| 中间层吃掉了这个头 | 两次 200 |
源站访问日志里看不到 If-Match |
第三种有个细节:§13.1.1 提到,在某些场景下从成功响应里省掉 ETag 反而更好——它会促使客户端下一步去做 GET,从而消除「资源现在到底是什么状态」的困惑。所以「成功响应没有 ETag」不是 bug,可能是有意的。
第四种来自一句常被忽略的许可:
A cache or intermediary MAY ignore If-Match because its interoperability features are only necessary for an origin server.
也就是说,前置条件的判定责任在源站。你在网关、CDN、反向代理上配的那一层,可以选择不参与。这不代表它一定会吃头,但代表「我在代理层加了 If-Match,所以并发安全了」不是一个能成立的推论。
还有一层风险,规范自己在讲「已生效的 2xx」时点出来了:
multiple user agents writing to a common resource as a semaphore (e.g., a nonatomic increment) are likely to collide and potentially lose important state transitions
对这类资源,原文的建议是「better off being stringent in sending 412 for every failed precondition on an unsafe method」。换句话说:能不能回 2xx 放过,取决于这个资源经不经得起「看起来已经生效」的误判。计数器、状态机、库存这类东西经不起。
五、写方法上的 If-None-Match:* 是「只在不存在时创建」
If-None-Match 更常出现在 GET 上(缓存校验,304),但它在写路径上有一个很实用的形态:
If-None-Match can also be used with a value of “*” to prevent an unsafe request method (e.g., PUT) from inadvertently modifying an existing representation of the target resource when the client believes that the resource does not have a current representation (Section 9.2.1).
紧接着一句把它归了类:
This is a variation on the “lost update” problem that might arise if more than one client attempts to create an initial representation for the target resource.
注意这里的比较函数与 If-Match 相反,是弱比较:
A recipient MUST use the weak comparison function when comparing entity tags for If-None-Match (Section 8.8.3.2), since weak entity tags can be used for cache validation even if there have been changes to the representation data.
所以 If-None-Match: W/"abc" 与当前的 "abc" 算匹配。这一条与 If-Match 的强比较放在一起看,很容易记混:同一个 ETag,在「防止覆盖」和「确认没变」两个方向上,用的不是同一个比较函数。
条件为假时的出口也不同,这里没有 MAY:
An origin server that evaluates an If-None-Match condition MUST NOT perform the requested method if the condition evaluates to false; instead, the origin server MUST respond with either a) the 304 (Not Modified) status code if the request method is GET or HEAD or b) the 412 (Precondition Failed) status code for all other request methods.
两个 MUST,中间夹着「按方法分岔」。这就是第七节那张顺序表里 304 与 412 的唯一分界:方法,不是「谁的条件失败」。
六、If-Unmodified-Since 是退路,不是等价物
如果没有 ETag,规范给了一条基于时间的退路。原文承认它「accomplishes the same purpose as If-Match」,但紧接着列了三条一定会被忽略的情形:
A recipient MUST ignore If-Unmodified-Since if the request contains an If-Match header field
A recipient MUST ignore the If-Unmodified-Since header field if the received field value is not a valid HTTP-date (including when the field value appears to be a list of dates).
A recipient MUST ignore the If-Unmodified-Since header field if the resource does not have a modification date available.
第一条最要紧:两个都发,时间那个直接作废。它不是「双保险」,是多发了一个不参与判定的头。这也是为什么「我时间条件也带了,怎么还是被覆盖」这类问题的答案通常是——你自己把更准的那个条件废掉了。
还有一条时钟口径:
A recipient MUST interpret an If-Unmodified-Since field value’s timestamp in terms of the origin server’s clock.
客户端时钟快几秒、慢几秒,都不该影响判定;但这也意味着你没法用本地时间「校准」这个条件。
至于精度,§8.8.1 在解释弱校验器时提过 HTTP-date 的秒级分辨率问题;Last-Modified 只有一秒的粒度,同一秒内的两次修改在时间条件看来是「没变」。这一点与相邻那篇讲 If-Range 的文章是同一个来源:那边原文明确写了 If-Range 用精确相等、If-Unmodified-Since 用「早于或等于」,两者不能互换。
编辑判断:所以在能做 compare-and-swap 的资源上,
If-Unmodified-Since只适合当「资源根本没有 ETag」时的兜底。它能挡住跨秒的覆盖,挡不住同一秒内的两次写。
七、判定顺序:五步,304 与 412 的分岔只在方法
多个条件头同时出现时,顺序是写死的:
| 步 | 条件 | 为真 | 为假 |
|---|---|---|---|
| 1 | 源站且 If-Match 存在 |
进入第 3 步 | 412,除非能判定改动已生效 |
| 2 | 源站、无 If-Match、有 If-Unmodified-Since |
进入第 3 步 | 412,同上例外 |
| 3 | If-None-Match 存在 |
进入第 5 步 | GET/HEAD → 304;其它方法 → 412 |
| 4 | GET/HEAD、无 If-None-Match、有 If-Modified-Since |
进入第 5 步 | 304 |
| 5 | GET 且 Range 与 If-Range 都在 |
按 Range 回 206 |
忽略 Range,回 200 |
三处能直接落到排障上:
- 第 1 步和第 2 步是互斥的:有
If-Match就不会走到时间条件; - 第 3 步的 304 只属于
GET/HEAD:同一个「条件为假」,写方法拿 412,读方法拿 304。你看到 412 就断言「有人改了我的数据」是不够的——If-None-Match: *命中也回 412; - 第 4 步只在没有
If-None-Match时才走:这也是相邻那篇里「发了If-None-Match,Last-Modified就不参与判定」的出处。
还有一句容易被忽略的兜底,写在 §13.1 的开头:
Regardless, if the mapping is inconsistent or the server is unable to select an appropriate representation, then no harm will result when the precondition evaluates to false.
意思是:前置条件判错方向时,最坏结果是「这次不写」,不是「写坏」。这正是它比「先读后写」可靠的地方——后者在两次操作之间有一个窗口,前者没有。
八、412 到底证明了什么
看定义:
The 412 (Precondition Failed) status code indicates that one or more conditions given in the request header fields evaluated to false when tested on the server (Section 13).
逐词读:one or more conditions——它不告诉你是哪一条;evaluated to false——它说的是判定结果,不是「有人改过」。所以:
- 412 证明方法没有因为这次请求被执行;
- 412 不证明「存在并发写」——
If-None-Match: *命中、日期条件失败、甚至你发了一个源站从来没发过的 ETag,都会得到 412; - 412 不证明「重发就会成功」——如果条件本身永远不成立(比如第二节那个弱 ETag 的情形),重发只会一直 412。
编辑判断:把 412 记成「我的前置条件没通过」比记成「冲突」更准。冲突只是其中一种原因,而且是唯一一种「再读一次就能解决」的原因。
九、重试之前要重新取校验器
到这里,「该怎么办」其实已经被规范推出来了,剩下的是工程顺序。下面这一段是编辑建议,不是规范原文:
- 收到 412,先
GET目标资源,读回当前的ETag; - 把你的改动重新应用到新读到的内容上(不是把上次的请求体重发一遍);
- 用新的
ETag生成新的If-Match,再发一次; - 仍然 412,就退避并限制次数——这说明有持续的并发写,或者你的校验器来源不对。
第 2 步是最容易省掉的,也是最容易出错的:把旧请求体重发一遍,等于「用旧内容覆盖新内容」,只是这次是「合法地」覆盖——条件成立了,改动也生效了,别人的改动照样没了。
第 4 步的退避只能减少碰撞,不能消除。规范没有给「重试几次合适」的数字,本文也不编。
还有一个方向性问题:不要自己算哈希当 If-Match。实体标签是不透明的:
An entity tag is an opaque validator for differentiating between multiple representations of the same resource, regardless of whether those multiple representations are due to resource state changes over time, content negotiation resulting in multiple representations being valid at the same time, or both.
Since the value is opaque, there is no need for the client to be aware of how each entity tag is constructed.
§8.8.3.1 确实把「对表示数据做抗碰撞哈希」列为一种合法的强校验器生成方式,但那是对源站说的。客户端拿自己算的哈希去当条件,只有当源站恰好也用同一种算法、同一个字节范围时才会匹配——这不是能依赖的前提。用错了的表现是「所有写都 412」,而不是「偶尔冲突」。
顺带一条:If-Match 不是只能用在写方法上。原文允许 GET 带它,但用途很窄:
A client MAY send an If-Match header field in a GET request to indicate that it would prefer a 412 (Precondition Failed) response if the selected representation does not match. However, this is only useful in range requests (Section 14) for completing a previously received partial representation when there is no desire for a new representation.
十、十分钟核对
下面几组命令只做一件事:判断你这套接口到底有没有在判定前置条件。示例域名与 ETag 都是占位值,请换成你自己的资源。
第一组:先看源站给不给 ETag
u=https://example.com/api/config
curl -sS -o /dev/null -D - "$u" | grep -iE '^(etag|last-modified|vary):'
没有 ETag 也没有 Last-Modified,If-Match 就没有可用的值(除了 *),后面几组也不必跑。
第二组:用真 ETag 回写,看它认不认
etag=$(curl -sS -o /dev/null -D - "$u" | grep -i '^etag:' | tr -d '\r' | cut -d' ' -f2-)
curl -sS -o /dev/null -D - -X PUT "$u" \
-H 'Content-Type: application/json' \
-H "If-Match: $etag" \
--data '{"probe":1}' | sed -n '1p'
第三组:故意用错的 ETag,看它拦不拦
curl -sS -o /dev/null -D - -X PUT "$u" \
-H 'Content-Type: application/json' \
-H 'If-Match: "definitely-not-the-current-tag"' \
--data '{"probe":2}' | sed -n '1p'
第四组:两个请求抢同一个 ETag
for i in 1 2; do
curl -sS -o /dev/null -w "req$i=%{http_code}\n" -X PUT "$u" \
-H 'Content-Type: application/json' \
-H "If-Match: $etag" \
--data "{\"writer\":$i}" &
done
wait
判读:
| 看到什么 | 说明 | 下一步 |
|---|---|---|
第一组没有 ETag |
没有可用的校验器 | 先让源站发 ETag,或改用应用层版本号 |
第二组 2xx、第三组 412 |
前置条件在工作 | 正常;冲突处理按第九节 |
第二组 412、第三组 412 |
你的 ETag 与源站判定口径不一致,或当前表示是弱校验器 | 看 ETag 有没有 W/ 前缀 |
第三组也是 2xx |
源站没有判定这个头 | 写安全性不能靠它;要么改源站,要么在应用层加锁 |
第四组两个都 2xx |
同上,或者两次条件都「碰巧」成立 | 检查响应里的 ETag 是否每次都在变 |
第四组一个 2xx 一个 412 |
这是 If-Match 该有的样子 |
记下 412 的那次,确认它没有写进去 |
第三组是这组里最关键的一条:用它区分「条件失败」和「条件没被读」。只跑第二组,你分不出「服务器判定通过」和「服务器根本没看这个头」——两种都是 200。
第四组要在测试资源上跑。它会真的写两次,别对着生产配置做。
十一、上线前十二项清单
- 两个会写同一资源的进程,会不会真的同时跑?先确认这件事存在,再谈加锁。
- 写请求里有没有
If-Match?没有的话,任何「后写覆盖先写」都不是 bug,是设计。 If-Match的值是从上一次响应的ETag头里取的,还是你自己算的?- 那个
ETag有没有W/前缀?有的话,If-Match永远不会成立。 - 你的资源有没有可能根本不发
ETag?那就先解决这个,再谈条件写。 - 收到 412 之后,是「重发旧请求体」,还是「重新读、重新算、再写」?
- 重试有没有次数上限?退避策略是不是只减少碰撞、不消除?
- 创建型写入用的是
If-None-Match: *还是If-Match: *?两者含义相反。 - 有没有同时发
If-Match和If-Unmodified-Since?后者会被忽略,别把它当保险。 - 只靠
If-Unmodified-Since的资源,能不能接受同一秒内的两次写被视为「没变」? - 依赖前置条件的那一层,是源站还是中间的代理/网关?后者可以忽略它。
- 把「2xx」当成「我的改动生效」的地方,改得掉吗?至少日志里要能分开这两种。
七种误读
| 误读 | 原文怎么说 |
|---|---|
「带了 If-Match 就一定安全」 |
条件为假时确实 MUST NOT 执行,但回 412 只是 MAY,还可以回「已生效」的 2xx |
| 「412 一定代表有人改过」 | 412 只说「一条或多条条件判定为假」,If-None-Match: * 命中也回 412 |
| 「ETag 就是内容哈希」 | An entity tag is an opaque validator;哈希只是源站可选的生成方式之一 |
「If-Unmodified-Since 能替代 If-Match」 |
请求里含 If-Match 时,收件方 MUST ignore 时间条件 |
| 「弱 ETag 也能做条件写」 | 强比较要求 both are not weak;W/"1" 与 "1" 判为不匹配 |
| 「412 直接重试就好」 | 重试前要重新取校验器;条件永远不成立时,重试只会一直 412 |
| 「代理层加了条件头就并发安全」 | A cache or intermediary MAY ignore If-Match,判定责任在源站 |
本文不写:各家框架、对象存储、数据库对前置条件的默认实现,它们是否支持
If-Match、支持到什么程度,以及任何并发性能、冲突率、重试次数的数字。这些都需要各家官方文档或实测数据,本文没有取证。
本周能做的五件事:
- 找出所有「两个进程写同一份东西」的地方,看它们有没有带前置条件。
- 挑一个接口跑第十节的第三组命令——确认源站是真的在判定,而不是默默忽略。
- 检查你手里的
ETag有没有W/前缀,有就从条件写的方案里划掉。 - 把 412 的处理逻辑从「原样重试」改成「重读 → 重算 → 再写」。
- 在日志里把「2xx 且条件为真」和「2xx 但可能已生效」分开记,别混成一个成功。
边界说明
- 本文关于前置条件、比较函数、判定顺序与状态码的陈述,全部来自 RFC 9110 的原文;每处英文引用都能在 https://www.rfc-editor.org/rfc/rfc9110.txt 检索到。IANA 字段注册表只用于确认这三个字段名是
permanent注册项及其对应小节。 - 标注「编辑判断」「编辑推论」的段落是从原文推出的工程结论,不是规范原文。特别是第九节的重试顺序、第十节的命令组合与判读口径、第十一节清单的排序。
- 不讨论具体框架、语言运行时、对象存储、数据库对前置条件的实现与默认行为,也不给任何并发、冲突率、重试次数或耗时的数字。
- 不重复讲缓存侧的条件请求(
GET→ 304 与弱比较)与范围请求(If-Range与精确相等);两者分别在相邻的两篇里。 - 文中
example.com、config.yaml与各 ETag 值均为占位示例,不对应任何真实站点或真实资源。
来源
- RFC 9110: HTTP Semantics — https://www.rfc-editor.org/rfc/rfc9110.txt核查于 2026/11/3(2026-11-03 访问,HTTP 200,502 941 B;§8.8.1 强/弱校验器定义与
Strong validators are usable for all conditional requests的上下文、§8.8.3opaque validator与opaque-tag语法、§8.8.3.1 四种生成方式与Since the value is opaque、§8.8.3.2 强/弱比较定义与 Table 3、§9.2.1 安全方法的范围、§13 条件请求引言与lost update、§13.1 前置条件总述与「判定为假时最坏只是不写」、§13.1.1If-Match全文(强比较MUST、*的求值、MUST NOT perform、MAY412、已生效的 2xx、非原子semaphore警告、GET上的窄用途、中间层MAY ignore、含*的列表非法)、§13.1.2If-None-Match(弱比较MUST、*的创建语义、两个MUST与 304/412 分岔)、§13.1.4If-Unmodified-Since(三条MUST ignore、源站时钟口径、与If-Match同目的)、§13.2.2 五步顺序、§15.5.13412 Precondition Failed定义) - IANA: Hypertext Transfer Protocol (HTTP) Field Name Registry — https://www.iana.org/assignments/http-fields核查于 2026/11/3(2026-11-03 访问,HTTP 200,83 957 B;
If-Match→permanent、RFC 9110 §13.1.1,If-None-Match→permanent、§13.1.2,If-Unmodified-Since→permanent、§13.1.4)