来自 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 标准复原处理程序会重试 options.Retry.DisableForUnsafeHttpMethods(),除非您调用 POST。 有关详细信息,请参阅 构建可复原的 HTTP 应用。

503 响应中的 Retry-After

503 可以包含标头 Retry-After 。 其值为秒数,例如 120,或 HTTP 日期,例如 Fri, 31 Dec 1999 23:59:59 GMT。 代码需要同时处理这两者。 有关详细信息,请参阅 Retry-After。

停止调用不断失败的 API

重试有助于解决短暂故障。 当 API 不可用长达几分钟时,重试每个请求会给本已苦苦支撑的服务器增加负载,并且用户只能等待每次重试失败。 熔断器跟踪故障,当故障过多时,它会停止调用 API 一段时间并快速失败。 之后,它会放行少量请求,以检查 API 是否已恢复。 有关详细信息,请参阅 断路器模式。 .NET标准复原处理程序包括一个断路器,当至少 10% 的请求在 30 秒窗口内失败且请求总数至少为 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 按需失败。 测试的方式决定你是否在用户之前找到 bug。

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% 的请求失败。 在 Dev Proxy 输出中检查,确保你的应用会重试 GET 请求,且每个 POST 只发送一次。 开发代理不会检查应用是否在返回 503 时等待 Retry-After,因此请自行比较请求时间。 然后,启动开发代理 --failure-rate 100,查看当 API 持续失败时应用会如何表现。 有关详细信息,请参阅 更改请求失败率。 若要安装 Dev Proxy,请参阅 “设置 Dev Proxy”。

后续步骤

另请参阅