2025年2月28日·阅读约1分钟

用于清晰、用户友好的消息的 API 错误契约模式

设计一个包含稳定代码、本地化消息和面向 UI 提示的 API 错误契约,减少支持工作并帮助用户快速恢复。

用于清晰、用户友好的消息的 API 错误契约模式

模糊的 API 错误为何会造成真实的用户问题

模糊的 API 错误不仅仅是技术层面的烦恼。这是产品中的一个中断点:有人被卡住、猜下一步该做什么,往往就放弃了。那句“出了点问题”会变成更多的支持工单、用户流失以及看起来永远无法彻底解决的 bug。

一种常见场景是:用户尝试保存表单,界面显示一个通用的提示,而后端日志里记录了真正的原因(比如 “unique constraint violation on email”)。用户不知道该修改什么。支持也无法帮忙,因为没有可搜索的、可靠的代码。相同的问题以不同的截图和表述反复出现,却没有办法把它们归为同一类。

开发者需要的细节和用户需要的指引不是同一件事。工程师需要精确的失败上下文(哪个字段、哪个服务、哪个超时)。用户需要一个清晰的下一步:“该邮箱已被使用。请尝试登录或换个邮箱。”把两者混在一起通常会导致要么泄露内部信息(不安全),要么信息毫无用处(把一切都隐藏起来)。

这正是 API 错误契约的用武之地。目标不是“多一些错误信息”,而是统一的结构,让:

  • 客户端在所有端点上都能可靠地解释失败;
  • 用户看到安全、通俗的提示,能快速恢复;
  • 支持和 QA 能用稳定的代码定位问题;
  • 工程师得到诊断信息,同时不暴露敏感细节。

一致性是关键。如果一个端点返回 error: "Invalid",另一个返回 message: "Bad request",UI 无法引导用户,团队也无法衡量发生了什么。一个清晰的契约能让错误可预测、可搜索、也更容易修复,即便底层原因在变。

一致的错误契约在实践中意味着什么

API 错误契约是一种承诺:当出现错误时,你的 API 会以熟悉的结构返回可预测的字段和代码,无论哪个端点失败。

它不是调试用的转储,也不能替代日志。契约是客户端可以安全依赖的内容;日志则保存堆栈跟踪、SQL 细节以及任何敏感信息。

在实践中,稳健的契约保持少量但稳定的部分:跨端点一致的响应形状(包括 4xx 和 5xx),不改变含义的机器可读错误代码,以及面向用户的安全文本。它还能通过包含请求/追踪标识帮助支持,并提供简单的 UI 提示,例如用户应该重试还是修正某个字段。

一致性只有在你决定在哪里强制执行它时才会生效。团队通常从一个强制点开始再逐步扩展:API 网关用于规范化错误、中间件包装未捕获的异常、共享库构建相同的错误对象,或在每个服务层使用框架级别的异常处理器。

关键期望很简单:每个端点要么返回成功形状,要么在任何失败场景下返回错误契约。包括验证错误、认证失败、速率限制、超时和上游故障。

一个可扩展的简单错误响应形状

好的 API 错误契约保持精简、可预测,并对人和机器都有用。当客户端总能找到相同字段时,支持不再猜测,UI 能提供更清晰的帮助。

下面是一个适用于大多数产品的最小 JSON 形状(随着端点数量增加也能扩展):

{
  "status": 400,
  "code": "AUTH.INVALID_EMAIL",
  "message": "Enter a valid email address.",
  "details": {
    "fields": {
      "email": "invalid_email"
    },
    "action": "fix_input",
    "retryable": false
  },
  "trace_id": "01HZYX8K9Q2..."
}

为了保持契约稳定,将每一部分视为单独的承诺:

  • status 用于 HTTP 行为和大致分类;
  • code 是稳定的、机器可读的标识(API 错误契约的核心);
  • message 是安全的 UI 文本(可以本地化);
  • details 保存结构化提示:字段级问题、下一步建议以及是否可以重试;
  • trace_id 让支持在不暴露内部细节的情况下找到精确的后端失败。

将面向用户的内容和内部调试信息分开。如果需要额外诊断信息,把它们记录在服务器端并用 trace_id 关联(不要放在响应里)。这样既避免泄露敏感数据,也方便问题调查。

对于字段错误,details.fields 是一个简单模式:键与输入名一致,值保存简短原因,如 invalid_emailtoo_short。只有在确实有帮助时才添加指导。对于超时,action: "retry_later" 就足够;对于临时故障,retryable: true 帮助客户端决定是否显示重试按钮。

在实现前需要注意的一点:有些团队把错误包裹在 error 对象里(例如 { "error": { ... } }),另一些则把字段放在顶层。两种方法都可以,重要的是选一个并在所有地方保持一致。

