摘要

  • 429 Too Many Requests 表达的是:服务器归到某个主体名下的一串请求,已经越过本地规则设定的预算。它不等于请求格式错误,也不能证明一个自然人实施了滥用。
  • RFC 6585 允许服务器给出 Retry-After,但等待时间只是下一步建议,不是未来容量的预约;再次尝试时仍要面对届时的负载、权限、策略与资源状态。
  • 429 只统一了对外判词,没有统一背后的身份、计数范围和算法。真正的控制权仍在运营者手中;极端流量下,连生成拒绝响应都可能太贵,标准允许直接断开连接。

第五十一次请求并没有突然变坏

设想一个服务规定:每个登录账户每小时可以调用五十次。同一客户端发出的前五十条请求被接受,第五十一条被拒绝。它可能与上一条拥有相同的语法、相同的目标、同样有效的凭证,也同样符合业务含义。发生变化的不是这条消息,而是消息之外的一段历史:服务器把它和前面的请求放进同一个计数器,计数器越线了。

在缺少专用状态码时,服务可能返回笼统的客户端错误,让客户端误以为必须修改载荷;也可能返回整体不可用,让客户端误以为所有用户、所有资源都停止服务。RFC 6585 在 2012 年加入 429,给这种情况一个更精确的共同说法。

但标准刻意没有接管计数规则。它说响应正文应该解释条件,可以用 Retry-After 告知多久后再试;同时明确说,不定义源站如何识别“用户”,也不定义怎样统计请求。协议让结果可互操作,没有把运营者的账本变成全球统一制度。

“用户”是计数键,不天然是一个人

机器若要在花费更多资源之前作出判断,就必须先找到一个可以归并流量的键。这个键可能是登录账户、API 密钥、会话 cookie、租户、源地址,或者几项证据的组合。RFC 6585 举出凭证和有状态 cookie,只是说明可能性,并没有宣布它们等于自然人身份。

源 IP 最容易在边缘取得,却常把无关的人绑在一起。运营商级 NAT 后面可能有大量订户,公司出口之后可能有整个组织,隐私中继也会有意把地址与个人拆开。若限流器只按地址统计,一个人的突发流量会消耗所有人的份额。由此返回的 429 可以符合协议,但不能证明每位被拒者都用完了自己的额度。

认证账户更接近商业关系,却也可能覆盖多台设备、多个进程或整个团队;密钥还可能被共享或盗用。不同证据对应不同边界。地址证据最多支撑“这个地址范围的预算已用完”,不能升级为“这个人滥用服务”的道德判断。

这正是控制权必须与证据相称的地方:服务器可以保护自己的容量,但它不能让三位数字替自己完成身份认证。

同一个 429 背后可以有完全不同的范围

RFC 6585 允许按单个资源、整台服务器,甚至一组服务器计数。选择不同,读者看到的仍是 429,实际后果却不同。

按资源限制,可以只保护昂贵的查询接口,让账户设置和状态检查继续工作。整站共享额度便于保护共同容量,却可能让一次轻量读取和一次昂贵生成各占同样的一格。按租户统计可以贴近合同,需要每条请求都可靠地映射到正确租户。跨区域共享计数能保持一套额度,却必须处理复制延迟、网络分区和计数服务故障。

连“一次请求”也需要运营者定义。收到消息时就加一,还是认证成功、正式接纳、工作完成后才加?重定向是否再计一次?已经取消的 HTTP/2 流算不算?代理在内部重试两次,客户究竟花掉一格还是三格?状态码不会回答,因为这些问题发生在拥有执行状态的系统里。

于是可能出现一个看似矛盾的场景:每条 429 都格式正确,背后的计数却不准确。协议合规从来不是账目正确性的审计报告。

Retry-After 给的是时机,不是席位

按照 RFC 9110,Retry-After 可以写成 HTTP 日期,也可以写成从收到响应起计算的非负秒数。RFC 6585 允许 429 携带它,但没有强制要求。

两种形式都只告诉客户端“不要这么早回来”。绝对日期需要可用的时间参照,延迟秒数避免直接比较两端时钟;它们都不会在未来队列里保留一个位置。等候结束时,其他请求可能已经占用容量,策略可能改变,账户可能失去权限,目标资源也可能早已变化。

如果所有客户端都在同一秒醒来,恢复提示反而会制造新的尖峰。可靠客户端要把服务器提示与本地克制结合:限制重试次数、分散唤醒时间、为任务设置失效点,并按方法语义决定能否再次执行。非幂等操作不会因为计时器归零就自动安全;传输中断时是否产生过效果,仍需要另外的证据。

429 与 503 讲述不同的责任位置

RFC 9110 把 503 定义为服务器因临时过载或维护而无法处理请求。它描述的是服务自身当前的整体能力。429 描述的是:限流策略选定的主体,在选定窗口内发送得过多。

两者都可能出现 Retry-After,却不因此等价。某个租户被限流时,其他客户仍可正常使用;整个服务过载时,也可能没有任何单个客户超过个人额度。真实系统可以同时拥有租户额度和全局保护,返回的状态应当说明究竟是哪条规则作出了拒绝。

把全局容量不足说成客户端错误,是在转移责任;把单个额度耗尽说成全站故障,则隐藏了仍然开放的路径。精确状态码的价值正在于不混淆这两层。

拒绝本身也会消耗被保护的资源

RFC 6585 的安全考虑指出:遭受攻击或收到单一主体的大量请求时,逐条生成 429 会继续消耗资源。因此服务器没有义务每次都返回 429,可以直接丢弃连接或采取其他措施。

一条完整拒绝可能先经历解密、解析、认证、远程计数器查询、正文生成和加密发送。若攻击者用极小成本换取服务器完成整套工作,解释越详尽,防御越昂贵。把拒绝前移到边缘可以省下计算,却往往只能看到较弱的身份线索,因此更容易误伤共享地址后的用户。

这不是“永远解释”与“永远沉默”之间的道德选择,而是证据和成本之间的局部决策。系统应在仍能负担的层次,采取证据足以支撑的最窄行动。

缓存不能替计数器延长判决

RFC 6585 明确禁止缓存存储 429 响应。原因来自其时效性:判决绑定某个主体、某个范围、某个窗口和当下计数。缓存重放会在窗口结束后继续拒绝,或者把一个主体的债务错误地施加给另一个请求情境。

中间层若拥有自己的限流政策,可以基于自己的当前状态生成新 429;它不能把旧拒绝当作可复用内容。禁止缓存把判决留在仍能观察预算的系统附近。

三位数字能证明什么,又不能证明什么

429 不认证身份,不证明恶意,不说明算法公平,也不保证稍后成功。它没有规定所有服务都必须使用同一种计数,更没有要求服务器在攻击下为每条流量花钱解释。

它真正完成的是一件较窄却重要的事:把“请求内容有问题”“整个服务暂时不可用”和“这个本地计数范围已经超额”分开。客户端因此知道立刻重复通常无益;服务器也能在有余力时给出可执行的等待信息。

429 的历史并不是 HTTP 发明了配额,而是一个私人容量决定获得了共同语言,同时仍被限制在它能够证明的范围内。

来源