接口响应选择:HTTP状态码 vs JSON状态码

在后端开发中,存在两种常见的模式来处理接口响应:直接使用HTTP状态码和JSON状态码(将状态码添加到JSON响应中)。这两种模式各有利弊,到底应该如何选择呢?网上众说纷纭,互喷互怼,状况之惨烈,我都不敢下场参战。本文来具体分析下,答案需要靠你寻找。

两种模式
首先先说下什么是HTTP状态码。

HTTP状态码是Web开发中的标准,我们常用的大约有10个左右,2XX代表成功,301、302代表重定向,304代表协商缓存,4XX代表客户端错误,401代表未登陆,403代表没有权限,404代表资源不存在,5XX代表服务端错误,502代表网关错误。

但其实每个状态码有更详细的规范,以下内容扒自这篇文章Hypertext Transfer Protocol (HTTP) Status Code Registry[1],更新日期是2022-06-08,可能存在一定滞后,大家参考下:
HTTP状态码有很多,每个都有对应的含义,下面列出日常工作中常见的部分:

2XX (请求成功)表示成功处理了请求的状态代码。

200 (成功) 服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。
201 (已创建) 请求成功并且服务器创建了新的资源。
202 (已接受) 服务器已接受请求,但尚未处理。
203 (非授权信息) 服务器已成功处理了请求,但返回的信息可能来自另一来源。
204 (无内容) 服务器成功处理了请求,但没有返回任何内容。
205 (重置内容) 服务器成功处理了请求,但没有返回任何内容。
206 (部分内容) 服务器成功处理了部分 GET 请求。

3XX (请求被重定向)表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向。