不会破坏客户端的稳定错误代码模式

稳定的错误代码是 API 错误契约的支柱。它们让应用、仪表盘和支持团队即便在你改写文案、添加字段或改进 UI 时仍能识别问题。

一种实用的命名约定是:

DOMAIN.ACTION.REASON

例如:AUTH.LOGIN.INVALID_PASSWORDBILLING.PAYMENT.CARD_DECLINEDPROFILE.UPDATE.EMAIL_TAKEN。保持 domain 简短且易懂(AUTH、BILLING、FILES),用可读的动作动词(CREATE、UPDATE、PAY)。

把代码当作端点:一旦公开,就不要改变其含义。展示给用户的文本可以随着时间变好(更好的语气、更清晰的步骤、新的语言),但代码应保持不变,这样客户端不会崩溃,分析数据也保持干净。

还需要决定哪些代码是公开的、哪些是仅限内部使用。一个简单规则:公开代码必须安全可展示、稳定、有文档并可供 UI 使用;内部代码则记录在日志中用于调试(数据库名、供应商细节、堆栈信息)。一个公开代码可以映射到许多内部原因,尤其是当一个依赖会以多种方式失败时。

弃用最好是枯燥的。如果必须替换代码,不要在没有说明的情况下重用旧代码来表示新含义。引入新代码并将旧代码标记为弃用。给客户端一个过渡期,在此期间两者都可能出现。如果包含字段如 deprecated_by,请指向新代码(不是 URL)。

例如,即便你后来改进了 UI 文案并把“尝试另一张卡”和“联系银行”分开,也要保留 BILLING.PAYMENT.CARD_DECLINED。代码保持稳定,而引导文本可以演进。

本地化消息且不失一致性

构建完整的应用工作流
添加认证、支付和消息模块,无需手工编码每个集成
开始使用

当 API 返回完整句子并且客户端把它们当做逻辑来处理时,本地化就容易陷入混乱。更好的做法是保持契约稳定,并把最终显示的文本本地化。这样,不管用户使用哪种语言、设备或应用版本,相同的错误都有相同的含义。

首先决定翻译的存放位置。如果你需要一个跨 web、移动和支持工具的单一源,服务端文本会有帮助。如果 UI 需要对语气和布局有严格控制,客户端本地化更容易。很多团队采用混合方式:API 返回稳定代码加上 message key 与参数,客户端选择最佳显示文本。

对于 API 错误契约,message key 通常比硬编码句子更安全。API 可以返回类似 message_key: "auth.too_many_attempts"params: {"retry_after_seconds": 300},UI 根据 key 翻译并格式化,而不改变含义。

复数形式和回退链比想象中重要。使用支持各语言复数规则的 i18n 方案,而不仅仅是英语式的“1 vs many”。定义回退链(例如:fr-CA -> fr -> en),这样缺失的字符串不会导致页面空白。

一个好的护栏是把翻译严格当作面向用户的文本。不要把堆栈跟踪、内部 ID 或原始失败原因放入本地化字符串。把敏感细节放到不显示的字段(或日志)里,给用户安全且可行的提示。

把后端失败变成用户可执行的 UI 提示

避免混乱的后端重写
生成生产就绪的源代码,并在需求变更时干净地重新生成
开始构建

大多数后端错误对工程师有用,但太多时候会以“出了点问题”出现在屏幕上。好的错误契约能把失败转成清晰的下一步,而不泄露敏感细节。

一种简单的做法是把失败映射到三类用户操作之一:修正输入(fix input)、重试(retry)或联系支持(contact support)。这让 web 和移动端的 UI 即便面对众多后端失败模式也能保持一致。

  • 修正输入:验证失败、格式错误、缺少必填字段。
  • 重试:超时、临时上游问题、速率限制。
  • 联系支持:权限问题、用户无法自行解决的冲突、意外的内部错误。

字段提示比冗长消息更重要。当后端知道哪个输入失败时,返回机器可读的指针(例如字段名 emailcard_number)和短原因以便 UI 行内展示。如果有多个字段错误,一次返回全部,这样用户可以一次性修复。

将 UI 表现与场景匹配也很有帮助:临时重试消息用 toast 就够了;输入错误要行内展示;账户或支付阻塞通常需要阻断性对话框。

一致地包含安全的故障排查上下文:trace_id、时间戳(如果已有)以及建议的下一步比如重试间隔。这样,支付提供者超时可以显示“支付服务响应较慢,请稍后再试”并带重试按钮,而支持团队可以用相同的 trace_id 找到服务器端的完整记录。

逐步推进:端到端部署契约

