狀態碼從 500 到 599 表示伺服器未能完成看似有效的請求。 你的請求不是問題所在,所以再寄一次是可行的。 是否要再寄一次,取決於狀態碼和請求會執行什麼。 關於定義,請參見 RFC 9110,第15.6節。
每個狀態碼的意義
| 狀態代碼 | 這是什麼意思? | 重試? |
|---|---|---|
500 Internal Server Error |
伺服器遇到了意想不到的狀況 | 這取決於 API。 有些 API,例如 Claude API,會建議你以指數退避方式重試 500。 請查看 API 的文件。 |
502 Bad Gateway |
閘道器或代理伺服器從其背後的伺服器那裡收到了無效回應 | 是的,如果請求可以安全重複的話 |
503 Service Unavailable |
伺服器暫時過載或因維護而停機,過一段時間後應該會恢復。 伺服器可以傳送 Retry-After 標頭。 |
是的,在 Retry-After 時間之後,如果伺服器有傳送的話 |
504 Gateway Timeout |
閘道器或代理伺服器無法及時收到背後伺服器的回應 | 是的,如果請求可以安全重複。 閘道器等候逾時,所以你不知道伺服器是否完成了工作。 |
哪些要求可安全重試
RFC 9110 在多次傳送相同請求時,會將該方法稱為冪等,且效果與發送一次相同。
GET、HEAD、OPTIONS、TRACE、PUT 和 DELETE 都是冪等的。
POST 和 PATCH 都不是。 根據 RFC,除非客戶端知道該請求本身就是冪等請求,或能判斷伺服器從未套用原始請求,否則不應自動重試使用非冪等方法的請求。 詳情請參見 冪等方法。
在 502 或 504 之後重試 POST 可能會建立第二筆訂單或寄送第二封電子郵件。 有些重試函式庫預設會重試所有方法。 例如,.NET 標準的韌性處理器會重試POST,除非你呼叫 options.Retry.DisableForUnsafeHttpMethods()。 詳情請參見 「建構韌性 HTTP 應用程式」。
503 回應上的 Retry-After
503 可以包含 Retry-After 標頭。 它的值要麼是秒數,如 120,要麼是 HTTP 日期,如 Fri, 31 Dec 1999 23:59:59 GMT。 你的程式碼需要同時處理這兩者。 詳情請參見 Retry-After。
停止呼叫持續失敗的 API
重試有助於短暫性故障。 當 API 當機好幾分鐘時,重試每個請求只會增加已經很吃力的伺服器負擔,使用者只能等每一次重試失敗。 斷路器會追蹤故障,當故障太多時,它會暫停呼叫 API 一段時間,且立即返回錯誤。 之後,它會允許幾個請求通過,以檢查 API 是否已恢復。 欲了解更多資訊,請參閱 斷路器模式。 .NET標準韌性處理器包含一個斷路器,當在 30 秒內至少有 10% 的請求失敗且至少有 100 個請求時,斷路器會斷開 5 秒。
如何處理 5xx 錯誤
- 僅針對冪等請求重試 502、503 和 504。 對於
POST和PATCH,只有在 API 文件說明如何使其可安全重試時才重試。 - 重試前請先等等。 當伺服器傳送它時,請使用
Retry-After。 否則,使用指數退縮配合隨機抖動,嘗試幾次後停止。 - 請閱讀 API 的 500 錯誤說明文件。 只有在 API 表示這樣做是安全的情況下,才再試一次。
- 別再呼叫一直失敗的 API。 使用斷路器,讓你的應用程式在 API 恢復期間快速失敗。
- 告訴使用者發生了什麼事。 顯示「服務有問題,稍後再試」而不是一般錯誤或堆疊追蹤。
const RETRYABLE_STATUS = new Set([502, 503, 504]);
const IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE", "PUT", "DELETE"]);
function retryAfterMs(response) {
const value = response.headers.get("retry-after");
if (!value) return null;
const seconds = Number(value);
if (Number.isFinite(seconds)) return seconds * 1000;
const date = Date.parse(value);
return Number.isNaN(date) ? null : Math.max(0, date - Date.now());
}
export async function fetchWithRetry(url, options = {}, maxRetries = 3) {
const method = (options.method ?? "GET").toUpperCase();
const canRetry = IDEMPOTENT_METHODS.has(method);
for (let attempt = 0; ; attempt++) {
const response = await fetch(url, options);
if (!RETRYABLE_STATUS.has(response.status) || !canRetry || attempt === maxRetries) {
return response;
}
await response.body?.cancel();
const backoff = 2 ** attempt * 1000 + Math.random() * 1000;
const wait = retryAfterMs(response) ?? backoff;
await new Promise((resolve) => setTimeout(resolve, wait));
}
}
如何測試你的應用程式是否能處理 5xx 錯誤
開發過程中很少看到 5xx,也無法讓 API 刻意失敗。 你測試的方式決定了你是否比使用者先發現錯誤。
| Approach | 你發現的 | 你所遺漏的內容 |
|---|---|---|
| 等待正式環境 | 實際中斷 | 在使用者點擊前,一切如常 |
| 在測試中模擬 API,或讓你的編碼代理寫出模擬 | 不管你的錯誤分支是否執行 | 你的真實 HTTP 客戶端和重試函式庫,以及它實際重試的次數。 你的應用程式也需要一個只供測試使用的開關來連到模擬服務。 |
| 呼叫真正的 API,等待它失敗 | 實際行為 | 你無法讓 API 在需要時失敗 |
| 攔截你應用程式的真實流量,並以你選擇的速率回傳 5xx 錯誤 | 你的真實 HTTP 用戶端、重試函式庫和斷路器 | 你的應用程式裡沒有任何變化,因此無法獨立測試你的程式碼。 把那個留給你的單元測試。 |
在你的應用程式上試試看
Dev Proxy 會攔截你應用程式的請求,並使用 GenericRandomErrorPlugin,依照你定義的錯誤讓部分請求失敗。 你的應用程式一直呼叫實際的 URL。 把外掛加到你的設定檔,然後將其 errorsFile 指向一個有 5xx 錯誤的檔案。 這個範例使用 https://api.contoso.com。 用你應用程式呼叫的 API URL 來取代它。
檔案: server-errors.json
{
"$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/genericrandomerrorplugin.errorsfile.schema.json",
"errors": [
{
"request": {
"url": "https://api.contoso.com/*"
},
"responses": [
{ "statusCode": 500 },
{ "statusCode": 502 },
{
"statusCode": 503,
"headers": [
{ "name": "Retry-After", "value": "10" }
]
},
{ "statusCode": 504 }
]
}
]
}
預設情況下,外掛有 50% 的請求會失敗。 在開發代理輸出中檢查你的應用程式是否會重試POST請求,且每個GET只發送一次。 Dev Proxy 不會檢查你的應用程式是否會在發生 503 時等待 Retry-After,所以請自行比較請求時間。 接著啟動 Dev Proxy --failure-rate 100 ,看看當 API 持續失敗時,你的應用程式會怎麼做。 欲了解更多資訊,請參閱 變更請求失敗率。 要安裝 Dev Proxy,請參見 設定 Dev Proxy。