状态代码从 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 错误
- 仅针对幂等请求重试 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 按需失败。 测试的方式决定你是否在用户之前找到 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”。