把 API 错误契约作为一个小型的产品改动来推进效果最好,而非一次性重构。保持增量,尽早让支持和 UI 团队参与。

一个能快速改善用户可见提示且不破坏客户端的部署顺序:

  1. 清点现状(按域分组)。 从日志导出现有错误响应,按域分桶,如 auth、signup、billing、file upload、permissions。找出重复、不清楚的消息以及同一失败以五种不同形态出现的地方。
  2. 定义模式并共享示例。 文档化响应形状、必需字段以及每个域的示例。包含稳定代码名、本地化的 message key,以及可选的 UI 提示部分。
  3. 实现一个中央错误映射器。 把格式化放在一个位置,让每个端点返回相同结构。在生成式后端(或无代码后端)中,这通常意味着每个端点或业务流程调用同一个“将错误映射为响应”的步骤。
  4. 更新 UI 去解析代码并展示提示。 让 UI 依赖代码,而不是消息文本。用代码决定是否高亮字段、显示重试操作或建议联系支持。
  5. 添加日志和支持可用的 trace_id。 为每个请求生成 trace_id,服务器端记录原始失败细节,并在错误响应中返回,方便用户复制提供给支持。

第一轮之后,用一些轻量级产物保持契约稳定:按域维护错误代码目录、本地化消息文件、从代码到 UI 提示/下一步的映射表,以及一个支持手册,开始阶段就是“把 trace_id 发给我们”。

如果有遗留客户端,在短暂的弃用窗口里保留旧字段,但立即停止创建新的临时格式。

让错误更难被支持搞乱的常见错误

构建带有清晰错误的 API
设计统一的错误响应并在所有端点复用
试用 AppMaster

大多数支持痛点并非来自“用户不当使用”,而是来自歧义。当 API 错误契约不一致时,每个团队都会造出自己的解读,用户则被无从下手的提示困住。

一个常见陷阱是把 HTTP 状态码当成全部。“400” 或 “500” 几乎不会告诉你用户下一步该做什么。状态码有助于传输和大致分类,但你仍需要一个稳定、应用层面的代码来在不同版本间保持含义。

另一个错误是随时间改变代码的含义。如果 PAYMENT_FAILED 曾经表示“卡被拒绝”,后来又被用来表示“支付服务不可用”,你的 UI 和文档会默默变得错误。支持收到了诸如“我试了三张卡还是失败”的工单,而真实的问题是服务中断。

返回原始异常文本(或更糟,堆栈跟踪)虽然快速,却很少对用户有帮助,也可能泄露内部信息。把原始诊断放到日志里,而不是响应中。

一些导致噪音的模式包括:

  • 过度使用“万能”代码如 UNKNOWN_ERROR,这让用户无法得到指导;
  • 创建太多没有清晰分类的代码,让仪表盘和流程难以维护;
  • 在同一字段里混合面向用户的文本和开发者诊断,使本地化和 UI 提示变得脆弱。

一个简单规则:每个用户决策对应一个稳定代码。如果用户可以通过修改输入来解决,用具体代码和清晰提示;如果不能(例如提供商故障),保持代码稳定并返回安全消息与一个如“稍后再试”的操作,同时提供用于支持追查的关联 ID。

发布前的快速检查表

在发布前,把错误当作一个产品功能处理。出现错误时,用户应知道下一步该做什么,支持能找到确切事件,客户端在后端变化时不崩溃。

  • 统一形状: 每个端点(包括 auth、webhook、文件上传)返回一致的错误信封。
  • 稳定且有负责人代码: 每个代码有明确的归属(Payments、Auth、Billing)。不要为不同含义重用代码。
  • 安全且可本地化的文本: 面向用户的短文本绝不包含秘密(令牌、完整卡信息、原始 SQL、堆栈跟踪)。
  • 清晰的 UI 下一步: 针对主要失败类型,UI 应给出一个明显的下一步(重试、更新字段、更换支付方式、联系支持)。
  • 支持的可追溯性: 每个错误响应包含 trace_id(或类似字段),支持可以用它在日志/监控中找到完整事件。

对一些真实流程做端到端测试:带无效输入的表单、过期会话、速率限制以及第三方故障。如果你不能用一句话解释失败并在日志中用 trace_id 精确定位,说明还没准备好发布。

示例:让用户能恢复的注册和支付失败

测试真实失败场景
为注册和支付流程构建原型,提供清晰的用户提示与安全诊断
试用 AppMaster

良好的 API 错误契约能让同一失败在网站、移动端和可能发送的自动邮件中都易于理解。它也能为支持提供足够细节帮助,而不让用户到处截图。

注册:用户能修复的验证错误

用户输入 sam@ 并点击注册。API 返回稳定代码和字段级提示,客户端都能高亮相同输入。

