摘要

  • 428 Precondition Required 允许源站拒绝没有携带所需条件的状态变更请求,把“客户端最好谨慎”变成“源站可以强制要求写入依据”。
  • 典型做法是让客户端用 If-Match 带回强实体标签。428 表示应有的条件没有提交,412 则表示已经提交的条件经判断为假。
  • 428 不提供锁、合并或事务。真正的安全还取决于可靠的验证器、比较与提交的原子性,以及客户端在冲突后重新协调内容,而非只换一个新标签。

两次成功如何丢掉一次修改

两位编辑同时打开版本七。甲先改正一段并保存,服务器生成版本八。乙仍停留在版本七,又以整页替换的方式保存。若服务器只检查请求格式和权限,两次操作都可能返回成功,但甲的改动已经从最终内容中消失。

这就是“丢失更新”。它不需要攻击者,也不要求网络出错。问题在于第二次请求只说了“把这份内容写到这里”,没有说“只有服务器仍处于我阅读过的那个状态时才写”。目标和载荷并不足以描述一次安全写入;决定赖以成立的过去同样重要。

HTTP 很早就有条件请求。源站可以给表示分配实体标签,客户端也可以把标签带回来。缓存验证让这套机制广为人知:副本仍有效时,服务器用 304 省去响应正文。但同一种证据还能承担更严格的职责——防止写入建立在过期状态上。

2012 年 4 月发布的 RFC 6585 为这种政策增加了专门状态码。428 表示源站要求请求必须是有条件的;规范给出的典型用途正是阻止客户端把已经被第三方修改的资源覆盖掉。

428 把谨慎变成入场条件

如果客户端读取资源时得到强实体标签 "v7",之后以 If-Match: "v7" 提交 PUT,它声明的不只是一个字符串,而是自己的决策基础。源站只有在当前选定表示仍与 "v7" 强匹配时才执行方法。若另一位编辑已把状态推进到 "v8",条件为假,请求的变更不能发生。

没有 428,周到的客户端可以自愿这样做;有了 428,源站可以把条件化写入设为资源的入场规则。RFC 6585 的示例建议客户端尝试 If-Match,并要求响应说明如何成功重新提交。

示例不意味着所有 428 都只能靠一个具体头字段解决。If-Match: * 可以声明“当前表示必须存在”;If-None-Match: * 可以声明“当前表示必须不存在”,适合防止并发创建时误覆盖。没有实体标签时,If-Unmodified-Since 也能提供基于时间的条件。源站应明确告诉客户端它接受哪一种证据,而不是让调用者猜测。

428 的历史意义因此不在某个头字段,而在权力分配:拥有资源现状的源站可以说,没有写明前提的请求无权开始产生效果。

缺少条件与条件失败不是一回事

428 和 412 相邻,却回答不同问题。

428 Precondition Required 是政策判断:这个资源要求有条件请求,但来件没有提供可接受的前提。源站在尝试状态变更之前拒绝它。

412 Precondition Failed 是事实判断:请求已经带来前提,源站把它与当前状态比较后发现为假。客户端说明了自己见过的过去,但那个过去已经不是现在。

把两者混为一谈会损失重要的运维信息。428 激增,通常说明某批客户端不知道或没有遵守并发契约;412 激增,则可能意味着会话过旧、多人竞争加剧,或者验证器因无关变化而频繁跳动。两种故障需要不同修复。

428 也不是 409 的别名。409 可以表达请求与资源当前语义状态的冲突;428 要求在源站冒险执行方法之前先带来规定证据。它更不是 WebDAV 的 423:这里没有宣告资源被锁住,也没有替客户端建立锁。

强比较为何不能降级

现行 HTTP 语义 RFC 9110 规定,If-Match 必须采用强比较。弱标签适用于“内容虽非逐字相同,但足够让缓存复用”的场景;写入者的意图却是只要表示数据发生变化就停止方法。把弱等价当成写入许可,会让需要保留的差异滑过去。

这使验证器生成成为公开 API 的一部分。受保护状态改变而标签不变,会放行过期写入;无关的序列化次序或渲染噪声也让标签改变,则会制造大量虚假冲突。一个服务若强制 If-Match,却不向客户端提供可用的强标签,428 就不是恢复路径,而是死路。

时间条件的精度较低。If-Unmodified-Since 在没有实体标签时可用于防止丢失更新,但 RFC 9110 认为 If-Match 更准确;两者同时出现时,接收方忽略日期条件。时钟行为、秒级精度和更新时间生成方式,都让日期无法自动等同于精确版本身份。

判断必须贴着提交发生

规范把条件判断放在源站:先做普通请求检查,再在执行方法内容或动作之前立即判断。中间缓存或代理不能替源站断言 If-Match 是否成立,因为它不拥有权威现状。它必须把请求送到真正能作决定的位置。

应用内部也必须维持同一边界。假设处理程序读取当前标签,确认匹配,随后释放数据库事务,过一会儿才写入。另一项变更可能在“检查”和“提交”之间插入。对外看似完成了条件请求,对内却重新制造了检查时与使用时的竞态。

正确实现是原子的“比较并行动”。版本列、compare-and-swap 或等效事务必须让前提判断与受保护的修改成为同一次权威决定。HTTP 只规定线上可观察的语义,不能替服务内部凭空创造原子性。

验证器的范围也要与被保护状态一致。渲染页面的标签未必能保护背后多个记录;一次同时改账户、发消息、触发外部工作流的请求,更跨越了多种状态边界。428 不能把这些边界自动变成一个事务。

拒绝冲突并不等于解决冲突

最危险的“恢复”方式,是客户端收到 412 后只获取一个新标签,却保留原来的整页旧正文,再次提交。新条件可以通过,但中间发生的修改没有经过人或合并算法的审视。显性的冲突被包装成了一次最新授权的覆盖。

合理恢复通常需要重新取得当前内容,把用户尚未提交的意图与之比较,再决定合并、放弃还是明确替换。有些资源可以用字段级补丁或可交换操作缩小冲突面;有些争议只能由理解业务语义的人判断。428 让这项工作无法再被无声跳过,却不替任何人完成它。

客户端也不能把全部保护责任交给服务器。RFC 6585 明确说这些状态码是可选的,并特别提醒:客户端不能依赖 428 的出现来避免所有丢失更新。若 API 提供条件写入,关心自身结果的客户端应主动携带条件,而不是等源站提醒。

428 不能被缓存成长期判决

RFC 6585 要求缓存不得存储 428 响应。它描述的是一次具体操作不符合当前入场政策,并不是目标资源可复用的表示。若缓存日后重放旧的 428,它可能在策略或状态已改变后仍阻挡请求,还会让中间层冒充源站作出授权判断。

有用的响应应说明怎样重新提交。只返回“需要前提条件”不足以帮助陌生客户端。服务还应暴露当前验证器,说明支持的条件,并提示调用者先读取、协调再重试。

今天的 IANA HTTP 状态码登记表 仍把 428 记为 Precondition Required,规范来源是 RFC 6585。这个短名字记录的不是普通错误,而是一次协议观念的改变:写入者必须公开自己的依据,才能获得替换现在的资格。

来源与证据边界

本文证据来自 RFC 6585、RFC 9110 与 IANA 登记表。它们确立了状态码、条件字段、判断顺序和明确限制;不能证明当前部署比例、所有客户端库的行为、具体事故损失或恰好一次执行。428 不是锁、数据库事务或合并协议。