RFC 7230 定义的 HTTP 状态码在描述问题时存在语义模糊的问题。电子支付用户在产生消费行为时可能会收到 403 Forbidden
,一些通用的,基于 HTTP 的中间件,例如库、缓存、代理可能不会有什么意见,然而客户端却可能既不能理解问题到底是来自「余额不足」还是用户「没有特定商品的购买资格」,也无法(用足够的信息引导用户)解决该问题。因此在基于 JSON 的 HTTP API 设计中,我们通常会在响应体中塞一个错误模型。
Spring MVC 在处理异常时(比如参数校验不通过,抛出 MethodArgumentNotValidException
)默认返回如下结构:
HTTP/1.1 400
Content-Type: application/json
{
"timestamp": "2022-12-16T06:14:44.549+00:00",
"status": 400,
"error": "Bad Request",
"path": "/register"
}
有些项目将 HTTP API 视为一种传输数据的基础设施,这种思路下业务代码会主动捕获异常,包装成 APIResult
并使用 200 Success
返回。
public class Result<T> implements Serializable {
// 自定义的业务状态代码
private ResultCode code;
private List<T> data;
// 有的时候是错误描述
private String message;
}
我第一次看到这张图是在 How do you know what’s gone wrong when your API request fails?
其他组织是怎么做的
GitHub API
GitHub 提供了一系列 REST API 供开发者调用。
以 Search code
API 为例,故意遗漏 URL 查询参数 q
。响应简要描述了错误详情(使用了数组,必要时可以描述多个错误)以及为了协助调用者排除问题指出的文档位置。
HTTP/2 422
content-type: application/json; charset=utf-8
{
"message": "Validation Failed",
"errors": [
{
"resource": "Search",
"field": "q",
"code": "missing"
}
],
"documentation_url": "https://docs.github.com/v3/search"
}
Directus
Directus 是一款主流的 Headless CMS,其与内容展示端的交互主要靠基于 JSON 的 HTTP API 和 GraphQL。
在请求某个 Collection 的内容时故意构造错误的查询条件块,响应体通过 errors
数组描述了错误。
HTTP/1.1 400
Content-Type: application/json; charset=utf-8
{
"errors": [
{
"message": "****** field type does not contain the ****** filter operator",
"extensions": {
"code": "INVALID_QUERY"
}
}
]
}
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
类型。
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
}
HTTP/1.1 400
Content-Type: application/problem+json
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Invalid request content.",
"instance": "/register"
}
应用 RFC 8707 的挑战
RFC 8707 的一些设计在实际项目实施中有一些难度:
type
字段一般是指向解决方案的 HTML 文档的 URI,这些具体的文档在大部分项目中应该是缺失的(或对其表达形式缺乏较好的阐明)- 草案要求客户端根据
type
字段区分问题类型和识别扩展字段,添加新的扩展字段就意味着增加type
的类型 - 多个原因导致用户请求失败,最好一起反馈给请求方
这些问题还有待实践摸索,例如对第一个问题,有的项目就设置了指向对应 HTTP 状态码的 RFC 文档。对第三个问题,本文建议增加 explanations
数组供参考,至于 explanation
的结构则暂无较好的想法,还是暂时依靠人工介入好了。
HTTP/1.1 400
Content-Type: application/problem+json
{
"type": "about:blank",
"title": "Validation Error",
"status": 400,
"detail": "Method Argument Not Valid",
"instance": "/user/register",
"explanations": [
{
"field": "email",
"message": "must be a well-formed email address"
},
{
"field": "phone",
"message": "phone number must be 11 digits"
},
{
"field": "address.zipcode",
"message": "zip code must be 6 digits"
}
]
}
在 Spring Boot 3 中实现:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidationExceptions(MethodArgumentNotValidException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Method Argument Not Valid");
problemDetail.setTitle("Validation Error");
var explanations = ex.getBindingResult().getAllErrors().stream()
.map((error) -> {
var explanation = new HashMap<String, String>();
explanation.put("field", ((FieldError) error).getField());
explanation.put("message", error.getDefaultMessage());
return explanation;
})
.toList();
problemDetail.setProperty("explanations", explanations);
return problemDetail;
}
}
需要注意的是,为了方便配合日志查询和解决问题,常常需要为错误返回 trace 信息,这方面的工作可以交给一些日志链路追踪框架。