错误与状态码

Max PrilutskiyCEO 兼联合创始人预计阅读 2 分钟

调用在开发环境中一切正常。现在,你要编写生产环境中的错误处理逻辑——catch 块。单看第三方 API 返回的 HTTP 错误,往往让人摸不着头脑:只有一个红色状态码,却回答不了凌晨三点最关键的问题——到底是我的请求、密钥、套餐出了问题,还是对方的服务器出了问题?这些错误中,哪些该重试,哪些该展示给用户?

Lingo.dev 用统一的结构给出答案。本组页面所述端点返回的每个错误,无论同步还是异步,都采用相同的 JSON 对象,状态码也都来自同一张固定的表。/v2 下的平台 API 则使用标准的问题详情格式,详见平台 API 错误。状态码不只是标签,更是行动指引:它告诉你应该修正请求、轮换密钥、为账户充值、退避等待,还是重试。读懂状态码,就知道下一步。一个按状态码分支处理的错误处理器,就能覆盖整个 API。

本页内容

错误结构#

每个非 2xx 响应都采用相同的响应体结构:一个 JSON 对象,仅包含一个描述出错原因的 message 字段。

json
{
  "message": "Invalid API key"
}

完整约定就这么简单。无需拆解外层封装,也无需针对不同端点的错误格式做特殊处理。/process/localize 返回的 400 与查询任务时返回的 404 结构相同——区别仅在于状态码和 message 文本。

按状态码匹配,而不是按消息文本

HTTP 状态码才是稳定可靠的信号,错误处理逻辑应以它为依据进行分支。message 字符串是写给查看日志的人看的;请将它视为描述,而不是机器可读的错误码,不要按其具体措辞进行模式匹配。

状态码#

七个状态码涵盖所有响应。这里按由谁解决分组,因为这也决定了你的重试策略。

你发送的请求存在问题(修正请求,不要盲目重试):

状态含义
400 Bad Request请求验证失败:缺少字段、语言区域无效、callbackUrl 使用了 HTTP 而非 HTTPS,或请求载荷格式不正确。
401 UnauthorizedX-API-Key 标头缺失或无效。请参阅身份验证。
403 Forbidden密钥有效,但无权访问所请求的资源。
404 Not Found所请求的资源(引擎、任务或任务组)不存在。

你的组织已达到账户限制(需通过账单管理解决):

状态含义
402 Payment Required组织已达到信用额度上限。
429 Too Many Requests组织已达到每日 token 配额。升级套餐可提高上限。

我们这边出现了故障(暂时性错误,可以重试):

状态含义
500 Internal Server Error发生意外故障:数据库出错,或翻译调用在引擎配置的所有模型上均失败。

401 和 403 看似相近,实际却是不同的问题:401 表示我们完全无法识别调用方,403 则表示我们识别出了密钥,但该密钥无权访问。解决 401 要从密钥本身入手(轮换或检查密钥);解决 403 则要调整密钥的访问权限。

哪些错误应该重试#

审慎的集成开发者看到错误表时,首先会问一个通常被忽略的问题:这些错误中,哪些应该重试?上面的分组就是答案。

  • 4xx——不要盲目重试。400、401、403 或 404 表示你的请求存在问题。原样重试,只会得到相同的错误。先修正输入、密钥或资源 ID,再重新发送。
  • 402 和 429——先退避等待,再解决限制。从请求层面看,这些并非暂时性错误;只要相应的限制没有变化,下一个请求仍会碰壁。不要在循环中连续重试,而应展示限制信息,并解决问题(充值或升级套餐)。
  • 500——采用退避策略重试。这是唯一真正属于暂时性错误的类别。500 可能意味着此次调用中所有已配置的模型都超时了;重试则可能调用到运行正常的模型。请使用指数退避,并设置重试次数上限。

异步 API 以不同方式报告结果

此重试策略适用于你自行发起的同步调用。异步本地化 API 不会通过状态码告知任务的最终结果:请求被接受后,POST 就会返回 202,随后每个目标语言区域都会通过持久化后台工作流,作为独立任务运行。你需要轮询任务或接收 webhook 来获取结果,而不是在原始调用中捕获状态码。请参阅在哪里查看异步任务错误。

