在RESTful API中如何选择合适的HTTP状态码

引言

在最近的RESTful API设计过程中,我遇到了一个问题。当业务逻辑需要返回一个错误代码时,究竟应该遵循HTTP的原则返回对应的状态码,如200, 403, 500等,还是统一返回200,而将详细的错误信息写入响应体内?

这个问题涉及到了RESTful API是否应该遵循HTTP状态码的原则,让我们一起来探讨一下。

搜寻

先来看看其他公司的解决方案

compare
Reference to Google Cloud

目前市面上的解决方案主要可以归类为两种:

  1. 解决方案1:只使用200状态码,把所有的错误信息写在响应体里。
  2. 解决方案2:根据错误的类型,归类到不同的状态码并返回。

这两种方案都有一定的道理。

比如在客户端调用时,大多数HTTP客户端都是按照HTTP的协议来设计的,例如axios。

1
2
3
4
5
try {
let result = await axios.get("/api");
} catch (e){
// do error hanlding
}

使用状态码的方式返回可以让客户端快速判断出请求的状态 [6], 从而做出相应的反应。

当然,反对者可能会认为这种方式过于繁琐,如果直接使用200作为所有请求的返回值,那么就不需要考虑其他请求状态了,直接依据body里的code作为判断依据岂不是更为简单?

然而,单纯使用200状态码作为返回值也并不理想,因为客户端通常依赖返回码来判断请求的状态。只返回200可能会让客户端错误地认为请求是正常的,而实际状态却可能是错误的。

此外,虽然HTTP通常被视为一个Application Layer(应用层协议),但它在很多情况下更像是一个Transport Layer(传输层协议)。

比如API服务通过HTTP来实现,Web服务也通过API来实现。所以,当你尝试将HTTP的状态码和业务逻辑的错误代码组合在一起时,你必须先预定一个规则来确保错误代码与HTTP状态码的规则相符。例如,某个业务的错误代码对应某个HTTP状态码。

尽管这样在短期内可能显得整洁,但随着项目的发展,你要维护的规则可能会越来越多,有时候你甚至可能会违反自己的规则。因此,通过将错误类型归集到不同的状态码来返回的方法同样存在无法回避的问题。

我的方案

因此,在我看来,较好的方案应该是要分离HTTP逻辑与业务逻辑。应让各自负责自己的部分,即由HTTP处理HTTP的错误,而将业务逻辑的错误在HTTP中视为无错误。业务错误由客户端自己在body中获取。

总体而言,我的解决方案与第二个解决方案相似,但有所不同。

因为我的微服务框架包括三个层次:delivery(传递层)、service(服务层)和repository(仓储层)。传递层负责处理传入的HTTP请求,所有业务逻辑都在服务层内部处理。

由此,我直觉地将所有错误分为两个场景:第一个场景是在传递层发生的错误,这些错误直接与HTTP有关,例如参数验证、路径参数、URL、速率限制等。第二个场景是服务层发生的错误,包括仓储层错误和其他错误。

在将错误分为这两层之后,解决方案自然而然地浮现出来。如果错误发生在传递层,我将使用HTTP状态码进行响应。否则,我将使用200状态码,并在响应体内附带错误信息。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (g *ginApp) handleCreatePaste(c *gin.Context) {
var param pasteReq = pasteReq{Expires: 3600}
if err := c.ShouldBindJSON(&param); err != nil {
c.JSON(http.StatusBadRequest, model.NewErrorCodeResponse(
errcode.ParameterError.WithDetail(err)))
return
}
if param.Title == "" {
param.Title = fmt.Sprintf("%s %s", time.Now().Format("2006-01-02 15:04:05"), c.ClientIP())
}
paste, err := g.pasteSrv.CreatePaste(param.ContentType, param.Title, param.Content, param.Password, c.ClientIP(), param.Expires)
if err != nil {
c.JSON(http.StatusOK, model.TryErrorCodeResponse(err))
return
}
c.JSON(http.StatusOK, model.NewDataResponse(paste))
}

我的这种方案的优势在于,如果客户端的请求方式正确,那么它只会收到200的响应,至于具体结果如何,则需要自己判断。如果请求方式错误,那么它会得到相应的HTTP状态码。

这样做实际上是对错误进行了分类和分别处理。只要客户端能保证请求方式正确,就不需要考虑状态码的问题。

当然,我的这种解决方案也不能完全避免上述的一些问题,更好的方案仍在开发中等待实现。

Reference

  1. Web API Design: The Missing Link
  2. https://cloud.google.com/blog/products/api-management/restful-api-design-what-about-errors
  3. https://softwareengineering.stackexchange.com/questions/305250/should-i-use-http-status-codes-to-describe-application-level-events
  4. https://stackoverflow.com/questions/56736771/http-response-always-return-response-code-200-even-request-fail-and-return-stat
  5. http://cn.voidcc.com/question/p-xwqrmqht-bm.html
  6. https://baijiahao.baidu.com/s?id=1693811021976970402&wfr=spider&for=pc