API 中的 500、502、503 和 504 錯誤:它們的意義與處理方式

狀態碼從 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 錯誤

  1. 僅針對冪等請求重試 502、503 和 504。 對於 POST 和 PATCH,只有在 API 文件說明如何使其可安全重試時才重試。
  2. 重試前請先等等。 當伺服器傳送它時,請使用 Retry-After。 否則,使用指數退縮配合隨機抖動,嘗試幾次後停止。
  3. 請閱讀 API 的 500 錯誤說明文件。 只有在 API 表示這樣做是安全的情況下,才再試一次。
  4. 別再呼叫一直失敗的 API。 使用斷路器,讓你的應用程式在 API 恢復期間快速失敗。
  5. 告訴使用者發生了什麼事。 顯示「服務有問題,稍後再試」而不是一般錯誤或堆疊追蹤。
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。

下一步

也請參閱