API 错误分类:从清理需求到契约规格

这个案例展示如何把“优化 API 错误”这种模糊需求,改写成 SDK、AI 生成客户端和评审者都能依赖的稳定契约。

风险客户端兼容与重试
边界只改 envelope,不改 endpoint 行为
证据Fixtures、SDK 示例、OpenAPI examples

写 spec 之前的需求

弱工单

优化 API 错误。
让客户端更容易处理。

Spec-First 改写

Feature: Stable API error taxonomy
Owner: API Platform
Status: Draft for review

Goal:
- 统一 error code、category、message、trace_id 和 retryable。
- 保持现有 HTTP status 行为。
- 让生成客户端不用解析字符串就能分支处理。

Non-goals:
- 不改变 endpoint 业务逻辑。
- 第一版不移除 legacy 字段。
- 不新增认证策略。

阻止隐性破坏的契约

error-envelope.md

{
  "error": {
    "code": "ORDER_ALREADY_EXISTS",
    "category": "conflict",
    "message": "An order already exists for this idempotency key.",
    "trace_id": "req_01H...",
    "retryable": false,
    "details": {}
  }
}

acceptance-criteria.md

- Given 参数校验失败
  When API 返回 422
  Then error.category 是 validation,且 details.field_errors 存在。

- Given 重复 idempotency key
  When API 返回 409
  Then error.retryable 是 false,code 保持稳定。

compatibility.md

- 保留 legacy top-level message 一个版本。
- 新 envelope 字段只做 additive response change。
- SDK fixture 发布后再更新文档示例。
- 迁移期间记录 unknown category 日志。

test-evidence.md

自动化:
- 400/401/409/422 契约 fixtures
- SDK parser 兼容测试
- OpenAPI example snapshot

手工:
- client support 已评审发布说明
- 日志包含 category 和 trace_id

为什么它应该进入规格包

区分形状和行为

spec 把变更限制在 error envelope,避免实现漂移到 endpoint 语义。

保护生成客户端

稳定 category 和 retryable 字段给 AI 生成 SDK 一个机器可读的分支点。

留下迁移证据

fixtures、示例和日志让兼容性在发布前可见,而不是等工单来了再确认。

评审时应该看什么

关键评审动作,是把错误响应当成公开产品行为,而不是内部清理。模糊的“优化错误”需求很容易让实现重命名字段、合并 category,或改变 retry 行为,因为这些改动看起来“只影响错误”。规格包会把 envelope 固定成契约:第一版只允许 additive 字段,旧解析行为要保留一个迁移窗口,每个状态码都要有 fixture。

这个案例最重要的是 compatibility 文件。它避免团队在 SDK 还不能解析前就发布新示例,也让客服和文档拿到与真实响应一致的发布说明。现在很多用户会把 API 示例直接交给 AI 助手,过期示例可能在几周后变成生成客户端的 bug。

形状检查

每个响应都应保持相同 envelope key、稳定 category、trace identifier 和明确 retryable 字段,客户端不应该解析英文 message。

兼容检查

第一版应保留 legacy 字段,发布 SDK fixture,并在移除旧示例或改文档前记录 unknown category。

证据检查

评审者需要看到常见状态码契约测试、OpenAPI 示例快照,以及至少一个生成客户端 parser 示例。

如何把这个案例迁移到你的项目

任何会被开发者复制进代码的公开响应形状,都可以套用这个案例:REST 错误、webhook payload、GraphQL errors、SDK exceptions、CLI 输出或生成客户端示例。具体字段可以不同,但迁移纪律应该一致:先新增字段再移除旧字段,先发布 fixture 再重写文档,并给消费者一个不依赖 prose 的 retry 判断方式。

规格还应该写明谁会消费这个契约。内部前端、外部 SDK、集成伙伴、客服工具和 AI 助手,可能分别从不同示例学习。如果这些示例漂移,bug 会出现在 API 团队没有直接触碰的地方。所以证据清单里需要 SDK 解析和 OpenAPI 快照,而不只是服务端测试。

评审 AI 生成实现时,要严格处理意外行为变化。改变状态码、message 文案或 retry flag 的重构,看起来可能只是清理代码,但它会破坏客户端控制流。这类改动应先回到规格,而不是直接进代码。

评审时应拒绝的反模式

这些反模式之所以单独列出来,是因为它们很容易在人类评审和 AI 代码生成中被忽略。它们不一定立刻让测试失败,但会让 SDK、文档示例和用户复制的集成代码逐渐漂移,最后变成难排查的兼容问题。

靠 message 分支

客户端不应该通过解析英文句子判断是否重试。应提供稳定 code、category 和 retry flag。

先改文档再补 fixture

在 parser fixture 和 OpenAPI 快照证明新形状稳定之前,不要先更新示例。

静默删除字段

没有迁移窗口就移除 legacy 字段,会把清理项目变成破坏性变更。

复用这个案例前的最后检查

把这个案例用于自己的 API 前,先列出所有会读取错误响应的人和系统:前端、SDK、第三方集成、内部脚本、客服工具和文档示例。然后逐一确认它们能否接受新 envelope。如果有任何一类消费者不能同时升级,就必须保留兼容字段或延长迁移窗口。

还要检查日志和支持流程。新的 error category 如果不能在日志里搜索、不能和 trace_id 对上、不能帮助客服解释失败原因,它就只是漂亮字段。规格包需要证明这些字段会进入真实排障链路,而不是只出现在 JSON 示例里。否则页面看起来完整,实际仍然不能指导发布,也不能支撑评审和排障复盘。

上线后还要观察什么

API 错误规格不是合并后就结束。真正的验证来自客户端、文档和支持链路是否都开始使用同一套分类。

Unknown category

保留一个短期日志查询,确认新 envelope 没有产生无法分类的错误,也没有让旧 SDK 掉进默认分支。

文档示例漂移

发布后检查 OpenAPI examples、快速开始文档和 SDK README,避免读者复制到旧响应形状。

这些观察项应该在发布计划里有负责人和复查时间,否则错误分类会停留在代码层,无法进入真实支持和排障流程。

改 API 响应前,先套用这个模式

先生成规格包,再配合 API 契约检查清单,确保每个响应示例都有证据。

编辑说明

这个案例聚焦 API 契约安全:错误格式被视为公开行为,而不是内部清理。