关于本文
本文是通过利用生成式 AI 的自动生成流程创建的。我们确认了 RFC 9110 的Retry-After规范与 Python 官方文档,并在首次创建时的 Python 3.13.5 以及本次修复确认时的 Python 3.12.14 中运行了不连接网络的检查程序。目标是解析头部值,并未执行实际的 HTTP 重试处理。
验证状态:已确认本地 Python 运行(同一教程的 16 个单元测试在 Python 3.13.5 和 3.12.14 中均成功)/未确认外部 API 及各操作系统实机/已完成同一执行内的五角色自检 QA
当 API 返回“请稍候”时,如果能用数字确认应该等待多久,将有助于推进故障排查。然而,Retry-After中存在单纯的秒数以及 GMT 日期这两种不同的书写方式。120虽然相对容易理解,但要将Sat, 24 Oct 2026 00:02:00 GMT转换为“从现在开始的多少秒”,就必须注意当前时间和时区。在本文中,我们将使用一个小型的 Python 程序,在不发送 HTTP 请求的情况下仅解析字符串,从而观察两者的差异。
首先运行:用两种写法表示相同的 120 秒
准备 Python 3.10 或更高版本,从GitHub 上的教程版本获取retry_after.py并在同一文件夹中运行。不需要额外的库或 API 密钥。为了固定比较条件,将--now传递给包含日期时间和 UTC 偏移量的字符串。
python retry_after.py 120 --now 2026-10-24T00:00:00+00:00
在创建时的本地运行中,我们确认了以下输出。
{"kind": "delay-seconds", "wait_seconds": 120, "target_utc": "2026-10-24T00:02:00Z", "action": "inspect_only_no_sleep_no_request"}
接着,仅将值更改为 HTTP-date 格式。
python retry_after.py 'Sat, 24 Oct 2026 00:02:00 GMT' --now 2026-10-24T00:00:00+00:00
这里实际测得的wait_seconds是120了。但是kind是http-date。前者表示“自接收之日起 120 秒”,后者表示“直到指定的 UTC 时间”,两者的意义起点有所不同。使用固定时间可以在排除网络延迟和执行时间差异的情况下进行比较。在实际通信环境中,如何获取接收时间也是设计上的一个讨论点。
为什么会有两种类型:阅读 RFC 规定
RFC 9110 §10.2.3将Retry-After的值定义为delay-seconds或HTTP-date。前者是一个大于等于 0 的十进制整数,并不是直接接受负数或小数作为等待秒数的格式。后者是一个日期时间。在 503 响应中,它用作服务可用前的参考时间,在 3xx 响应中,则用作重定向请求前的等待时间。
这里重要的是,不能仅凭 HTTP 状态码为 429 这一事实,就断定该标头一定存在。在没有标头的情况下,退避策略取决于 API 端的使用条件和客户端设计。本教材仅负责解析输入了标头值的情况,并不实现 429 的发生条件或服务器指定的完整重试策略。
HTTP-date 有多种允许的表示方式。RFC 9110 §5.6.7除了目前常见的 IMF-fixdate 之外,还包含用于兼容性的旧日期时间格式。RFC 要求发送方使用 IMF-fixdate,同时要求接收方为了兼容性而接受所有三种 HTTP-date。本代码作为学习用途,仅接受 IMF-fixdate,不会对旧格式进行推测转换。因此,它并不是用来替代符合 RFC 的通用 HTTP 客户端的实现。如果要将其应用到实际运营中,请添加接受旧格式的测试,或遵循所使用的 HTTP 库的规范。在此,优先考虑了“用固定时间比较秒数与当前格式日期”这一观察范围,这是一个刻意为之的限制。
代码要点:对日期时间和秒数进行分支处理
教材中的parse_retry_after函数首先会检查字符串是否仅由数字构成。如果是数字,则将其转换为整数并加到当前时间上。如果是日期时间格式,则在检查语法后,使用 Python 标准库的email.utils.parsedate_to_datetime。此函数用于将日期时间字符串转换为datetime,其规范可以在Python 官方文档中找到。
from datetime import datetime, timezone
from retry_after import parse_retry_after
now = datetime(2026, 10, 24, 0, 0, tzinfo=timezone.utc)
seconds, target, kind = parse_retry_after("120", now)
print(seconds, kind)
这段简短代码的实际测量输出是120 delay-seconds。该函数仅返回解析结果,不调用sleep或 HTTP 通信。为了避免在学习时担心向 API 发送大量重试请求,我们将观察功能和实际等待功能分开了。在将程序集成到业务中时,这种分离也有利于编写单元测试。
在日期时间格式中,我们还会检查星期几与日历上的日期是否一致。因为如果仅仅交由通用的日期时间解析器处理,它可能会默默接受错误的星期。此外,如果now没有时区,将会报错。2026-10-24 09:00因为仅凭这一点无法判断是 UTC 还是日本时间,从而会导致等待秒数发生巨大变化。
修改一个地方:过去的时间与小数秒
我们将刚才的日期时间改为 1 秒前的Fri, 23 Oct 2026 23:59:59 GMT。基准时间保持相同的2026-10-24T00:00:00+00:00不变。本教程中,等待时间将返回 0 秒。这样的设计是为了不将过去的时间视为负的等待时间。但这并不意味着“因为是 0 秒就可以无条件重新发送”。最大尝试次数和 API 侧的使用条件仍需另行遵守。
另一方面,如果基准时间包含小数秒,且距离指定的日期时间只剩 0.999999 秒,等待秒数将被向上取整为 1 秒。因为如果向下取整为整数,可能会导致在指定时间之前进行重试。在实际客户端中,还需要考虑测量时钟的类型、HTTP 响应的接收时机以及网络延迟。这里的计算仅作为标头解析的教学内容,并不能解决分布式系统中的所有时间同步问题。
识别错误:绝不默默修正
Retry-After: -1和Retry-After: 1.5作为delay-seconds是无效的。在命令行中,INVALID_RETRY_AFTER将显示在标准错误中,并以退出码 2 结束。星期与日期不符的日期时间、不存在的日期以及缺少时区信息的--now也不予受理。在失败时没有自动纠正为“先等1秒再说”,是为了在运维时能够区分非法的服务器响应和正常的响应。
如果命令无法启动,请检查Python的安装、工作文件夹和文件名。在Windows环境中python如果无法使用,py -3请考虑通过--now启动。当对日期的解释与预期不符时,请首先确认偏移量,并确保HTTP-date侧为GMT。如果输入了旧格式的日期时间而被拒绝,则不属于本教材的适用范围。不要擅自修改字符串,应保留收到的值并决定应对策略。
如果在工作中应用:不要将解析与重试混为一谈
在API运维中,重要的是在发生故障时分头记录“服务器返回了什么”和“客户端做了什么”。如果分别保存头部字符串、HTTP状态、接收时间、解析结果以及实际的下次请求时间,就可以事后确认是否以不必要短的间隔进行了重试。但是,不得在日志中随意留下身份验证令牌、个人信息、机密的URL或响应正文。
实际的重试处理需要考虑超时、最大尝试次数、指数退避、幂等性、网络故障与服务器响应的区分。例如,当以相同内容重新发送更新类API时,必须考虑到处理被重复执行的可能性。本教材的输出是等待时间的观察结果,而不是判断是否可以重试的完整规则。
初次创建时在Python 3.13.5、本次修复确认时在Python 3.12.14的本地环境中执行了相同提交的教材,两者均确认了16个单元测试、语法检查以及秒数格式和日期时间格式的CLI输出。未实施Codex CLI、实际API连接、Windows/macOS的真机测试以及其他AI的独立审查。同一执行中的5角色自我QA并非其他提供商进行的独立审查。如果需要额外的兼容性,安全的做法是先作为规范决定是否接受RFC的旧形式,并添加针对各个形式的测试。
