API 错误处理与重试策略:给接口定一套“讲得清”的错误语言
接口报错是前后端最常见的沟通场景,可很多站点把错误处理做成了“黑盒”:要么返回一串 HTML 错误页,要么只给一个干巴巴的 500,前端拿到之后只能靠猜。问题通常不在某一个错误码,而在没有一套统一约定。好的错误设计应该让调用方一眼看懂三件事:错在哪一层、为什么错、能不能重试。
错误分层:先分清是客户端的错还是服务端的错
| 状态码 | 含义 | 常见触发场景 | 是否应重试 |
|---|---|---|---|
| 400 | 请求格式错误 | JSON 解析失败、缺必填字段 | 否 |
| 401/403 | 未认证/无权限 | token 过期、签名错误 | 否(刷新凭证后重发) |
| 404 | 资源不存在 | 路径拼错、资源已删除 | 否 |
| 422 | 语义校验失败 | 邮箱格式不对、库存不足 | 否 |
| 429 | 限流 | 超出配额或频率限制 | 是(遵循 Retry-After) |
| 500/502/503/504 | 服务端异常 | 崩溃、网关超时、上游不可用 | 是(有限次数) |
统一的错误响应结构
4xx 和 5xx 都应该返回相同的 JSON 结构,而不是一段随意的文本。
{
"code": "RATE_LIMIT",
"message": "too many requests",
"request_id": "abc123",
"retry_after": 3,
"details": {"limit": 100, "window": "1m"}
}
code:机器可读的错误码,前端可以据此做分支判断message:给人看的说明request_id:贯穿日志的关键,排查问题就靠它retry_after:429 时返回建议等待秒数,比客户端自己猜更准确
重试策略:不是所有错误都值得重试
重试不是“报错就再调一次”。不节制的重试会把一次小故障放大成雪崩——服务端越忙,客户端越疯狂重试,结果越忙。记住两个原则:只重试可恢复的错误,每次重试之间要等待。
| 参数 | 建议值 | 说明 |
|---|---|---|
| 可重试错误 | 429、502、503、504 | 400/401/403/404/422 一律不重试 |
| 初始等待 | 1s | 指数退避的起点 |
| 退避系数 | 2 | 1s → 2s → 4s → 8s |
| 最大等待 | 30-60s | 防止间隔无限拉长 |
| 重试上限 | 3 次 | 超过即失败,交给上层处理 |
| 抖动(jitter) | 随机 ±20% | 避免客户端在同一时刻齐发 |
带抖动的指数退避是一个很小的改动,却能显著降低重试风暴的发生概率。客户端最好同时尊重服务端返回的 Retry-After 响应头,它比本地算出来的时间更准。
幂等键:让“重试”不会造成重复下单
POST 类请求重试最大的风险是副作用被重复执行——用户点了两次“提交订单”,结果下了两单。解决办法是幂等键:客户端在请求头里带一个唯一值,服务端记录这个键,遇到重复键直接返回第一次的结果。
Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
场景:支付回调连着报了三次错
一个典型的排查案例:某天支付回调接口开始连续返回 502,支付服务商的重试机制触发了三次都失败,最后任务被丢进了死信队列。开发人员顺着日志里相同的 request_id 一路查到网关,发现是上游某台机器内存被打满。整个过程里,因为错误结构统一、request_id 齐全,从发现问题到定位只花了十几分钟。如果当时错误响应是乱糟糟的文本,这个排查时间至少要翻三倍。
客户端侧还要做什么
- 在网关或 SDK 层统一封装重试,业务代码里不要到处写
for循环重试 - 加熔断器:连续失败达到阈值(如 5 次)后,短时间内直接快速失败,不再发请求
- 超时设置:连接超时和读超时分开放,默认 5s/10s,避免慢接口拖死整个页面
错误码命名与接口文档约定
有了统一结构,还要让错误码本身可读。建议用“领域 + 场景 + 状态”的方式命名,例如 ORDER_INSUFFICIENT_STOCK、PAYMENT_CARD_DECLINED、AUTH_TOKEN_EXPIRED,比裸数字 1001 好懂得多。错误码一旦发布就尽量保持稳定,客户端通常会对特定 code 做硬编码分支,改名会破坏兼容。
接口文档里应该为每个错误码写清楚:触发条件、message 的模板、是否需要重试、有没有关联的 retry_after。可以生成一份统一的“错误码参考页”,前后端都以它为准。实践中,很多团队把错误码表放在接口文档(如 OpenAPI)的顶部,并用自动化测试断言“所有 4xx 都返回相同结构、都带 request_id”。
另一个常被忽略的约定是日志联动:每个错误响应都要把 request_id 和对应日志关联起来,方便从用户截图里的报错反查后端。建议在入口中间件里统一注入 request_id,并把它透传到下游服务,形成一条完整的调用链。
参考:Stripe API 错误处理文档 https://docs.stripe.com/api/errors
参考:Google Cloud API 错误模型 https://cloud.google.com/apis/design/errors
参考:RFC 6585(429 Too Many Requests) https://www.rfc-editor.org/rfc/rfc6585