摘要

  • 已过期归档的 HTTPAPI Idempotency-Key 草案,把键的生命周期与到期政策放在资源方契约中。Stripe 与 AWS 的官方材料显示了不同保留边界,不能据此推导所有服务都有一个通用有效期。
  • 去重记忆与产生业务后果的授权,应有不同生命周期。窗口外的一项旧操作若结果不明,应查询、核对或暂停,而不是因为服务器查不到旧键,就自动变成一项新的执行指令。

一次超时告诉客户端的事情很少。它知道自己没有收到答复,却未必知道服务器没有开始、仍在处理中,还是刚完成就断了连接。若操作还跨越另一个服务,本地失败甚至可以与远端成功同时发生。表面上同样是一条超时提示,下一步该做什么却完全不同。

幂等机制试图减少这种不确定性。调用方给请求一个身份,服务在承诺范围内识别同一请求的再次到达,返回旧结果或说明正在进行,而不是重新产生效果。关键条件不只是客户端保留了一个随机字符串,还包括服务能否继续记得这项身份、怎样比较载荷,以及记多久。

设想一个任务执行器发送指令后失去答复,随后离线几天。它恢复时仍持有原请求和原幂等键。然而服务器可能已经按保留政策清掉对应条目。同样的字节回来,去重层却可能把它当成首次请求。客户端没有明确作出第二次决定;改变的是另一端的记忆。

这就是“安全重试”最容易遗漏的边界。识别窗口有期限,已经发生的后果却可能更长久。一个资源可能仍占用容量,一项指令可能已传给外部对象,一次披露也许已不可收回。键不存在,不等于旧效果不存在,更不等于有权再做一次。

draft-ietf-httpapi-idempotency-key-header-07 对这项契约有较清楚的说明。第 07 版于 2025 年 10 月 15 日发布,提议以结构化头字段中的字符串携带键,规定唯一性,并允许资源方生成载荷指纹,区分完成后的重复与执行中的重复。键的生命周期和适用到期政策由资源方负责说明。

必须同时说明它的现状:第 07 版已于 2026 年 4 月 18 日过期,Datatracker 将其列为 HTTPAPI 工作组的过期归档 Internet-Draft。它不是仍然活跃的草案,不是 RFC,也不是已批准的标准。工作组来源不等于共识批准。本文以该提案和当前服务文档分析问题,不把其中语义擅自提升为普遍 HTTP 义务。

超时留下的是知识缺口,不是未执行证明

RFC 9110 对方法幂等性的讨论,关注同一方法重复调用所预期的效果,不要求每次日志和应答一模一样,也不保证一条多系统链路端到端恰好执行一次。服务可以为某种 POST 或 PATCH 提供幂等契约,但一个头字段本身不能把任意操作变成全局“只一次”。

草案还提醒,通用客户端没有事先知识时,不能假定任意服务器都会尊重它提供的键。唯一性属于什么调用方和操作范围,载荷哪些部分参与等价比较,执行中如何处理,过多久会失去识别能力,都需要契约。

完成后重试、执行中重试、不同载荷复用同键,是三类不同问题。草案分别建议返还已完成结果、报告冲突,以及拒绝不一致复用;错误结果也可能是应该返还的旧结果。不能把它们一概视为“换个新键就能继续”的理由。那样做有时并非修复请求,而是在旧结果未明时发出另一项行动。

载荷指纹也不是中性的技术细节。全部字段、部分字段或签名摘要,定义了哪些差异会被视为同一请求。增加一个影响业务的字段而未纳入比较,可能把不同意图混成同一个;仅有格式变化却被当成新请求,则可能把同一意图拆成多个。散列强度不能弥补等价规则的语义错误。

保留期限本来就是保障的一部分

无限保存每个键、每个应答和全部载荷,未必合理。存储成本、数据敏感性、碰撞范围、资源生命和可能迟到多久,都影响保留设计。治理问题不是禁止清理,而是让调用方在清理之后知道自己失去了什么,以及还需要凭什么继续。

Stripe 当前官方文档提供了一个具体例子:端点执行开始后,它保存某键的首个状态码与应答体,错误也包括在内;若参数校验失败,或与并发请求冲突而尚未开始执行,则不保存这一幂等结果。文档允许在键至少存在 24 小时后自动清理,并说明原条目清理后再复用,会产生一项新请求。

这不是所有键必定恰好 24 小时到期的承诺,更不是所有提供方的通用规则,也不是对某次重复收费事故的报道。它只是该服务的明确边界。调用方若能在几天后从队列、设备或备份中重新送出旧操作,就应把恢复跨度与这一边界比较,而非继续把原键当成无限保险。

