错误处理与重试

网关的错误分两类来源:网关自己产生的上游厂商透传回来的。 先学会一眼分清这两类,剩下的处理逻辑就都顺了。

网关错误的结构

凡是网关自己拒绝的请求,返回体都是这个形状:

gateway error
{
  "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 位序号

code = HTTP + seq
  401  01
  └┬┘  └┬┘
   │    └── 序号:同一状态码下的第几种原因
   └─────── HTTP 状态码

所以看到 42905 就知道 HTTP 是 429(限流),看到 50201 就知道是 502(上游侧)。 唯一的例外是 41301(请求体过大),它的 HTTP 状态是 413。 全部错误码见 错误码总表

上游错误

如果请求已经成功路由到上游厂商,而厂商返回了错误,网关原样透传:状态码、 响应体结构都是厂商的原始格式,不做包装。这意味着:

哪些能重试

错误码HTTP可重试怎么做
40000、40002–40004400 请求本身错了,重试一万次也一样。改请求。 (40005 已于 2026-08-24 停用,不会再出现; 40001 归在下面的 401 一档。)
40100–40103401 密钥问题。见 鉴权与 API Key
40201–40203403 资格不足。开通计费或换模型,见 计费 / 模型准入
41301413 请求体超 100 MiB。拆小。
42901–42905429 限流。按 Retry-After 等待后重试。
42906 / 42907429 额度/积分耗尽。补额度前重试无意义。
42908429 上游全部处于冷却中,是临时态。退避后重试。
50201–50209
50230 / 50233 / 50234
502 谨慎 多为配置类问题,重试通常无效;但 50204/50205/50207/50208 属网络抖动,可退避重试 1–2 次。
50401504 上游超时。退避重试,同时考虑调低 max_tokens
50001 / 50002500 谨慎 内部错误。重试一次;持续出现请反馈。
🚫
不要对 4xx 做无脑重试

除了 429,其余 4xx 都是「你的请求有问题」。循环重试只会白白消耗你自己的限流配额, 把本来能成功的请求也一起挤掉。

限流响应头

网关返回 429 时会附带这几个头,直接照着做退避即可:

响应头含义
Retry-After建议等待的秒数。这是最该用的一个。
X-RateLimit-Limit-Requests请求数上限
X-RateLimit-Remaining-Requests剩余请求数(触发限流时恒为 0
X-RateLimit-Limit-Tokenstoken 数上限
X-RateLimit-Remaining-Tokens剩余 token 数(触发限流时恒为 0

参考实现

指数退避 + 抖动 + 遵守 Retry-After,这是一个够用的骨架:

retry.py
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))

生产接入建议

💡
网关内部已经替你重试过一轮

单个模型挂多个上游时,网关在收到上游失败后会自动换一条链路重试。 等错误传到你手上,说明所有可用链路都试过了—— 所以客户端侧的重试次数不需要设得很大。