402 与 429:两种不同的限制#

这两个账户级状态码看起来很像,都像是在说“用完了”。但混淆二者,就会让开发者找错解决方向。它们代表不同的限制,也需要不同的解决方法:

  • 402 Payment Required——组织已达到信用额度上限。这是计费限制。在组织的计费状态发生变化之前,后续调用仍会失败。
  • 429 Too Many Requests——组织已达到每日 token 配额。这是会重置的用量上限,可通过升级套餐提高。

错误处理器需要区分二者,原因在于:402 需要人工处理计费问题;429 则是配额问题,可以等待配额重置,也可以通过升级套餐提高配额。如果一律显示笼统的“付款问题”,操作人员就无法知道究竟该采取哪种措施。

402 的响应体与其他错误相同,只有状态码能表明这是信用额度限制:

json
{
  "message": "Organization has reached its credit limit"
}

在哪里查看异步任务错误#

这里需要划清一条界限:越过这条界限,状态码处理器就不再适用了。

本页的状态码属于传输层:它们说明 API 是否接受了你的 HTTP 请求,以及能否处理该请求。异步 API 返回 202 只意味着请求已被接受,并不代表翻译成功。异步任务可能顺利被接受,却在后续运行中因模型超时而失败。这类失败不会体现为原始调用的 HTTP 状态码,而是记录在任务本身。

因此,异步失败会在以下三个地方体现,而不在本表中:

  • 单个任务状态。某个语言区域处理失败时,对应任务会带有 status: "failed" 和 errorMessage。请参阅任务状态。
  • 任务组状态。部分语言区域成功、其他语言区域失败时,任务组会报告 partial,成功的语言区域仍会正常交付。请参阅跟踪任务组。
  • Webhook 投递。失败会以带有 error 字段的 translation.failed 事件投递。请参阅webhook 投递。

还有一个容易混淆的区别:非关键的流水线阶段失败,不会导致任务失败。任务仍会以 completed_with_warnings 完成,并附带各步骤的警告,而不是错误。这属于流水线可观测性问题,而非错误码问题。请参阅观测流水线运行。

平台 API 错误#

https://api.lingo.dev/v2 下的平台 API 采用不同的错误返回方式:它使用 RFC 9457 问题详情格式,并以 application/problem+json 发送:

json
{
  "type": "https://lingo.dev/docs/api/errors/glossary-in-use",
  "status": 409,
  "title": "Glossary in use",
  "detail": "Glossary gls_1 cannot be deleted while 2 engines apply it. Detach it from those engines first.",
  "id": "gls_1",
  "engineCount": 2
}
  • type 用于标识问题,是指向该问题专属页面的链接。该页面列出了问题的字段,以及会返回该问题的端点。请根据 type 进行分支处理。
  • status 重复列出 HTTP 状态码。
  • title 是简短摘要,同一类型的问题每次出现时,摘要都相同。
  • detail 说明本次发生了什么,以及接下来该怎么做。它是写给人看的,因此应直接展示,而不是解析。
  • 其他成员包含本次问题的具体信息,例如上面的 id 和 engineCount。detail 仅由这些信息生成,因此客户端可以根据 type 和这些字段,用自己的语言生成文本。

请求验证失败时会返回 400,并在 errors 下以 { "path", "message" } 对的形式列出每个无效值。每个端点还可能返回 service-unavailable(503),这类错误可以安全地采用退避策略重试;也可能返回 internal-server-error(500)。

各状态码分别对应以下规则:

状态含义
401未能识别调用方。
402套餐或权益限制,可通过升级或充值解除。
403已识别调用方,但不允许执行此操作。
404资源不存在,或调用方无法查看该资源。
409请求与当前状态冲突,例如资源仍在使用中。
422请求格式正确,但被某项规则拒绝。
429会随时间重置的限制。
502上游提供方发生故障。
503我们这边出现了暂时性故障。请采用退避策略重试。

后续步骤#

要写好错误处理器,可以先从集成时最早遇到的两个问题入手:身份验证,以及异步任务在何处报告自身结果。