AWS Builders Library 的例子展示另一种处理:迟到请求回来时,另一方可能已经删掉最初创建的资源。其 EC2 示例仍保持语义等价的答复,而不是把资源重新创建。文中讨论了与资源生命相连、再加迟到间隔的保留方式,同时指出不同服务和资源有不同要求。

两个例子说明的是契约差异,不是可以抽出一个统一 TTL。几秒内的 SDK 重试、长时间离线设备、灾难恢复和人工支持,迟到模式并不相同。增加退避时间能缓和短期负载,却不能补回已经被清掉的识别记忆。重试跑得更久,不自动意味着恢复更安全。

区分键、操作、效果和新意图

第一项身份是去重键,在服务规定范围中用于识别再次到达。第二项是业务操作:被授权的一项指令或决定。第三项是实际产生的效果或资源。第四项是新的意图,可能允许另一次效果。它们可以关联,却不应被一个随机字符串全部替代。

换键不能证明换了意图。旧键在清理后再次出现,不能证明旧操作失败。资源已删除,不能证明它从未存在。反过来,完全相同参数也可能来自合法的第二次决定,例如用户确实希望再次执行同一动作。只比较字节无法回答授权问题。

一个持久而紧凑的操作记录,可以让普通去重窗口结束后仍有核对路径。它可以保留请求方范围、被授权的操作、参数承诺、接受时间、终态、效果引用和查询办法。不必永久复制全部敏感载荷,也不必永远保存每次旧应答。访问和保留期限仍应被约束在正当用途之内。

这是一项治理建议,不是本文发明的新 Idempotency-Key 必填字段。不同服务可通过不同架构满足它。需要守住的不变量更窄:去重缓存清理时,不应连同判断一项旧操作是否还能行动的唯一证据一起清掉。这里的去重状态也不等同于 RFC 9111 中普通 HTTP 响应缓存的所有语义。

窗口之外应有明确处置

到期政策应说明从何时起算,适用哪个调用者、哪类操作,如何保留已完成错误,怎样处理仍在执行的工作,以及迟到交付会遇到什么。仅写“保留若干小时”,不够解释客户端丢失答复后的恢复动作。

窗口外的模糊重试可以被拒绝,进入可审核等待,或与持久操作身份核对。客户端可以停止自动送达,查询已知操作与资源,必要时升级给有权决定的人。若确实需要另一次效果,就应明确表达新的意图,不把它藏在原请求恢复里。

服务当然可以公布“清理后复用按新请求处理”。这并不替调用方创造新授权。客户端工作流应在边界处停止宣称原来那份只一次保障仍成立。协议允许再次发送,和业务上有权再次执行,是两件事。

人工入口同样重要。支持人员在三天后点击“重试”,可能已进入不同于 SDK 几秒后重试的契约。为了绕开冲突自动换键,可以让工单变得顺畅,却可能把未知旧结果变成第二项操作。界面和操作规程应让这种变化可见,而不是把它当成透明的传输修补。

记忆与后果须能对上

记下键却未创建资源,会造成虚假的完成;创建资源却未记下键,可能再次执行。AWS 的说明强调,记录身份与相关变更应协调为一个原子性的全有或全无过程。

但一个本地事务并不必然包含全部下游后果。首个系统已提交行,另一个系统已执行指令,后者答复却丢失,这些情况可以同时存在。操作记录应指向对每项效果真正有权威的系统,区分完成、等待、失败和未知。“第一张表已经提交”不能替整条链路证明终态。

测试应包含正常窗口之后的迟到、资源删除后的重试、参数变化、调用者范围变化,以及下游部分完成。验收目标不是选定状态码出现,而是不产生未经授权的第二次效果,并如实说明仍未解决的状态。

备份恢复出旧指令、设备晚连、人工导出再次送达已经核对过的任务,都是正常恢复路径。它们越容易被当成边缘情况,保留政策越容易变成组织协调缺口。真正的安全重试,应包含失败恢复可能跨过多久,而不只是平稳时两次请求相隔多短。

来源

  1. 过期草案当前记录
  2. 草案历史
  3. 07 版 HTML
  4. 07 版纯文本
  5. 07 版 XML
  6. HTTPAPI 工作组章程
  7. HTTPAPI 工作组文档
  8. 草案官方源码仓库
  9. 官方问题讨论
  10. RFC 9110:HTTP 语义
  11. RFC 9457:API 问题描述
  12. RFC 8941:HTTP 结构化字段
  13. RFC 9562:UUID
  14. RFC 9111:HTTP 缓存
  15. Stripe:幂等请求
  16. Stripe:底层错误处理
  17. AWS:以幂等 API 安全重试
  18. AWS:超时、重试与退避
  19. Lu Heng:最小初始规范与本地未来决策
  20. Lu Heng:政策的镜面