摘要

  • Accept-Patch 列出某个资源接受的补丁文档媒体类型。它的出现说明该资源具备 PATCH 能力,却不会认证调用者、授予写权限、替客户端选择格式,或证明某次修改有效。
  • 稳健实现应把能力发现、补丁语言、状态前提和授权拆成四次判断。声明只能帮助准备请求;其他判断必须依据真正到达的请求及其上下文作出。

一次 OPTIONS 响应同时列出 application/json-patch+json 与 application/merge-patch+json。客户端由此得知服务器为该资源声明了两种补丁语法。它还不知道当前账户能否写入,不知道此前读取的版本是否仍然有效,也不知道具体操作会不会违反业务规则。它更没有拿到一张通行证。

误读往往来自声明的正式外观:字段由服务器发出,名称已经注册,又常与 Allow 并列。软件很容易把它们压缩成一个“可编辑”布尔值。但 RFC 5789 没有给予这种压缩依据;它在定义格式声明之外,另外要求服务器对 PATCH 请求进行授权。

资源能力不是个人权限

PATCH 的存在源于 PUT 与局部修改的差异。PUT 请求完整替换表示;PATCH 则携带一份补丁文档,由其媒体类型规定如何把指令施加到现有状态。目标 URI 指定资源,Content-Type 指定补丁语言,前提字段把操作约束在已观察的状态,认证和访问控制决定主体能否执行。

Accept-Patch 位于这些请求时判断之前。它是一组可带参数的媒体类型。RFC 5789 说,支持 PATCH 的资源应在 OPTIONS 响应中给出该字段;字段若出现在任何方法的响应中,就隐含表示该请求 URI 所标识的资源允许 PATCH,每个列出的媒体类型也表示该格式可用于该资源。

这里的“允许”描述的是资源层面的协议能力,不是对所有主体的授权。RFC 5789 的安全部分另行要求授权请求,并明确提到访问控制与认证。因此,公开 GET 可以携带同样的格式列表,而匿名 PATCH 仍然得到 401 或 403。两个账户看到的声明可以完全相同,实际可修改字段却彼此不同。

缺失也不能被过度解释。Allow 可以包含 PATCH 而没有 Accept-Patch;这时只公布方法,没有公布文档格式。Accept-Patch 出现在非 OPTIONS 响应里依旧传递隐含的方法能力。RFC 9110 又指出,实际允许的方法集合由源服务器在每次请求时决定,而且可能动态变化。因此,一次发现只是带时间的观察,不是能力租约。

媒体类型才规定程序

RFC 5789 不设所有实现都必须支持的默认补丁格式。服务器必须确保收到的文档适用于目标资源。已验证勘误 3169 把边界写得更清楚:PATCH 的具体语义来自请求媒体类型。仅因服务器会解析 application/json 或 application/xml,就把它们当作补丁语言,会把公共协议退化成资源私约。

RFC 6902 定义的 application/json-patch+json 使用有顺序的操作数组,其中包括 add、remove、replace、move、copy 与 test。RFC 7396 定义的 application/merge-patch+json 更像目标文档,通过比较确定新增和替换,并让 null 专门表示删除已有成员。

两者都处理 JSON,却不能互换。JSON Patch 的 test 能在操作序列内部校验一项具体假设;Merge Patch 对对象形数据很简练,但不适合依赖显式 null 的结构,也难以细致表达数组变化。服务器同时声明二者,并不表示客户端可以自动转写,更不表示列表顺序就是通用偏好。

客户端必须在 Content-Type 中明确选择。SDK 不应只取第一个值,也不应给已经构造好的正文换个标签。参数可能参与语义,而应用可能只具备正确生成其中一种格式的能力。

被拒绝之后,声明仍然只是说明

支持某种格式不等于本次请求必然成功。RFC 5789 列出彼此独立的失败条件:文档结构错误可用 400;目标不支持媒体类型可用 415;语法正确但无法处理可用 422;资源不存在且格式不能作用于空资源时可用 404;状态或并发冲突可用 409;明确前提不成立时用 412 最有帮助。

415 的设计最能说明 Accept-Patch 的职能。服务器拒绝刚刚收到的文档,同时应在响应中列出支持的替代格式。这个字段不会追认失败请求,不允许客户端只改 Content-Type 后原样重发,也不承诺另一份正确编码的文档会通过认证、授权、状态与业务检查。

