错误处理与重试
网关的错误分两类来源:网关自己产生的和上游厂商透传回来的。 先学会一眼分清这两类,剩下的处理逻辑就都顺了。
网关错误的结构
凡是网关自己拒绝的请求,返回体都是这个形状:
{
"code": 40101,
"message": "api key not found",
"error": {
"message": "api key not found",
"type": "gateway_error",
"code": "40101"
}
}
error.type 恒为 "gateway_error"——这就是区分两类错误的判据。
顶层的 code 是数字,error.code 是同一个值的字符串形式(为了兼容 OpenAI 错误体结构)。
错误码怎么读
网关错误码是 5 位数字,规则是 HTTP 状态码 + 2 位序号:
401 01
└┬┘ └┬┘
│ └── 序号:同一状态码下的第几种原因
└─────── HTTP 状态码
所以看到 42905 就知道 HTTP 是 429(限流),看到 50201 就知道是 502(上游侧)。
唯一的例外是 41301(请求体过大),它的 HTTP 状态是 413。
全部错误码见 错误码总表。
上游错误
如果请求已经成功路由到上游厂商,而厂商返回了错误,网关原样透传:状态码、 响应体结构都是厂商的原始格式,不做包装。这意味着:
- 你现有的官方 SDK 错误处理代码可以继续用,不需要改。
- 响应体里不会有
"type": "gateway_error"。 - 厂商的限流(如 OpenAI 的 429)与网关的限流(
429xx)需要分开对待——见下文。
哪些能重试
| 错误码 | HTTP | 可重试 | 怎么做 |
|---|---|---|---|
| 40000、40002–40004 | 400 | 否 |
请求本身错了,重试一万次也一样。改请求。
(40005 已于 2026-08-24 停用,不会再出现;
40001 归在下面的 401 一档。)
|
| 40100–40103 | 401 | 否 | 密钥问题。见 鉴权与 API Key。 |
| 40201–40203 | 403 | 否 | 资格不足。开通计费或换模型,见 计费 / 模型准入。 |
| 41301 | 413 | 否 | 请求体超 100 MiB。拆小。 |
| 42901–42905 | 429 | 是 | 限流。按 Retry-After 等待后重试。 |
| 42906 / 42907 | 429 | 否 | 额度/积分耗尽。补额度前重试无意义。 |
| 42908 | 429 | 是 | 上游全部处于冷却中,是临时态。退避后重试。 |
| 50201–50209 50230 / 50233 / 50234 | 502 | 谨慎 | 多为配置类问题,重试通常无效;但 50204/50205/50207/50208 属网络抖动,可退避重试 1–2 次。 |
| 50401 | 504 | 是 | 上游超时。退避重试,同时考虑调低 max_tokens。 |
| 50001 / 50002 | 500 | 谨慎 | 内部错误。重试一次;持续出现请反馈。 |
除了 429,其余 4xx 都是「你的请求有问题」。循环重试只会白白消耗你自己的限流配额, 把本来能成功的请求也一起挤掉。
限流响应头
网关返回 429 时会附带这几个头,直接照着做退避即可:
| 响应头 | 含义 |
|---|---|
| Retry-After | 建议等待的秒数。这是最该用的一个。 |
| X-RateLimit-Limit-Requests | 请求数上限 |
| X-RateLimit-Remaining-Requests | 剩余请求数(触发限流时恒为 0) |
| X-RateLimit-Limit-Tokens | token 数上限 |
| X-RateLimit-Remaining-Tokens | 剩余 token 数(触发限流时恒为 0) |
参考实现
指数退避 + 抖动 + 遵守 Retry-After,这是一个够用的骨架:
import random, time
import httpx
# 只有这几个码值得重试;其余一律直接抛给调用方
RETRYABLE = {42901, 42902, 42903, 42904, 42905, 42908, 50401, 50204, 50205}
def call_with_retry(payload, max_attempts=4):
for attempt in range(max_attempts):
r = httpx.post(
"https://www.apigoto.com/v1/chat/completions",
headers={"Authorization": f"Bearer {API_KEY}"},
json=payload,
timeout=120,
)
if r.status_code < 400:
return r.json()
body = r.json()
code = body.get("code")
if code not in RETRYABLE or attempt == max_attempts - 1:
raise RuntimeError(f"[{code}] {body.get('message')}")
# 服务端给了 Retry-After 就听它的,没给就指数退避
wait = int(r.headers.get("Retry-After", 0)) or 2 ** attempt
# 抖动:避免一批客户端同时被限流后又同时重试,把峰值原样搬到下一秒
time.sleep(wait + random.uniform(0, 0.5))
生产接入建议
-
日志里记
code,不要只记 message。 message 是给人看的、可能随版本调整措辞;code是稳定契约。 - 给请求设超时。推荐读超时 120 秒起步;长文本生成或推理型模型要更长。 超时设太短的表现是「客户端报超时但账单照扣」——上游其实还在生成。
- 重试上限 3–4 次。再多不会提高成功率,只会拉长故障时的响应时间。
- 连续失败要熔断。上游整体不可用时(大量 502/504),继续打只会加剧拥塞。
- 流式请求的重试要谨慎。已经吐出部分内容的流不能简单重发——见 流式响应 里关于终止事件的判据。
单个模型挂多个上游时,网关在收到上游失败后会自动换一条链路重试。 等错误传到你手上,说明所有可用链路都试过了—— 所以客户端侧的重试次数不需要设得很大。