API 的 Spec-First 错误处理模式

API 的 Spec-First 错误处理模式
Daniel Marsh · Spec-First 工程笔记

错误处理是 API 规格里最容易被含糊带过的一块。所有人都同意它很重要,但没人把它写下来,然后到了联调那一周,团队就开始争论超时到底该是 500 还是 503、客户端该不该重试。规格的职责就是在任何人动手写 handler 之前,把这些问题全部回答掉。

发布于 2026-03-01 · ✓ 已更新 2026-05-06 · 阅读约 6 分钟 · 作者:Spec Coding 编辑团队 · 审校:编辑政策

一份信封,所有端点

我最坚持的第一条决策,是整个 API 只用一个错误信封。不是每个端点一种形状,也不是每个团队一种形状。就一个对象,字段稳定到每个客户端都能依赖:一个稳定的错误代码字符串、一条给人看的消息、一个 retryable 布尔值、一个用于校验失败的可选字段路径,再加一个 request id。如果一个端点返回 {"error": "..."}、另一个返回 {"message": "..."}、第三个返回 {"errors": [...]},每个客户端都得写三套适配器,然后把它们塞进一个 switch 里。

把信封写进规格几乎不花什么成本。等到六个端点已经用不同形状发布出去之后再来改,就是一整个季度没人愿意认领的活。

我真正在用的 4xx vs 5xx 判据

我的试金石是这样:如果客户端把完全相同的字节再发一次,请求成功了,那就是 5xx;如果客户端必须改点什么才能成功,那就是 4xx。就这么简单。数据库瞬时抖动不是 400,格式错误的 JSON 不是 503。我把这句话一字不差地写进我写的每一份 API 规格里,因为工程师一累,这条线就开始模糊。

有趣的边界情形要单独明确:唯一约束冲突是 409,不是 500,因为是客户端引起的;下游供应商超时是 5xx,因为重试同一请求是可能成功的;限流是 429,因为客户端稍后用同样的字节就能成功,只是此刻不行。

校验:选 400 还是 422,然后别再吵了

我不在乎你选哪个,我在乎的是规格得选一个。团队会为 400-vs-422 的争论浪费好几周时间,而这本来一行规格就能解决。我的默认是:语义校验失败(字段值不对、取值互相冲突、业务规则违反)用 422;语法失败(JSON 格式错误、缺必填字段、Content-Type 不对)用 400。但任何一致的规则都行,不一致才是 bug。

在文档里写这条规则时,把它和信封定义写在一起,让人不可能漏看。然后每个 handler 在评审时都指向同一页。

批量端点里的部分成功

批量端点是规格悄悄崩掉的地方。你 POST 10 条到 /bulk-update,3 条校验失败、7 条成功。服务端有三种合理选择,规格必须押注其中之一:

三种都说得通。在规格里选一个,意味着客户端只写一条代码路径;不选,意味着每个客户端都把三种都写了,然后猜错的时候来提工单。对于一次 POST /items/bulk 十项中有三项校验失败的情形,我默认的响应体长这样:

HTTP/1.1 200 OK
{
  "request_id": "req_7fK2...",
  "results": [
    {"index": 0, "status": "ok", "id": "itm_01"},
    {"index": 1, "status": "ok", "id": "itm_02"},
    {"index": 2, "status": "error", "error": {
      "code": "validation_failed",
      "message": "price must be non-negative",
      "field": "price",
      "retryable": false
    }},
    {"index": 3, "status": "ok", "id": "itm_04"},
    ...
  ],
  "summary": {"ok": 7, "error": 3}
}

注意每条子错误上的 retryable: false。这是我一直在坚持的第二件事。

把 retryable 显式地写出来

我的信封里每个错误都带一个 retryable 布尔值。不靠状态码隐含、不靠错误消息推断、不靠月相猜测。服务器说 retryable: true,客户端就带退避重试;说 false,客户端就把错误往上抛、停下来。这一个字段让每一个 SDK 作者都免于手写"状态码到重试策略"的映射表,也让服务端以后可以改变主意而不打破客户端。

