API 报错别只返回一句话:设计稳定、可诊断的错误响应

客户端、API 网关与服务共同处理错误响应

接口成功时,调用方关心数据;接口失败时,它还要决定提示用户、修正参数、重新登录还是稍后重试。如果服务有时返回字符串、有时返回 HTML、有时把所有失败都塞进 200,每个消费者只能靠猜。错误响应同样是公开契约,应当稳定、可解析,也能帮助值班人员快速定位请求。

把失败拆成不同层次

一个可用的错误模型至少回答四个问题:请求在 HTTP 层为何失败,程序应走哪个分支,人应该看到什么,以及运维人员去哪里查证据。

信息 面向对象 设计原则
HTTP 状态码 网关、SDK、通用客户端 表达失败的大类
error_code 调用方程序 稳定、可枚举、不随文案改变
detail 开发者或最终用户 可读,但不能作为分支条件
trace_id 开发与运维 能关联日志、指标和调用链

不要用 200 包装失败。认证缺失、资源不存在、状态冲突和服务异常应保留各自的 HTTP 语义;业务错误码再补充精确原因。例如,同为冲突,ORDER_ALREADY_PAIDSTOCK_RESERVATION_EXPIRED 显然需要不同处理。

采用统一的 Problem Details 外壳

统一错误外壳连接客户端判断与服务端诊断

可以用 application/problem+json 作为统一格式,并在通用字段外增加业务扩展:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
"type": "https://api.example.com/problems/order-state-conflict",
"title": "Order state conflict",
"status": 409,
"detail": "The order has already been paid.",
"instance": "/orders/o_123/confirm",
"error_code": "ORDER_ALREADY_PAID",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}

type 标识问题类别,可指向长期稳定的说明页;title 是该类别的简短名称;detail 描述这一次失败;instance 标识具体请求或资源。客户端应主要依据 HTTP 状态和 error_code 决策,不要匹配 detail,因为文案会改动、翻译,也可能因安全策略被收敛。

让参数错误能精确落到字段

表单校验失败若只返回“参数错误”,用户无法修正输入。可以增加 errors 数组,用 JSON Pointer 指向字段,并为每项提供机器码:

1
2
3
4
5
6
7
8
9
10
11
12
{
"status": 422,
"error_code": "VALIDATION_FAILED",
"errors": [
{
"pointer": "/shipping/address/postcode",
"code": "INVALID_FORMAT",
"message": "Postcode format is invalid."
}
],
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}

字段路径必须对应请求体,而不是后端 DTO 或数据库列名。数组顺序不应承载业务含义;同一字段存在多条规则时,也要允许返回多项。前端可用 pointer 定位控件,用 code 选择本地化文案,同时保留顶层错误供非表单客户端使用。

状态码与重试策略要一致

错误码越多,不代表契约越清晰。先用 HTTP 状态表达通用语义,再为调用方确实需要区分的情况增加业务码。

场景 常用状态 调用方动作
请求格式或字段不合法 400 / 422 修正请求,不自动重试
未登录或凭据失效 401 刷新凭据或重新登录
当前状态发生冲突 409 重新读取状态后决策
请求过于频繁 429 遵循 Retry-After 退避
服务内部异常 500 幂等且预算允许时重试,并记录 trace_id

不要把所有 5xx 都标成可重试:写请求可能已经生效,只是响应丢失。是否重试还取决于操作幂等性、超时阶段与重试预算。若服务明确要求等待,应通过响应头给出信号,而不是把秒数藏在可变文案里。

保留诊断能力,但避免泄密

生产响应不能暴露堆栈、SQL、文件路径、内部主机名或访问令牌。服务端应把完整异常与 trace_id 一起写入受控日志,对外只返回足够行动的描述。trace_id 也不要包含用户 ID、时间戳明文等业务信息,使用不可预测、无语义的标识更安全。

错误日志至少应记录路由、状态码、业务错误码、耗时和追踪标识;敏感字段先脱敏。对于预期的校验失败,不必按系统故障报警;对未知异常则统一映射为通用 500,同时保留原始异常供内部排查。

用契约测试守住演进边界

错误结构发布后就可能被网页、移动端和第三方集成依赖。删除字段、改变类型、复用旧错误码表达新含义,都是破坏性变更。新增可选字段通常更安全,但客户端仍应忽略不认识的扩展字段。

测试应覆盖成功路径之外的关键失败分支:验证状态码、媒体类型、必需字段和错误码集合,并确认响应不会泄露内部异常。最后准备一份错误码目录,写清触发条件、客户端动作和是否可重试。好的错误契约不是把报错包装得更漂亮,而是让程序能稳定决策,让用户知道下一步,也让工程师凭一个追踪标识找到真实原因。