RFC 7230 定义的 HTTP 状态码在描述问题时存在语义模糊的问题。电子支付用户在产生消费行为时可能会收到 403 Forbidden,一些通用的,基于 HTTP 的中间件,例如库、缓存、代理可能不会有什么意见,然而客户端却可能既不能理解问题到底是来自「余额不足」还是用户「没有特定商品的购买资格」,也无法(用足够的信息引导用户)解决该问题。因此在基于 JSON 的 HTTP API 设计中,我们通常会在响应体中塞一个错误模型。
Spring MVC 在处理异常时(比如参数校验不通过,抛出 MethodArgumentNotValidException)默认返回如下结构:
有些项目将 HTTP API 视为一种传输数据的基础设施,这种思路下业务代码会主动捕获异常,包装成 APIResult 并使用 200 Success 返回。

我第一次看到这张图是在 How do you know what’s gone wrong when your API request fails?
其他组织是怎么做的
GitHub API
GitHub 提供了一系列 REST API 供开发者调用。
以 Search code API 为例,故意遗漏 URL 查询参数 q。响应简要描述了错误详情(使用了数组,必要时可以描述多个错误)以及为了协助调用者排除问题指出的文档位置。
Directus
Directus 是一款主流的 Headless CMS,其与内容展示端的交互主要靠基于 JSON 的 HTTP API 和 GraphQL。
在请求某个 Collection 的内容时故意构造错误的查询条件块,响应体通过 errors 数组描述了错误。
RFC 7807 Problem Details
IETF 提议的 RFC 7807 标准定义了一种可读性较好的 Problem Detail 类型,即为通用组件提供了抽象层度较高的 HTTP 状态码,又为具体错误处理机制提供了细粒度的描述。
Problem Detail 一般由以下部分组成:
type: URI:指向对应解决方案的文档的 URI,是确定问题的类型的主要依据title: string:简短易读的错误描述,起到对type补充说明的作用status: number:错误状态码,在服务调用发生嵌套时尤其有用,方便起见可以套用 HTTP 状态码detail: string:对错误产生的原因做简要阐述,但是不要把 debug 信息放进去,也不要试图把结构化信息 stringify 后存储在这里instance: URI:发生错误的 endpoint(相对路径)
开发者可以自行扩展属性以携带其他信息,例如 errors[],trace/span 等。
标准还建议 API 返回特定的 Content-Type 例如 application/problem+json 或 application/problem+xml。
o.s.h.ProblemDetail
Spring 通过 ResponseEntityExceptionHandler 可以捕获几种常见的异常(例如 MethodArgumentNotValidException 什么的)。在 Spring Boot 3 中,继承 ResponseEntityExceptionHandler 并使用 RestControllerAdvice 注解修饰,Spring Boot 就会为这几种异常返回 org.springframework.http.ProblemDetail 类型。
应用 RFC 8707 的挑战
RFC 8707 的一些设计在实际项目实施中有一些难度:
type字段一般是指向解决方案的 HTML 文档的 URI,这些具体的文档在大部分项目中应该是缺失的(或对其表达形式缺乏较好的阐明)- 草案要求客户端根据
type字段区分问题类型和识别扩展字段,添加新的扩展字段就意味着增加type的类型 - 多个原因导致用户请求失败,最好一起反馈给请求方
这些问题还有待实践摸索,例如对第一个问题,有的项目就设置了指向对应 HTTP 状态码的 RFC 文档。对第三个问题,本文建议增加 explanations 数组供参考,至于 explanation 的结构则暂无较好的想法,还是暂时依靠人工介入好了。
在 Spring Boot 3 中实现:
需要注意的是,为了方便配合日志查询和解决问题,常常需要为错误返回 trace 信息,这方面的工作可以交给一些日志链路追踪框架。