retryable 这个字段还会逼出一场设计讨论。如果一个端点会返回 retryable: true,规格就必须同时说明它是不是幂等的。没有幂等键的重试,就是向客户重复扣款的那条路。

幂等性和限流要进规格

任何有副作用、又宣称失败可重试的端点,都需要一份 Idempotency-Key 请求头契约。规格要说明:这个 key 是客户端生成的字符串,保留 24 小时,重放会返回原始响应并带上 X-Idempotent-Replay: true 响应头。写一次,所有 POST、PATCH、DELETE 都复用这个模式。

限流也一样。429 配 Retry-After 是基本盘。规格还必须写清限流的维度:是按 API key、按 IP、按组织、按路由、按分钟、还是按小时。知道自己"每个 key 每分钟 1000 次"的客户端会去搭令牌桶;只看到 429 的客户端只能瞎猜。

Auth、下游和 Webhook 的信号

有三处规格必须表态、但通常没有表态的地方:

用 Given/When/Then 写验收标准

上面这些规则只有被写成验收标准才是可执行的。下面是我给前面那个批量端点写的块:

- Given a POST /items/bulk with 10 items
  And 3 items fail server-side validation
  When the request is processed
  Then the response status is 200
  And the body contains results[] with status "ok" or "error" per item
  And the summary totals match the results array
  And each error has code, message, field, and retryable=false

- Given a POST /items/bulk during a downstream outage
  When any item fails due to the outage
  Then the entire response status is 503
  And the envelope error.retryable is true
  And Retry-After is present

- Given a repeated POST with the same Idempotency-Key within 24h
  When the original request succeeded
  Then the response body matches the original
  And X-Idempotent-Replay is true

三个块,然后实现、测试和客户端 SDK 都有了同一份参考。

我拒绝放过的几件事

我在评审一份 API 规格时,会扫六样东西,少一样就把草稿打回:错误信封、4xx/5xx 判据、校验状态码的选择、部分成功模型、retryable 标志、幂等性契约。其他东西都可以以后再澄清,这六样不行,因为它们决定了每一个将来要接入的客户端长什么样。写下来花一小时,不写下来让团队赔上一个季度。

错误契约清单

错误处理可靠的前提,是客户端能根据稳定字段分支,而不是解析一段文字。让契约保持小、明确,并同时从服务端和客户端测试。

下载:api-error-runbook.md

上线前准备三份错误样本

我不太信“错误处理已经统一”这种说法。上线前直接看样本最快。服务端、前端和 SDK 都用同一组样本跑一遍,很多争议会立刻暴露。

错误样本包

validation_failed:
- status: 422
- retryable: false
- field: email
- user copy: 邮箱格式不正确

dependency_timeout:
- status: 503
- retryable: true
- Retry-After: 30
- client action: 延迟重试,不显示成功

idempotent_replay:
- status: 200
- retryable: false
- header: X-Idempotent-Replay: true
- client action: 复用原结果,不再次提交

样本要进契约测试,也要进 SDK 文档。否则服务端以为自己讲清楚了,客户端却还在靠字符串判断错误类型。

需要追踪的证据

追踪让客户端困惑的工单、未分类的 5xx、重试风暴、缺失的 correlation ID,以及 SDK 无法区分终止错误和可重试错误的分支。这些信号能说明错误规格是在发挥作用,还是已经漂移。

专题阅读路径

这篇文章归入 API 契约 主题。先读 Hub,再结合下面的清单、模板或工具落到具体项目里。

交互式生成规格
填写表单,生成完整的功能规格 Markdown——免费使用,无需注册。
试用规格生成器

编辑说明

最近复核:2026-04-28。编辑部检查了示例、内链和可复制评审片段,确保内容更适合真实项目使用。

本文面向软件交付团队,介绍API 的 Spec-First 错误处理模式。示例均为工程场景说明,不构成法律、税务或投资建议。