客户端可以保留失败事实,向上层展示可用格式,再由本地逻辑判断能否构造语义等价的新请求。那是一项新的操作,将面对新的状态和权限环境。发现减少猜测,却不强制重试。

If-Match 保护状态,不证明身份

有些补丁以特定版本为基点。两个客户端从同一版本构造修改时,较晚执行的一份可能破坏已经变化的资源。RFC 5789 建议此类格式使用条件请求,并以 If-Match 携带强 ETag 为例。

RFC 9110 规定服务器在执行方法之前评估 If-Match,并使用强比较。客户端表达的是:只有当前表示仍与我观察的版本一致才执行。条件失败返回 412,从而避免服务器擅自猜测如何重新套用补丁。

ETag 不是凭据。无权修改的人可能知道最新值,有权修改的人也可能提交旧值。服务器必须分别评估访问权和状态前提,Accept-Patch 对二者都不给结论。

IANA 方法注册表把 PATCH 标为“不安全、非幂等”。RFC 5789 说明某项具体请求可以设计成幂等,但那取决于格式和操作。重复“追加”与重复带 test 的“替换”效果不同。格式声明无法预判未来文档的重试性质。

原子性从执行阶段开始

一旦请求获准执行,服务器必须原子地应用整份补丁。执行过程中不能向 GET 暴露半成品;只要文档无法全部完成,就不能留下其中任何变化。

RFC 6902 用失败的 test 操作展示这一点:整份 JSON Patch 都不产生改变。原子性是执行契约,不是 Accept-Patch 带来的权限。RFC 5789 还允许应用语义影响其他资源,因此提交边界要覆盖直接受影响对象。

合理的处理顺序是:认证主体、授权目标和操作、验证 Content-Type、解析对应语言、评估前提与业务约束、暂存变化,最后共同提交。读取 Accept-Patch 不会启动事务、预留版本或加锁。若把发现视作锁,就凭空发明了一套协议。

缓存响应实际修改,而不是响应发现

PATCH 属于不安全方法。RFC 9111 要求经过的缓存,在收到非错误响应后使目标 URI 的已存响应失效;同源 Location 与 Content-Location 也可成为候选,跨源失效则被禁止。

这些后果发生在真实的不安全请求之后。仅包含 Accept-Patch 的 OPTIONS 没有修改状态,不能成为清缓存命令。即使 PATCH 成功,失效也只影响请求经过的缓存,并非全球广播。格式声明既不扩大范围,也不证明所有读者已经看到新状态。

签名只保护声明本身

RFC 9421 可以让应用定义的签名方案覆盖 Accept-Patch。验证成功能够证明被覆盖字节的完整性,并在相应信任体系中关联签名密钥,但不会自动补上访问控制决定。

签名配置仍需规定必备组件、密钥解析、算法和策略。额外的授权协议可以把主体、方法、目标和内容绑定起来;那种效力来自额外协议,而不是格式列表。密码学保存一项声明,不扩张声明的含义。

保留可审计的分层

客户端至少应分开保存:资源 URI 与观察时间、Allow 或 Accept-Patch 所表达的方法能力、完整媒体类型及参数、构造补丁所依据表示的验证器,以及新请求使用的身份与策略环境。界面可以把这些信息组合成操作流程,数据模型不应把它们抹成一个字段。

服务器也可让 OPTIONS 描述相对稳定的协议能力,在 PATCH 真正到达时再认证、授权、核对类型、解释对应语言、评估 If-Match、检查业务规则并原子提交。415 可以提供下一步信息,却不担保下一步结果。

测试应覆盖交叉情形:Allow 有 PATCH 而无列表;GET 响应携带列表;匿名与授权账户看到同一列表;If-Match 已过期;通用 JSON 被拒绝;两种 JSON 补丁故意产生不同效果;test 失败后零变化;签名完好的声明之后仍返回 403。

当前 RFC 5789 勘误页列出三项已验证和一项已拒绝。5521 从 204 示例中移除了 Content-Location 及相关说明,旧示例不能作为现行策略依据。7513 只修复内部链接,没有改变方法分类;被拒绝的 3419 也不能改写标准。

来源