{
  "error": {
    "code": "AUTH.EMAIL_INVALID",
    "message": "Enter a valid email address.",
    "i18n_key": "auth.email_invalid",
    "params": { "field": "email" },
    "ui": { "field": "email", "action": "focus" },
    "trace_id": "4f2c1d..."
  }
}

在 web 端,你把提示显示在邮箱框下;在移动端,聚焦邮箱字段并显示小横幅;在邮件中可以写:“我们无法创建你的账户,因为邮箱地址看起来不完整。” 相同代码,相同含义。

支付:安全说明的失败

卡片支付失败时,用户需要引导,但不应暴露处理器内部信息。契约可以把用户可见内容和支持验证内容分开。

{
  "error": {
    "code": "PAYMENT.DECLINED",
    "message": "Your payment was declined. Try another card or contact your bank.",
    "i18n_key": "payment.declined",
    "params": { "retry_after_sec": 0 },
    "ui": { "action": "show_payment_methods" },
    "trace_id": "b9a0e3..."
  }
}

支持可以要求 trace_id,然后验证返回的稳定代码是哪个,拒付是不可恢复还是可重试、该尝试属于哪个账户和金额,以及是否发送了 UI 提示。

这就是 API 错误契约的价值所在:当后端提供者或内部失败细节变化时,你的 web、iOS/Android 和邮件流程仍保持一致。

随时间测试和监控你的错误契约

让错误引导用户
将后端失败映射为:修正输入、稍后重试或联系支持
立即试用

API 错误契约不是“发布就完事”。当相同错误代码在重构和新功能后仍然导致相同用户行为时,才算稳定。

先从外部像真实客户端那样做测试。为每个你支持的错误代码编写至少一次触发该错误的请求,并断言你依赖的行为:HTTP 状态、code、本地化 key 以及 UI 提示字段(比如哪个表单字段要高亮)。

一个小型测试集能覆盖大部分风险:

  • 针对每个错误场景至少有一个旁通的成功路径测试(防止过度验证);
  • 每个稳定代码至少有一个测试以检查返回的 UI 提示或字段映射;
  • 一个测试确保未知失败返回安全的通用代码;
  • 一个测试确保每种支持的语言都有本地化键;
  • 一个测试确保敏感细节不会出现在客户端响应中。

监控可以捕捉测试遗漏的回归。按时间跟踪错误代码的计数并对突增报警(例如某支付代码在发布后翻倍)。也要监测生产中新出现的代码:如果出现未记录的代码,说明有人绕过了契约。

早期决定哪些信息留给内部、哪些发给客户端。一个实用划分是:客户端获得稳定代码、本地化键和用户操作提示;日志获得原始异常、堆栈跟踪、请求 ID 和依赖方失败信息(数据库、支付提供商、邮件网关)。

每月审查一次错误,结合真实支持对话。挑出按量级排序的前五个代码,阅读若干相关的工单或聊天记录。如果用户不断提出相同的后续问题,说明 UI 提示缺了步骤或文案太含糊。

下一步:在你的产品和流程中应用该模式

从最昂贵的混乱处入手:那些掉队率最高的环节(通常是注册、结账或文件上传)和导致最多工单的错误。先标准化这些,你能在一个迭代内看到效果。

保持推广的务实方式:

  • 选出导致支持量最高的前 10 个错误,分配稳定代码和安全默认文案;
  • 为每个界面(web、mobile、admin)定义代码 -> UI 提示 -> 下一步的映射;
  • 把契约作为新端点的默认要求,缺失字段视为审查不通过;
  • 保持一份小型内部手册:每个代码的含义、支持应要求的信息以及谁负责修复;
  • 跟踪若干指标:按代码的错误率、“未知错误”计数以及与每个代码相关的工单量。

如果你在使用 AppMaster (appmaster.io),建议尽早把这件事内置:为端点定义一致的错误形状,然后在 web 和移动界面中把稳定代码映射到 UI 文案,这样用户在任意端都会得到相同的含义。

一个简单示例:如果支持不断收到“支付失败”的抱怨,标准化能让 UI 对某个代码显示“卡被拒”并提示换卡,而对另一个代码显示“支付系统暂不可用”并提供重试按钮。支持可以要求 trace_id,而不是盲目猜测。

把常规清理列入日程:淘汰未使用的代码、改进含糊的提示,并在有真实流量的语言中补充本地化文本。契约保持稳定,而产品继续演进。

容易上手
创造一些 惊人的东西

使用免费计划试用 AppMaster。
准备就绪后,您可以选择合适的订阅。

开始吧
用于清晰、用户友好的消息的 API 错误契约模式 | AppMaster