Retry After and Date
Retry-After
Retry-After 是一個 HTTP Response Header,有兩種表達方式
Retry-After: 3
=> 3 秒後再請求一次Retry-After: Sat, 24 May 2025 10:57:26 GMT
=> 請等到這個時間點再請求一次
其中,第二種表達方式,跟 Date: Sat, 24 May 2025 10:57:26 GMT
HTTP Header 的格式一模一樣,都是 UTCString
使用時機
Retry-After 通常會用在以下情境
- 503 Service Unavailable
- 429 Too Many Requests
- 30x Redirections
實作環節
老樣子,使用 NodeJS HTTP 模組實作
import { STATUS_CODES } from "http";
import httpServer from "../httpServer";
httpServer.on("request", function requestListener(req, res) {
if (req.url === "/200") {
res.statusCode = 200;
res.setHeader("Retry-After", 3);
return res.end(STATUS_CODES[200]);
}
if (req.url === "/429") {
res.statusCode = 429;
res.setHeader("Retry-After", 3);
return res.end(STATUS_CODES[429]);
}
if (req.url === "/503") {
res.statusCode = 503;
res.setHeader("Retry-After", 3);
return res.end(STATUS_CODES[503]);
}
});
使用瀏覽器分別打開
結論是,瀏覽器 (chrome v136, edge v136, firefox v138) 都沒有實作自動重新請求
undici RetryAgent
既然瀏覽器沒有實作 Retry-After
,我找到 undici,An HTTP/1.1 client, written from scratch for Node.js
,裡面就有實作 Retry-After
的邏輯。
undici 針對 Retry-After
的實作,在 retry-handler.js 這個檔案。除了會去判斷 Retry-After
這個 Response Header,同時也會針對 statusCode 跟 method 等等去判斷,具體的預設值,重點節錄如下:
this.retryOpts = {
retryAfter: retryAfter ?? true,
maxTimeout: maxTimeout ?? 30 * 1000, // 30s,
minTimeout: minTimeout ?? 500, // .5s
maxRetries: maxRetries ?? 5,
methods: methods ?? ["GET", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE"],
statusCodes: statusCodes ?? [500, 502, 503, 504, 429],
// other options
};
我們使用 undici 的 Agent
跟 RetryAgent
來實作 HTTP Client。
undici 的 RetryAgent
是 RetryHandler
的高階封裝,通常情況下,使用 RetryAgent
即可滿足大部分業務場景
另外,Agent 的中文是 "代理人",在這邊的意思是 "幫使用者發起 HTTP 請求的代理人",概念跟 User-Agent 的 Agent 類似。現在很夯的 AI-Agent
,概念也是類似,意思就是 "幫使用者跟 AI 溝通的代理人"
import { Agent, RetryAgent } from "undici";
httpServer.on("request", function requestListener(req, res) {
if (req.url === "/200") {
console.timeLog("Retry-After");
console.log(req.url, req.headers);
// 其餘 code 不變
}
if (req.url === "/429") {
console.timeLog("Retry-After");
console.log(req.url, req.headers);
// 其餘 code 不變
}
if (req.url === "/503") {
console.timeLog("Retry-After");
console.log(req.url, req.headers);
// 其餘 code 不變
}
});
console.time("Retry-After");
const httpClientWithRetry = new RetryAgent(new Agent(), { maxRetries: 2 });
httpClientWithRetry
.request({
origin: "http://localhost:5000",
path: "/429",
method: "GET",
})
.then((response) => response.body.text())
.then((bodyText) => bodyText)
.catch((err) => err);
在終端機的輸出,就會看到如同我們定義的,retry 兩次,間隔 3 秒
Retry-After: 13.197ms
{
url: '/429',
headers: { host: 'localhost:5000', connection: 'keep-alive' }
}
Retry-After: 3.020s
{
url: '/429',
headers: { host: 'localhost:5000', connection: 'keep-alive' }
}
Retry-After: 6.030s
{
url: '/429',
headers: { host: 'localhost:5000', connection: 'keep-alive' }
}
如果改成去請求 /200
的路由,因為不在預設的 statusCodes
,理論上就不會 retry
httpClientWithRetry.request({
origin: "http://localhost:5000",
path: "/200",
method: "GET",
});
// 其餘 code 不變
接著看看輸出
Retry-After: 14.005ms
{
url: '/200',
headers: { host: 'localhost:5000', connection: 'keep-alive' }
}
fetch 跟 retry 的歷史
使用瀏覽器的 fetch
,即便收到 Retry-After
的 Response Header,也不會自動 retry
fetch("http://localhost:5000/429");
我在 whatwg (Web Hypertext Application Technology Working Group),專門維護 HTML 標準的組織,有找到一個 issue,在 2015 年的時候,就已經有開發者提出 fetch
可否支援 Retry-After
的請求,不過直到現在都還沒有實作
同時我也找到一個 Weekly Downloads 約 400 萬的套件,幫 fetch
實作 retry 功能
Vercel,開源 NextJS 的公司,曾經也做了一套給 NodeJS 使用的 fetch
,叫做 @vercel/fetch 。但後來 NodeJS 將 undici 實作的 fetch
作為正統,並且在 NodeJS v21 正式釋出穩定版。Vercel 後來也不再維護 @vercel/fetch,並且將 repo 轉成 read-only,詳細可參考這個 issue,裡面就有提到 undici 已經有實作 Retry Logic,也就是上面我們使用的 undici RetryAgent
小結
在實作 Retry-After
的時候,我原本以為瀏覽器會實作 Retry 的機制,後來實測才發現沒有,但我也沒有找到瀏覽器沒有實作 Retry 機制的相關討論串或是 issue。
還好 undici
有實作,才讓我有東西可以參考。並且也搜到很多 fetch
相關的歷史,真的是收穫滿滿!
參考資料
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
- https://datatracker.ietf.org/doc/html/rfc6585
- https://httpwg.org/specs/rfc9110.html#field.retry-after
- https://undici.nodejs.org/#/docs/api/RetryAgent.md
- https://undici.nodejs.org/#/docs/api/RetryHandler.md