📘 第六章 HTTP首部
📄 一句话概括

Retry-After 告诉客户端「过多久再来」,有两种写法:秒数,或者一个 HTTP 日期。 它配 503 和 429 出现,没有它客户端只能瞎猜。

📝 本节和前面几节的关系

上一节是「你不够格」,这一节是「你来得不是时候」。前者要凭据,后者只要耐心。

一、两种写法,都要会认

第一种是纯数字,单位秒:Retry-After: 120。 第二种是 HTTP 日期:Retry-After: Sun, 25 Oct 2026 06:35:47 GMT。 判断方法很简单:头一个字符是数字就按秒算,否则按日期解析。 Demo 里把「现在 + 0/+3/+60 秒」各生成一个日期串,可以看出它和秒数是同一件事的两种表达。 只支持一种的服务器,遇上只认另一种的客户端就白说了。实际用到秒数的场合更多,因为不用管时钟对不对。

二、它配哪些状态码

最典型的是 503 Service Unavailable:服务器暂时扛不住,但过一阵能好。 次典型的是 429 Too Many Requests:限流了,等一会儿再试。 严格说 Retry-After 也可以出现在 301/302 里,意思是「别急着立刻跟过来」。但那种用法很少见。 注意它和 503 的关系是必须,不是可选。 一个光秃秃的 503,客户端只能按自己的经验退避——猜短了把刚缓过来的服务器又打趴,猜长了就是白等。

三、客户端该怎么做

照做就行,但要有上限:单次等待封顶、总重试次数封顶、并且只对幂等请求重试。 POST 盲目重试可能建出两条订单。 Demo 里服务端前两次回 503 带 Retry-After: 1,第三次给 200。 客户端老老实实睡 1 秒,累计耗时 0.00 → 1.03 → 2.03 秒,节奏完全是被那三个数字牵着走的。 反过来说,如果你在写服务端,别为了「显得友好」乱发一个很大的值。 客户端可能真会等那么久,那期间你的服务对它就是不存在的。

四、动手 Demo:两次 503,一次 200

服务端前两次回 503 并带上 Retry-After: 1,第三次才给内容; 客户端每收到一次就按那个数字睡,记录累计耗时。然后单独演示日期写法的换算。

def fn(raw, st):
    st[u'n'] = st.get(u'n', 0) + 1
    if st[u'n'] < 3:
        return X.reply(u'HTTP/1.1 503 Service Unavailable',
                       [(u'Retry-After', u'1')], b'busy')
    return X.reply(u'HTTP/1.1 200 OK', [], TEXT)

for i in range(3):
    r = X.ask(p, X.req(u'GET', u'/s', u'127.0.0.1'))
    ra = [ln for ln in X.head_lines(r) if ln.lower().startswith(u'retry-after:')]
    if ra:
        time.sleep(float(ra[0].split(u':')[1]))   # 照它说的睡

实跑输出:

--------------------------------------------------------------------
Retry-After:多久之后再来
--------------------------------------------------------------------
  服务端前两次说「1 秒后再来」,第三次才给内容。客户端老实照做:
    第 1 次  HTTP/1.1 503 Service Unavailable       Retry-After: 1  累计 0.00 秒
    第 2 次  HTTP/1.1 503 Service Unavailable       Retry-After: 1  累计 1.02 秒
    第 3 次  HTTP/1.1 200 OK                          累计 2.04 秒

  Retry-After 也能写成 HTTP 日期,这时候要看的是「什么时候」,不是「多久」:
    现在 + 0   秒 → Sun, 25 Oct 2026 06:37:56 GMT
    现在 + 3   秒 → Sun, 25 Oct 2026 06:37:59 GMT
    现在 + 60  秒 → Sun, 25 Oct 2026 06:38:56 GMT

  两种写法客户端都要能认:纯数字按秒算,带字母的按日期解析再减当前时间。
  只写一种的服务器,遇上只认另一种的客户端就白说了。

  这条首部配 503 和 429 是必须的。没有它,客户端只能瞎猜——
  猜短了是把刚缓过来的服务器又打趴,猜长了是白等。
  上面那三次请求累计花了 2.03 秒,正是客户端照着 Retry-After 睡出来的。

三次请求的状态码依次是 503、503、200,累计耗时 0.00、1.03、2.03 秒。 两次等待正好两秒,多的那 0.03 是本机开销。 第一行时间是 0.00 不是 1.00,因为第一次还没睡——是「收到 503 之后才开始等」。 下面那三行日期串是同一次的换算:现在、加 3 秒、加 60 秒。 客户端拿到日期串要先减当前时间,减出负数就按 0 处理。 整个 Demo 里,睡多久完全由服务端那一个数字决定。 这就是它的价值:把「等多久」这个决定权从客户端手里收回来。

30 秒实验

  1. 把 Retry-After 改成 3,看累计耗时会不会变成 6 秒。
  2. 改成日期串,用 time.mktime 解析后相减,看能不能对上秒数。
  3. 想一想:如果 503 不带 Retry-After,客户端应该按什么策略退避?

常见误区

  1. 「Retry-After 只能写秒数」——也能写 HTTP 日期。
  2. 「503 不带 Retry-After 也行」——能跑,但客户端只能瞎猜,容易被重试打垮。
  3. 「所有请求都该重试」——POST 不幂等,盲目重试会重复建单。
自测三问(点开对答案) 1. `Retry-After` 的两种写法怎么区分? —— 首字符是数字按秒算,否则按 HTTP 日期解析再减当前时间。 2. 它最常配哪两个状态码? —— 503 Service Unavailable 和 429 Too Many Requests。 3. 为什么服务端不该随手写一个很大的值? —— 客户端可能真等那么久,那期间你的服务对它等于不存在。

「等会儿再来」是缓和手段。下面三个字段转向自报家门与内容区分:Server、Vary、WWW-Authenticate。

相关笔记