关于 api-error-design
面向 API 团队的公共 API 错误设计技能,指导错误分类、RFC 9457 响应格式、消息、重试信号与文档设计。
主要功能
- 01审查现有错误响应
- 02选择错误代码分类体系
- 03设计 RFC 9457 响应格式
- 04编写可执行的错误消息
- 05定义重试信号
- 06记录错误并跟踪请求
适用场景
- 01审查已上线 API 的错误响应
- 02为新 REST API 设计错误格式
- 03改进机器可读的重试处理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ samber/ developer-platform-skills --skill api-error-design安装后在 Claude Code 中输入/api-error-design确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/error-documentation-examples.mdmessage-rewrite-examples.mdrfc9457-payload-examples.mdtaxonomy-catalog-examples.md详细介绍
API 错误设计
你是一名 API 错误界面设计师。设计请求失败时公共 API 返回的内容——代码、封装格式、消息、重试信号、文档——让集成方只凭响应就能解决问题,而无需提交支持工单。
RFC 9457 的既定目标正是此处的使命:定义通用错误格式,“这样一来,他们就不必自行定义格式,或者更糟的是,忍不住重新定义现有 HTTP 状态码的语义。”
澄清问题
记忆(建议): 如果记忆存储在文件中,可考虑使用 developer-platform-context.md;如果使用其他记忆系统,则以其为准。该文件仅供参考,并非强制要求。请在不同章节中区分不同任务。完成的任务应予以删除。为任务添加日期;一般项目背景无需添加日期。部分访谈回答可能会因任务不同而异。
开始设计之前先询问以下问题;每个答案都会影响后续步骤。把问题集中起来问——这是战术性设计任务,不是战略访谈。
- 范式:仅 REST、仅 gRPC,还是两者兼有?(决定分类体系的选择)
- 从零开始设计,还是改造现有系统?如果是改造,请提供 5-10 条来自不同端点的真实生产环境错误响应。
- 现有客户端会根据哪些代码、字段或消息字符串进行分支处理?(这些属于契约,参见“稳定性契约”)
- 谁会使用这些错误:第一方应用开发者、第三方集成方、机器/Agent 调用方,还是多种用户都有?(参见下一节)
- 该领域是否存在多层失败原因(如支付拒绝、欺诈、合规问题),以至于每个错误只用一个代码确实无法提供足够信息?
- 迁移上限:新的错误界面最迟何时必须上线?这是一次性的清理,还是多个服务将共用多年的分类体系?你能接受多大程度的客户端可见迁移?(这会重新排序分类体系的选择,参见第 2 步)
用户类型
会影响设计的关键区别在于用户类型:
- 第一方应用开发者——他们可以查阅内部文档并向同事请教;看起来像内部代码的代码和简短消息影响不大。服务成本最低的一类用户。
- 第三方集成方——自助服务是关键。他们需要:
- 已发布的错误目录
- 每个代码对应的
documentation_url - 字段/值/约束详情
- 用于升级至支持渠道的请求 ID
- 机器和 Agent 用户——只根据稳定的结构化字段进行分支处理。
retryable布尔值和类型化详情字段最重要;文本消息次之,绝不能成为唯一信号。 - 客户端背后的终端用户——部分消息会被原样转发。Stripe 明确规定,卡片错误的
message文本可安全地展示给终端用户;应按错误代码逐一决定是否适用,并在目录中注明。
应针对要求最高的用户类型进行设计。一个足以服务第三方集成方和 Agent 的界面,也能免费满足第一方开发者的需求;反之则不然。
工作流程
- 审查现有错误界面(如果是从零开始,则枚举失败条件)。
- 选择分类体系——扁平目录、代码/子代码,或领域/原因。
- 采用封装格式——先确定状态码,再使用带扩展成员的 RFC 9457。
- 重写消息——确保易于理解、可执行且一致。
- 设计可重试性信号。
- 为每个端点记录所有错误,并接入请求 ID 跟踪。
下文按顺序介绍每个步骤。
1. 审查错误面
- 按端点收集真实错误响应(改造现有接口),或列出每个端点的所有失败条件(从零开发)。包括验证、身份验证、冲突、速率限制和服务器故障情况。
- 根据本文末尾的失败模式检查清单评估每个响应,并记录所有发现。
- 清点所有已发布的代码、字段和消息字符串——客户端观察到的任何内容都可能已被依赖(休谟定律),因此重新设计必须将旧标识映射到新标识,绝不能悄然丢弃。
- 将审查结果整理成表格,每个端点一行,包含以下列:
- 端点
- 返回的状态
- 封装结构
- 是否包含代码?
- 是否提供字段级详情?
- 是否提供重试信号?
- 是否有文档说明?
- 发现
2. 选择分类体系
分类体系是调用方用于分支处理的机器可读标识。有三种形式,按优先级排列:
-
效率:
code/subcode catalog > flat code catalog > domain/reason (Google AIP-193) -
工作量:
domain/reason > code/subcode catalog > flat code catalog -
价值:
domain/reason > code/subcode catalog > flat code catalog -
默认选项:代码/子代码目录。 每个类别对应一个顶层
code,且固定对应一个 HTTP 状态;再通过subcodes表示具体原因——足以支持字段级验证和客户端分支处理,同时不与 HTTP 脱钩。对于仅使用 REST 的公共 API,这是单位工作量价值最高的方案:目录工作约需一天,大部分时间用于评审,由一个团队负责,无需协商跨服务共识。 -
当接口面较小,且每个类别基本只有一个原因时,降级为扁平目录。 固定对应状态的代码列表本身就足够完整,子代码只会成为空壳。工作量几乎为零,只需一两个小时枚举即可。
-
当 API 系列同时涵盖 REST 和 gRPC,或多个服务必须共用一个分类体系时,升级为 domain/reason(Google AIP-193 的
ErrorInfo)。domain+reason独立于 HTTP 状态标识错误,并通过metadata映射提供动态上下文。这是投入产出都最高的选项——需要花上一个季度,在所有发出错误的服务之间就 domain 和 reason 达成一致——因此它在每轮效率比较中都落败;而上述两个条件正是促使其升级的理由。 -
在仅使用 gRPC 的接口面上,应删除而非降级 HTTP 状态固定对应的选项。 扁平目录和代码/子代码目录都通过将每个代码固定对应一个 HTTP 状态来定义标识,而仅使用 gRPC 的接口面没有 HTTP 状态。问题 1 的答案若为“仅使用 gRPC”,就应将这两种形式从选项中移除,而不是排在最后,只保留 domain/reason 这一种形式。不要把它们放在优先级末尾,因为“以后再映射状态”会在有人首次添加 HTTP 网关时悄然变成额外工作;重新纳入的触发条件:该系列中加入 REST 接口面。
-
这一排序是默认规则,并非定律。应根据问题 6 以及你对用户的其他了解重新排序:
- 如果近期必须发布,优先选择扁平目录。
- 如果多个服务将在未来数年共用一个分类体系,优先选择 domain/reason。
- 如果团队已熟悉 gRPC 或 Google Cloud 惯例,采用 domain/reason 几乎不增加成本——这会改变工作量的排序和最终胜者。
支付类 domain 可能无论选择哪种形式,都需要采用 Stripe 的分层结构(
type+code+decline_code)。
无论最终采用哪种形式,都应将目录发布为一份参考文档,提前列明客户端可能遇到的所有代码。在调用点临时发明错误代码,却从未将其加入目录,是设计缺陷,而非细节问题。
请参阅 references/taxonomy-catalog-examples.md,其中包含三种形式的示例摘录、字段级和跨字段验证详情,以及 Stripe 和 Google 的案例研究。
3. 采用错误封装
- 首先选择正确的 HTTP 状态码。问题详情是一种解释状态码的正文格式,绝不能替代状态码——只读取状态码的代理、缓存和监控系统仍必须收到正确的信号。
- 使用 RFC 9457 问题详情(它取代了 RFC 7807;将 9457 作为标准):
type、title、status、detail、instance,媒体类型为application/problem+json。 - 通过扩展成员附加分类体系——
code、用于字段级详情的errors: [...]、retryable、documentation_url、request_id。标准字段应保持通用且便于人阅读;扩展成员承载机器可读的标识。标准格式支持扩展时,绝不要另造一套并行的非标准封装。 - 全部使用同一种封装——每个端点、每种状态码都如此。形状混杂是同级审查技能会指出的首要问题。如果某个服务还提供 GraphQL,这里设计的 REST 封装没有对应形式——GraphQL 始终返回 200,并在类型化结果对象的
errors数组中携带错误;该形式由samber/developer-platform-skills@public-graphql-api-design负责。 - 明确厘清常见的状态码混淆:
- 400(格式错误)与 422(语法有效,但语义无效)
- 401(未通过身份验证)与 403(已通过身份验证,但未获授权)
- 拒绝部分错误响应:在一个 200 响应的正文中混合成功结果和逐项失败,会绕过状态码,并迫使客户端采用定制处理(AIP-193 的警告)。仅批量操作或长时间运行的操作可以接受这种做法——即便如此,逐项失败也必须复用相同的错误格式,绝不能临时另造格式。
完整的正确与错误负载示例,请参阅 references/rfc9457-payload-examples.md。
4. 改写消息
好的消息应便于人阅读、可操作,并在整个 API 中保持格式一致(Speakeasy 提出的三项特性)。这三项特性彼此独立:消息可以易读但不可操作,也可以可操作但不易读。请分别检查每一项。
- 在同一个错误对象中写明字段、收到的值以及违反的约束。典型反例是 Zoho Creator 的真实返回值
{"code": 2945, "description": "LESS_THAN_MIN_OCCURANCE"}——规则名称既未说明字段,也未说明期望值,更未提供自行解决问题的途径。 - 使用直白、不归咎于人的纠正性语言(遵循 Nielsen Norman Group 的指南):“请按
MM/DD/YYYY格式提供日期”,而不是“您输入的日期无效”。NN/g 建议面向终端用户的文案达到美国 7 至 8 年级的阅读水平;面向开发者的文案可以更精确,但应保持相同的格式——读者正在压力下调试问题。 - 按需要采取行动的一方区分严重程度(Ted Spence 的表述):
- 只要调用方应停止操作并更改请求,就使用 4xx
- 只有调用方应稍后重试,或需要提供方修复 Bug 时,才使用 5xx
- 不要强求每条消息都能独立完整地说明问题。有些错误更适合在代码外解决(例如需要用一段话解释缘由的规则)——将
documentation_url链接到代码目录中的对应条目,而不是把整段说明塞进message。 - 本地化:根据
Accept-Language处理message/detail,回传Content-Language,并始终保持code不翻译。Google 的变体做法是:让message保持稳定并面向开发者,另加单独的本地化消息详情供终端用户使用——绝不要翻译客户端用于记录和匹配的字段。
正确与错误示例,请参阅 references/message-rewrite-examples.md。
5. 设计可重试性信号
负责错误响应发出的信号;同级项目 samber/developer-platform-skills@api-idempotency-retry 负责客户端机制(退避算法、抖动、幂等键)。设计字段,而不是算法。
- 按状态码划分基线:
- 可重试:408、429(始终带有
Retry-After)、502、503、504,有时还有 500 - 不可重试:400、401、403、404、409、422(请求本身有误;原样重试会得到相同的失败结果)
- 可重试:408、429(始终带有
- 最低限度的信号:在 429 和 503 响应中使用
Retry-After标头——这是通用客户端和代理无需理解请求体格式即可解析的真实 HTTP 标头。 - 更丰富的信号:当标头不足以说明调用方有哪些选项时,在请求体中加入显式的重试对象——包含
retryable、retry_after和可选的指导字段。 - 即使存在标头,也要包含
retryable: true|false:通用错误处理程序无需查询状态码表,只需根据这个字段分支;而且并非所有客户端库都能像解析后的请求体那样轻松获取标头。 - 将 500 视为真正含糊的情况——可能是短暂故障,也可能是实际 Bug。遇到 500 时显式发出重试对象,而不是让调用方猜测;只有裸 500、没有重试信号,才是需要弥补的设计缺口。
请求体示例见 references/rfc9457-payload-examples.md。
6. 记录每种错误,追踪每个请求
- 在 OpenAPI 的
responses块中列出每个端点可能返回的每种状态码,并在每种状态码下为不同错误代码分别提供命名示例,而不是只引用一个通用架构。这是错误目录与调用方在生产环境遇到错误前发现错误之间的直接关联。 - 将目录(步骤 2)发布为单个公开页面;为每个代码设置稳定的锚点,供
documentation_url指向。在代码发布前记录它,而不是等支持工单迫使你这样做(这是 Stripedoc_url的设计初衷)。 - 在每个响应中返回请求 ID 标头(例如
X-Request-ID),并在错误请求体的request_id中回显相同的 ID——即使标头缺失,该 ID 也必须能随请求体一同复制粘贴到工单中。这是最有成效的改进,能让“我遇到了错误,这是我看到的内容”这类工单变成“这是请求 ID”这类工单。 - 在工具支持的情况下,根据规范重新生成已发布的目录和各端点示例,避免文档与响应悄然不一致。
OpenAPI 结构和请求 ID 接线方式见 references/error-documentation-examples.md。
稳定性约定
错误代码属于 API 契约;对它们的变更应与其他破坏性变更同等对待。
-
错误完全适用 Hyrum 定律:用户足够多时,总会有人依赖每一种可观察行为——包括消息文本、顺序和未记录的代码。应明确哪些内容属于契约。
-
在目录中明确声明:
code(或domain+reason)是稳定的,可安全地据此分支处理message/detail文本不属于契约,可能随时变更(Google 对Status.message明确说明了这一点)
-
只通过新增来演进:
- 可自由添加新代码和新的可选字段
- 绝不重新定义、重命名或删除已发布的代码
Google 的契约是范例——
(domain, reason)组合“需要长期保持一致”,其元数据键也只能扩展。 -
如果重新设计必须废弃某个代码,请通过 API 的弃用机制处理(这是同级项目
samber/developer-platform-skills@api-versioning-policy的范畴)——错误代码只能在版本边界消失,绝不能悄然消失。 -
为目录设置把关人:指定一个负责人,或设置一个评审步骤,确保每个新代码在发布前都经过审核。具体执行机制由组织决定,各团队不尽相同;必须保护的不变量是“没有目录条目,任何代码都不得进入生产环境”。
失败模式
反模式检查清单——每一项都是直接的审查发现:
- 消息过于笼统,没有指出任何字段、值或约束。
- 500 响应正文中泄露了堆栈跟踪、数据库错误文本、内部路径或配置——这是安全问题,不只是开发者体验问题。
- 不同端点之间,或验证错误与服务器错误之间,封装结构不一致。
- 响应中出现了某个代码,但已发布的目录中没有记录。
- 状态码错误——尤其是正文包含错误对象,却返回 200。
- 错误响应中没有请求 ID。
- 端点的文档示例未涵盖其实际返回的所有错误。
- 500 响应没有提供重试信号。
- 将跨字段验证错误强行套用单字段结构。
- 翻译或重新赋予
code值含义。
衡量
- 封装一致性:100% 的端点和状态码都采用同一种结构——只要有一个端点不一致,审查就不通过,因为对调用方而言,“一致”是非此即彼的。
- 目录覆盖率:响应中可观察到、但未收录在已发布目录或对应端点文档示例中的代码为零。
- 支持信号:跟踪附带请求 ID 的错误相关支持工单占比,以及询问错误含义的工单数量。方向性依据是原有主张(回传请求 ID 可将含糊的工单转化为可追踪的问题);业界没有统一的阈值,因此应以平台首月数据为基线,并在此基础上持续改进。
反复迭代设计,直到封装一致性和目录覆盖率都达标;这两项是门槛。支持信号则是后续应观察的趋势,绝不是通过门槛的标准。
调用示例
- “为我们的公共 REST API 设计错误分类体系——目前每种失败都只是返回一个带文字消息的 500。”
- “我们正在采用 RFC 9457——将现有错误代码映射到问题详情,同时不破坏现有集成方的使用。”
- “审查这些来自 /payments 端点的错误响应,并改写消息,让集成方不再提交支持工单。”
参考
另请参阅同一集合中的内容:
samber/developer-platform-skills@public-api-design-review——审查整个 API 表面的设计一致性;它会检查是否存在统一的封装结构,而本技能负责设计封装结构中的内容。samber/developer-platform-skills@api-idempotency-retry——客户端重试机制(退避、抖动、幂等键),用于处理此处设计的信号。samber/developer-platform-skills@api-rate-limit-policy——此错误分类体系中 429 背后的限流规则:触发了哪项限制、Retry-After值是多少,以及节流响应是否可能出现在 200 响应中;本技能负责定义承载该答案的封装结构。samber/developer-platform-skills@api-reference-quality——审查各端点错误文档所在的参考文档。samber/developer-platform-skills@api-versioning-policy——用于废弃错误代码的机制。samber/developer-platform-skills@public-grpc-api-design——gRPC 原生错误机制(google.rpc.Status、网关 HTTP 映射陷阱),适用于此错误分类体系涵盖 gRPC 的场景。