300 (多种选择) 针对请求,服务器可执行多种操作。 服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择。
301 (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
302 (临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
303 (查看其他位置) 请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码。
304 (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
305 (使用代理) 请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理。
307 (临时重定向) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。

4XX (请求错误)这些状态代码表示请求可能出错,妨碍了服务器的处理。

400 (错误请求) 服务器不理解请求的语法。
401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
403 (禁止) 服务器拒绝请求。
404 (未找到) 服务器找不到请求的网页。
405 (方法禁用) 禁用请求中指定的方法。
406 (不接受) 无法使用请求的内容特性响应请求的网页。
407 (需要代理授权) 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理。
408 (请求超时) 服务器等候请求时发生超时。
409 (冲突) 服务器在完成请求时发生冲突。 服务器必须在响应中包含有关冲突的信息。
410 (已删除) 如果请求的资源已永久删除,服务器就会返回此响应。
411 (需要有效长度) 服务器不接受不含有效内容长度标头字段的请求。
412 (未满足前提条件) 服务器未满足请求者在请求中设置的其中一个前提条件。
413 (请求实体过大) 服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。
414 (请求的 URI 过长) 请求的 URI(通常为网址)过长,服务器无法处理。
415 (不支持的媒体类型) 请求的格式不受请求页面的支持。
416 (请求范围不符合要求) 如果页面无法提供请求的范围,则服务器会返回此状态代码。
417 (未满足期望值) 服务器未满足"期望"请求标头字段的要求。

5XX(服务器错误)这些状态代码表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误,而不是请求出错。

500 (服务器内部错误) 服务器遇到错误,无法完成请求。
501 (尚未实施) 服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码。
502 (错误网关) 服务器作为网关或代理,从上游服务器收到无效响应。
503 (服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。
504 (网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求。
505 (HTTP 版本不受支持) 服务器不支持请求中所用的 HTTP 协议版本。

Value Description
100 Continue
101 Switching Protocols
102 Processing
103 Early Hints
104-199 Unassigned
200 OK
201 Created
202 Accepted
203 Non-Authoritative Information
204 No Content
205 Reset Content
206 Partial Content
207 Multi-Status
208 Already Reported
209-225 Unassigned
226 IM Used
227-299 Unassigned
300 Multiple Choices
301 Moved Permanently
302 Found
303 See Other
304 Not Modified
305 Use Proxy
306 (Unused)
307 Temporary Redirect
308 Permanent Redirect
309-399 Unassigned
400 Bad Request
401 Unauthorized
402 Payment Required
403 Forbidden
404 Not Found
405 Method Not Allowed
406 Not Acceptable
407 Proxy Authentication Required
408 Request Timeout
409 Conflict
410 Gone
411 Length Required
412 Precondition Failed
413 Content Too Large
414 URI Too Long
415 Unsupported Media Type
416 Range Not Satisfiable
417 Expectation Failed
418 (Unused)
419-420 Unassigned
421 Misdirected Request
422 Unprocessable Content
423 Locked
424 Failed Dependency
425 Too Early
426 Upgrade Required
427 Unassigned
428 Precondition Required
429 Too Many Requests
430 Unassigned
431 Request Header Fields Too Large
432-450 Unassigned
451 Unavailable For Legal Reasons
452-499 Unassigned
500 Internal Server Error
501 Not Implemented
502 Bad Gateway
503 Service Unavailable
504 Gateway Timeout
505 HTTP Version Not Supported
506 Variant Also Negotiates
507 Insufficient Storage
508 Loop Detected
509 Unassigned
510 Not Extended (OBSOLETED)
511 Network Authentication Required
512-599 Unassigned
HTTP状态码
用Deno代码来举例说明它的用法:

import { Application, Router } from ‘https://nd/x/oak@v12.6.0/mod.ts’;
const app = new Application();
const router = new Router();
router.get(‘/’, (ctx) => {
ctx.response.status = 400;
ctx.response.body = ‘参数非法’;
});

app.use(router.routes());
app.use(router.allowedMethods());
console.log(‘服务器正在运行,访问 http://localhost:8000’);
await app.listen({ port: 8000 });
访问页面http://localhost:8000,在网络里可以看到状态码为400,Content-Type是文本类型:

响应的就是错误信息:

下面说下它的优缺点。

优点
标准化:HTTP状态码是Web开发中的标准,使用它们可以确保与其他开发者、框架和工具之间的一致性。对于前端开发者而言,在浏览器的网络里就能清晰地看到接口的状态。
简单明了:HTTP状态码提供了一种直观的方式来表示请求的结果,不需要解析JSON响应来获取状态信息。
缓存支持:HTTP状态码可以与缓存机制更好地集成,提高性能。
缺点
有限的信息:HTTP状态码本身提供的信息有限,而错误信息为文本格式,不够详细。
需要额外解释:客户端需要对不同的状态码进行解析,并映射到特定的错误情况。
一定的学习成本。
JSON状态码
将状态码添加到JSON响应中,是一种比较常见的方式,我十几年前入行伊始就是这种用法,如果你去看阿里或腾讯的Web服务文档,也大多是这种响应格式:

{
‘code’: 200,
‘message’: ‘请求成功’,
‘data’: {
// 响应数据
}
}
如果是失败:

{
‘code’: 404,
‘message’: ‘未找到资源’,
‘data’: null
}
而接口的HTTP状态码,一般是200,表示成功(比如阿里云的调用内容检测API[2]),这也是这两种模式最大的分歧所在。HTTP状态码一把梭200,相当于将HTTP协议只用作传输协议,业务状态与HTTP状态码无关。

还是以上面的Deno代码举例,修改为这样:

router.get(‘/’, (ctx) => {
ctx.response.status = 200;
ctx.response.body = {
code: 400,
message: ‘参数非法’,
data: null
}
});
在浏览器网络里看到状态码为200,Content-Type为JSON:

响应为:

下面说下这种方案的优缺点:

优点
丰富的信息:通过将状态码添加到JSON响应中,你可以提供更丰富的错误信息,包括错误消息、错误代码、详细描述等,更有助于客户端理解问题所在。状态码只有三位,可能远远无法覆盖具体业务中的错误,所以某些响应是在HTTP状态码的基础上做了部分扩展,甚至可能完全与HTTP状态码无关,而是自定义的错误码(比如10001)。
一致的数据结构:使用相同的数据结构和字段来处理所有接口响应可以简化客户端的代码逻辑,使其更易于处理响应。
易于扩展:JSON响应可以轻松扩展,添加更多的自定义字段,以满足特定的需求。
甩锅方便:在日趋复杂的现代运维环境中,前端接口抵达后端服务器通常不是从A到B那么简单,中间可能有多层链路,一般有网关层或多层Nginx转发。如果前端开发在F12网络里看到有非200的错误请求,就可以直接判断出来问题出在上层架构(锅是IT运维的)而不是业务服务器,能节省问题的排查时间。反之,如果只响应HTTP状态码的情况下,遇到一个404错误,你怎么判断是哪个链路的错误?
缺点
不够标准化:使用这种模式可能导致与其他开发者、框架和工具之间的差异,特别是在处理错误时的字段和格式上。正因为没有统一的规范,所以有的状态码字段并不用code,而是用status,也有的是首字母大写的Code(比如这篇《阿里云短信发送API[3]》,这种反人类的响应一般是用Go开发的);也有的错误信息不用message,而是msg。这种情况下,一般都要提供完善的开发文档。如果前端代码对接不同标准的服务端,那简直是灾难,好在日常工作中这种情况不容易出现。
不利于协商缓存:协商缓存只有显式设置状态码为304才能生效。而如果有重定向的业务,是不可能摆脱301、302状态码的。
增加前端排障难度:如果是使用HTTP状态码,前端开发者在浏览器网络里能一眼看到错误接口,而如果后端响应的清一色200,那排查起来实在不是件愉快的事情。至于运维的难度,反而不用担心,因为现代运维平台已经非常成熟,各种规则都可以轻易配置。
需要额外解释:也有HTTP状态码的问题,客户端仍然需要针对不同的状态码或错误码进行处理,而且除了需要处理HTTP失败的情况,还得处理HTTP成功中的失败。
HTTP状态码的补丁方案
HTTP状态码的主要缺点是错误信息不够丰富,这时有两种方案可以作为补充。

HTTP状态码+JSON错误码
第一种是在响应Body里,将业务错误码与具体错误信息结合,添加到JSON结构体中。例如:

{
‘code’: 10001,
‘message’: ‘错误详情信息’
}
这种情况下,code就是错误码(它可以不是数字,也可以是字符串)。因为是JSON结构,所以还可以扩展额外的字段定义错误的关联信息,比如国际化错误信息,或者区分出给开发人员看的提示信息和给用户提示的信息等。

还是以上面的Deno代码为例,修改为:

router.get(‘/’, (ctx) => {
ctx.response.status = 400;
ctx.response.body = {
code: 10001,
message: ‘参数非法’,
};
});
虽然这种方案仍然可以选择正常响应非JSON结构的数据,但建议还是统一为JSON。比如你响应了一个数字或bool,前端怎么知道你的具体意图是字符串还是这两种呢?

一般来说,前端判断接口响应的Content-Type为application/json时,才用JSON解析。

这时后台需要显式设置响应头为
application/json
,因为框架也无法判断你的意图:

router.get(‘/’, (ctx) => {
ctx.response.status = 200;
ctx.response.body = true;
ctx.response.headers.set(‘Content-Type’, ‘application/json; charset=utf-8’);
});
前端使用
res.json()
就能取到bool值,如果是文本,则用
res.text()

const res = await fetch(‘/’);
const data = await res.json();
console.log(data, typeof data); // true, bool
如果后端想避免显式设置响应头的麻烦,那么可以直接响应一个JSON:

router.get(‘/’, (ctx) => {
ctx.response.status = 200;
ctx.response.body = { result: true };
});
当然,如果是用我的oak_nest[4]的话,加个装饰器就可以了:

import { Controller, Get, Header, HeaderJSON } from ‘https://nd/x/oak_nest@v1.15.0/mod.ts’;

@Controller(‘’)
export class AppController {
@Get(‘test’)
@Header(‘a’, ‘b’)
@HeaderJSON()
test() {
return true;
}
}
扯远了。通常情况下,后端抛出的错误状态码主要是400、401、403、404、500等,偶尔会有413,其中绝大多数都是400。502错误一般由网关层产生,不会在业务层抛出。实际开发中,这并不会给我们带来太大的心智负担。GitLab的API就是这种方式,所有的RESTful API都响应HTTP状态码:

如果失败了,错误信息也是个JSON,它的结构体可以是多样的:

GitHub的API[5]也类似:

优点
这种方案结合了HTTP状态码和JSON状态码的优点:

标准化
简单明了
缓存支持
丰富的信息
易于扩展
缺点
需要额外解释。
甩锅不便:还是前面的问题,如果遇到404错误,怎么判断是哪个链路的错误?如果网关层没有进行数据格式的转换(即默认错误信息为文本或HTML),那么根据响应的Content-Type类型是否为JSON,才能判断出错误是来自网关层还是业务层。如果网关层进行了数据转换(即统一为JSON),那么可以添加独立的响应头或者约定独立的code值来作区分。总之,没有上面200一把梭方便,当然,它也会受网关层数据转换的影响。
响应错误码标头
还有种方案是将错误码添加到响应头中。这种方式提供一种轻量级的方式来传递错误信息,而不必在响应体中增加额外的字段,尤其适用于一些特定的错误情况,例如需要快速判断请求的有效性或进行流控等。

Deno代码改动如下:

router.get(‘/’, (ctx) => {
ctx.response.status = 400;
ctx.response.body = ‘参数非法’;
ctx.response.headers.set(‘code’, ‘10001’);
});
在网络里可以看到响应头code:

与JSON中包含错误码一样,良好的文档和规范非常重要,以确保开发人员能够正确地使用和理解错误码的含义。

优点
标准化
简单明了
缓存支持
改造简单:旧系统(使用HTTP状态码)不用修改原来的错误响应。
甩锅方便:只要找到约定的标头,就能确定这个响应来自业务层(前提是网关层不注入)。
缺点
有限的信息:更丰富的信息还得靠JSON。
需要额外解释。
总结
后端开发中处理接口响应有两种常见模式:使用HTTP状态码和将状态码添加到JSON响应中。HTTP状态码标准化、简单明了、与缓存机制集成,但信息有限。JSON状态码提供丰富错误信息、一致数据结构、易于扩展,但缺乏标准化且增加前端排障难度。

对于HTTP状态码信息有限性的缺陷,一种解决方案是将两种模式结合使用,可以在JSON响应中将业务错误码与具体错误信息结合,以提供更丰富的错误描述。另外,还可以将错误码添加到响应头中,只要保证所有接口都添加错误码,也能轻易判断错误来源。

当然,随着业务链路的日趋复杂,最好还是部署链路追踪的监控平台,方便运维排障,在页面上就能清晰地看到每个链路的耗时情况,针对性地进行优化。

综上所述,HTTP状态码和JSON状态码各有利弊,结合两种模式可以在一定程度上弥补它们各自的不足,提供更好的接口响应处理方式。具体如何选择,取决于具体需求和开发团队的偏好。

参考资料
[1]
HTTP Status Code Registry: https://www./assignments/http-status-codes/http-status-codes.xhtml

[2]
阿里云调用内容检测API: https://help.aliyun.com/document_detail/53414.html

[3]
阿里云短信发送API: https://help.aliyun.com/document_detail/419273.html

[4]
oak_nest: https://nd/x/oak_nest@v1.15.0

[5]
GitHub的API: https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值