接口返回的状态码,到底有什么区别?
这篇文章主要介绍 HTTP 接口中常见状态码的含义和区别,包括 400、401、403、404、409、500 等,并结合实际开发场景说明什么时候应该使用不同的状态码。同时也聊到了 HTTP 状态码和业务错误码之间的关系,帮助理解如何设计更加清晰、规范的接口返回。
平时开发后端接口的时候,状态码基本是绕不开的。尤其是做前后端分离项目,前端调用接口以后,除了看接口返回的数据,还会根据 HTTP 状态码来判断这次请求到底是成功了,还是哪里出了问题。刚开始接触的时候,400、401、403、404、500 这些数字看起来都差不多,很容易混在一起,甚至有些项目里不管发生什么问题都直接返回 500,最后前端只能通过 message 去判断具体是什么错误。项目小的时候可能感觉没什么,但项目一大,这种方式就会越来越乱。
其实 HTTP 状态码本身并不复杂。它主要分成几个范围,其中我们平时最常遇到的是 2xx、4xx 和 5xx。2xx 一般表示请求成功,4xx 表示客户端发过来的请求有问题,5xx 则更偏向服务器内部的问题。最常见的 200 OK 就代表请求正常完成了,比如前端调用 /api/user/1 查询用户,服务器成功查到了数据,就可以直接返回 200。而 201 Created 一般用在创建资源成功的场景,比如注册用户、创建文章、创建订单等,本质上也是成功,只不过它比单纯的 200 更明确地表达了“创建了一个新的资源”。
真正容易混淆的是 4xx。比如 400 Bad Request,通常表示客户端传过来的请求本身就有问题。假设接口要求提交用户名和密码,但是前端连用户名都没有传,或者传过来的 JSON 格式就是错误的,这种情况就是典型的 400。这里的问题并不在服务器,而在请求本身。所以可以简单理解成,400 更像是在说:“你的请求我没办法正常处理,因为你传过来的东西就不符合要求。”
401 Unauthorized 又和 400 不一样。它更常见于登录认证的场景,比如一个接口必须登录之后才能访问,但是请求里面根本没有 Token,或者 Token 已经过期了,服务器无法确认当前用户的身份,这时候通常会返回 401。也就是说,401 关注的是“你是谁”,而不是“你传的参数对不对”。和它特别容易混淆的还有 403 Forbidden,区别其实也很好理解:401 是你没有通过身份认证,403 则是服务器已经知道你是谁了,但是你没有权限做这件事情。比如普通用户已经正常登录了,但是他访问了管理员才能调用的删除接口,这时候就不应该返回 401,而应该返回 403。
404 Not Found 也比较常见,它表达的是请求的资源不存在。最直接的情况就是访问了一个根本不存在的接口,例如请求 /api/abc,而服务器上根本没有这个路由。除此之外,如果接口本身是存在的,只是对应的数据不存在,也可以考虑使用 404。比如请求 /api/user/100,这个用户 ID 在数据库里根本不存在,从资源的角度来说同样属于“找不到”。所以 404 不只是“接口地址不存在”,也可以表示“你要找的资源不存在”。
还有一个状态码在实际项目里也比较实用,就是 409 Conflict。它通常用来表示当前请求和服务器已有的数据产生了冲突。比如注册用户时提交了一个已经存在的用户名,参数格式本身没有问题,服务器也没有故障,但是因为数据库里已经有这条数据了,所以不能继续创建。这个时候用 409 会比直接返回 500 更准确,因为这显然不属于服务器内部异常,而是当前数据状态导致的冲突。有些项目还会使用 422 来表示请求格式是正确的,但是具体的数据无法通过业务校验,比如年龄传的是 -10,类型没问题,JSON 也没问题,但从业务规则来看就是不合法。400 和 422 在不同项目里的使用方式会有一些区别,所以这个地方最好提前统一团队规范,不要每个人按照自己的理解来写。
相比之下,5xx 就更偏向服务器自己的问题了。最典型的就是 500 Internal Server Error。比如代码出现了未处理的异常、数据库突然连不上、程序发生 panic,或者内部逻辑出现了意料之外的问题,这时候才更适合返回 500。所以开发的时候最好不要养成“有错误就返回 500”的习惯。用户不存在不是服务器故障,参数错误也不是服务器故障,权限不足同样不是服务器故障。如果什么问题都返回 500,前端虽然知道“请求失败了”,但是却不知道这到底是用户输入的问题,还是服务器真的挂了。
在微服务或者有网关的项目里,还经常会看到 502、503 和 504。比如 Nginx 或其他网关去调用后面的 Go 服务,如果上游服务没有正常返回结果,可能会出现 502 Bad Gateway。如果服务当前不可用,比如正在维护或者过载,可以使用 503 Service Unavailable。而 504 Gateway Timeout 一般表示网关等待上游服务太久,没有在规定时间内拿到响应。这几个状态码在普通单体项目里可能不常见,但到了微服务、网关、负载均衡这些场景,基本都会遇到。
另外一个经常值得讨论的问题,就是 HTTP 状态码和业务错误码到底是不是一回事。实际上它们最好不要混在一起。比如用户提交了一个错误参数,HTTP 层面可以返回 400,然后响应体里面再带一个业务错误码,比如:
{ "code": 10001, "message": "用户名不能为空" }
这里的 400 表示这是一个错误请求,而 10001 则表示具体是哪一种业务错误。这样做的好处是职责比较清楚,HTTP 状态码处理 HTTP 层面的事情,业务错误码负责具体业务。比如前端看到 401,可以直接判断登录状态失效,需要跳转登录页;看到 403,可以提示当前用户没有权限;如果 HTTP 状态码是 200,那再根据业务层面的 code 去判断具体是不是业务处理成功。
以前有一些项目喜欢所有接口都返回 200,然后在 JSON 里面自己定义一套错误码,例如明明用户没有登录,HTTP 返回的还是 200,只是响应里面写一个 code: 401。这种方式并不是完全不能用,实际上很多项目也确实这么设计,但它的问题在于 HTTP 状态码失去了原本的意义。监控系统、网关、浏览器、各种 HTTP 客户端看到的永远都是“请求成功”,真正的错误只能继续解析响应体才能知道。如果项目比较简单,也许问题不大,但随着系统越来越复杂,这种设计往往会让排查问题变得麻烦。
所以我自己理解状态码的时候,会把它当成一种“对请求结果的描述”。如果请求成功,就返回对应的 2xx;如果是客户端请求的问题,就考虑 4xx;如果真的是服务器内部出现异常,再使用 5xx。其中最常用的几个其实不用死记,400 是请求有问题,401 是没有通过认证,403 是没有权限,404 是资源不存在,409 是数据冲突,500 是服务器内部异常。只要把这几个概念真正弄明白,平时写接口基本就够用了。