Appearance
错误处理规范
本章导读:错误响应是 API 使用频率最高的"文档"——调试时客户端第一个读到的是它。IETF 已经把标准答案写成了 RFC 9457(Problem Details for HTTP APIs,取代 RFC 7807)。本章给出规范字段的完整语义、错误码体系设计、以及各状态码下的错误实践细节。
1. 为什么"永远 200 + code"是错的
json
// 反面教材
HTTP/1.1 200 OK
{ "code": 404, "msg": "文章不存在" }- SDK 的异常映射、网关的重试/熔断、监控的成功率统计全部失灵——HTTP 层看一切正常;
- 缓存层会把错误响应当 200 缓存(除非逐条配 no-store);
- 浏览器/代理无法区分 401 触发登录流程。
原则:状态码表达"错误的大类"(协议语义),响应体表达"错误的细节"(业务语义)。
2. RFC 9457:Problem Details
错误响应的标准媒体类型:application/problem+json(也有 XML 变体 application/problem+xml)。五个注册字段:
json
// HTTP/1.1 422 Unprocessable Content
// Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-failed",
"title": "请求校验失败",
"status": 422,
"detail": "字段 title 超过最大长度 200(实际 233)",
"instance": "/v1/articles",
// 允许扩展成员(RFC 9457 §3.2)
"code": "VALIDATION_FAILED",
"errors": [
{ "field": "title", "constraint": "maxLength", "expected": 200, "actual": 233 }
],
"traceId": "0af7651916cd43dd8448eb211c80319c"
}| 字段 | 语义 | 规范要求 |
|---|---|---|
type | 机器可读错误类型的 URI,指向人类可读文档 | 默认 about:blank;用语义化路径作错误码目录(见 §3) |
title | 类型的简短人读摘要 | 按语言可本地化(内容协商 Accept-Language);同一 type 的 title 应稳定 |
status | 绑定的 HTTP 状态码 | 必须与真实状态码一致(RFC 9457 校验点) |
detail | 本次错误实例的人读解释 | 可本地化;包含具体上下文(哪个字段、什么值) |
instance | 本次错误发生的具体上下文标识(URI 或 ID) | 可指向日志系统里该请求的记录(配合 traceId) |
NOTE
RFC 9457 相比旧 7807 的两点重要更新:① 明确 type 可以不去解引用(它就是标识符,不是必须能打开的网页——但提供文档页是良好实践);② 对 Content-Type 后缀协商与扩展成员的兼容性做了更严格的规定。
3. 错误码体系:用 type URI 建目录
把 type 的最后一段当作全平台唯一的机器可读错误码,错误码即文档 URL:
https://api.example.com/problems/article-title-too-long
https://api.example.com/problems/payment-balance-insufficient
https://api.example.com/problems/invalid-cursor治理规则:
- 一码一义:禁止
PARAM_INVALID走天下;粒度到"客户端能据此分支处理"为止; - 命名:小写 kebab,资源 + 问题(
article-title-too-long),新增走评审(它是公开契约); /problems/{code}页面自动从错误目录生成(文档即实现);- 兼容旧客户端需要数字业务码时,放扩展字段
code,不要塞进type。
4. 状态码 × 错误场景对号入座
| 场景 | 状态码 | 响应体要点 |
|---|---|---|
| JSON 语法错误、参数类型错 | 400 Bad Request | detail 指明具体字段 |
| 未认证 / Token 过期 | 401 Unauthorized | 必须带 WWW-Authenticate: Bearer error="invalid_token", …(RFC 6750) |
| 已认证无权限 | 403 Forbidden | 可不解释细节(防探测) |
| 资源不存在(或按安全策略对无权限者伪装不存在) | 404 Not Found | 无 body 也可 |
方法不支持(对 /articles/42 发 POST) | 405 Method Not Allowed | 必须带 Allow: GET, PUT, DELETE(RFC 9110) |
| Accept 无法满足 | 406 Not Acceptable | 列出可用表示 |
| 请求体超过大小上限 | 413 Content Too Large | 可带 Retry-After;上传场景给上限值 |
| 媒体类型不支持 | 415 Unsupported Media Type | detail 给出期望的 Content-Type |
缺少必需的条件头(强制 If-Match 的接口) | 428 Precondition Required(RFC 6585) | — |
| 条件头不满足(ETag 版本过旧) | 412 Precondition Failed | 给当前版本信息,指导重试 |
| 业务规则冲突(重复昵称、状态机非法迁移) | 409 Conflict | type 区分具体规则 |
| 语义校验失败(格式对但值非法) | 422 Unprocessable Content(源自 WebDAV RFC 4918,RFC 9110 正式收录并更名) | errors[] 逐字段 |
| 请求实体不完整(如分片顺序乱) | 422 或 400,团队统一约定 | — |
| 触发限流 | 429 Too Many Requests(RFC 6585) | 必须带 Retry-After;建议 RateLimit 头(见限流章) |
| 服务内部异常 | 500 Internal Server Error | 绝不回显堆栈/SQL;detail 用通用文案 + traceId |
| 上游依赖失败 | 502 Bad Gateway | 同上 |
| 过载/维护 | 503 Service Unavailable | 应带 Retry-After(RFC 9110) |
| 网关等待上游超时 | 504 Gateway Timeout | — |
| 资源曾存在已永久删除 | 410 Gone | 与 404 区分,帮客户端清理缓存 |
TIP
高频辨析:400 vs 422——能不能正确解析+字段级语法错误 → 400;语法全对但业务/语义校验失败 → 422。401 vs 403——"你是谁"→401;"你不能"→403。404 vs 405——URL 对方法错是 405,不是 404。
5. 校验错误的批量返回
一次提交多个字段错误时,全部返回而不是"发现第一个就报":
json
// HTTP/1.1 422 Unprocessable Content
{
"type": "https://api.example.com/problems/validation-failed",
"title": "请求校验失败",
"status": 422,
"detail": "2 个字段不符合约束",
"instance": "/v1/articles",
"errors": [
{ "field": "title", "constraint": "required" },
{ "field": "publishedAt", "constraint": "mustNotBeBefore", "argument": "createdAt" }
]
}field用请求体中的 JSON Pointer(RFC 6901)定位深层结构:"field": "/author/address/city";- 表单/查询参数错误同理给
parameter+location(query/path/header)——参考 OpenAPI 的Parameter描述维度。
6. 安全红线
- 不回显内部信息:堆栈、SQL、文件路径、服务器版本(
Server头最小化);5xx 的 detail 面向用户,内部诊断靠traceId关联日志。 - 认证前不泄露资源存在性:未登录访问他人资源返回 404 而不是 403(防枚举)——但要全 API 一致,否则反而成了信号。
- 错误响应同样禁缓存或短缓存:
Cache-Control: no-store(401/403/429 等含敏感语义)。 instance/detail不得包含用户输入原样回显(XSS/日志注入),输出前转义截断。
7. 框架落地示例(Spring Boot)
Spring 的 ProblemDetail(Spring Framework 6 / Spring Boot 3+)原生支持 RFC 9457:
java
return ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY,
"字段 title 超过最大长度 200(实际 " + title.length() + ")")
.setTitle("请求校验失败")
.setType(URI.create("https://api.example.com/problems/validation-failed"))
.setInstance(URI.create("/v1/articles"))
.setProperty("code", "VALIDATION_FAILED")
.setProperty("errors", List.of(Map.of(
"field", "title", "constraint", "maxLength", "expected", 200)));全局异常处理器统一转 Problem,保证任何 5xx 也不会漏出默认错误页格式。
8. 本章小结
- 错误响应标准化三件套:正确的状态码 +
application/problem+json(RFC 9457)+ 平台级 type 错误码目录。 - 扩展字段(
code/errors[]/traceId)合法且必要——协议字段给机器与中间件,扩展字段给业务客户端与排障。 - 状态码义务别忘了头部搭档:401→
WWW-Authenticate、405→Allow、429/503→Retry-After。
9. 下一步
设计规范齐了,进入第 04 篇把状态码全表过一遍 → HTTP 状态码详解