系统服务化构建-状态码设计要点

更新于 2月13日  约 13 分钟

Code状态码码是接口设计中的常见概念,本文主要讨论接口开发中Code码设计。从客户端和服务器端开发的角度,给出具体的工程实践建议和思考。

从笔者之前的一份接口文档定义开始说起,文档中定义的服务端接口输出格式如下

接口输出格式

返回数据由两部分构成,第一部分是对结果集的说明,第二部分是data节点

{
    "code": 4302,

    "message": "no sign",

    "time": 1487832032,

    "data": []
}

第一部分,无论错误与否,都会有如下片段。

code:信息代号

message:信息描述

time:接口返回时间

第二部分是具体数据如下:

data节点

我们可以 看到code=4302,4302并不是一个HTTP 协议状态码,而是一个业务状态码,是业务领域的含义,并非我们常见的HTTP 协议层面的响应状态码。

业务状态码与HTTP 状态码

在REST 接口设计规范中,我们通常都会被引导为这里的Code 应该是HTTP 协议状态码 200,404 或者501等。

实际上这是实践中的一种折中的方式,Code 会包含HTTP状态码和业务状态码

业界为什么会有这种实践,与客户端的解析数据方式有很大关系,下文中会给出答案。

说到这里,我们引出了两个概念,一个是业务状态码,一个是HTTP请求状态码。

两个概念很好理解

业务状态码

状态码对应.jpg

业务状态码是服务端给出的关于业务描述的码,用于客户端明确得知本次请求的资源的状态情况。上文例子中的4032被认为是一个缺少签名sign的业务状态码。有业务状态码输出表明当次HTTP 请求是通的。

业务状态码是可变的,没有业界标准,是一种资源状态描述,与HTTP响应状态码也不存在对应关系。

如下文图片HTTP-200 显示,接口是通的 HTTP 状态响应返回 200,但是业务没有执行成功,code用1 表示。

HTTP-200.png

HTTP/1.1 200 OK
Server: nginx
Date: Wed, 13 Nov 2019 01:27:03 GMT
Content-Type: application/json; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
X-Powered-By: PHP/5.6.15
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, PUT, GET, DELETE, OPTIONS
Access-Control-Allow-Headers: token, app-key, content-type, etcp-base
Access-Control-Max-Age: 86400
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff

{
  "code": 1,
  "message": "states is wrong",
  "data": []
}

HTTP状态码

HTTP请求状态码是HTTP协议的一部分,用于表明HTTP响应状态。

rest响应401.png

HTTP 状态码是HTTP 协议的工程实现,不符合协议规定的服务器端实现,我们可以认为 服务器的HTTP 实现是错误的。

这里举一个简单的幂等性例子,我们知道DELETE 方法是幂等的,如果之前已经删除过特定的资源,再次请求时也应该返回200的响应码,而不是404资源不存在的响应。

服务器端的开发实践

为什么上文中着重介绍状态码的两种分类,因为在业界开发中,这两种码会交叉使用,都有具体的使用场景,语义上不应该被混淆。

这里抛出几个问题

如何用Code码表明此次访问是连接成功的

如何用Code码表明此次访问达到了客户端预想的结果

客户端应该先接收HTTP状态码还是业务状态码

客户端HTTP 请求

先对本文中的客户端做一个简单定义,即调用服务器端接口的调用者,主要是前端WebView,安卓和iOS工程师,统称大前端。前端WebView的请求会涉及到跨域CORS

其实简单来说,客户端工程师最关心两个问题:

第一,接口有没有通。

第二,接口有没有返回我想要的数据。

有经验的客户端工程师会关心接口如果不通,返回提示是否可以指导我排除错误,或者说跟踪到问题所在。接下来接口设计是否合理,是否有隐患,就看工程师职业水平和职业素养了。

客户端排除法

客户端HTTP 请求的通用方法是采用排除法,什么是排除法,客户端在请求服务端的REST 接口时,会先在网络层面判断接口是否通,包括404或者200。客户端只关心本身有用的Code,其余都按异常处理。

网络层判断这个任务客户端会交给具体的HTTP 拦截器 (Intercept),之后才会接受当次接口的描述信息也就是data 和code,做业务前端处理。

axios 就是一个主要用于浏览器请求的HTTP 客户端,包含请求响应拦截器(Intercept request and response)

Promise based HTTP client for the browser and node.js

以下代码是两段响应拦截,分别是拦截HTTP 协议的401验证不通过和自定义业务代码的验证不通过。

HTTP 401

axios.interceptors.response.use(function (res) {
  return res.data;
}, function (res) {
  let response = res && res.response || {};
  if (response.status == 401) {
    tool.showToast('登录已过期,请重新登录。');
    tool.removeReUserInfo();
    location.hash = "#/login";
  } else {
    tool.showToast('请求数据失败,请稍后再试。');
  }
});

自定义业务代码

axios.interceptors.response.use(function (res) {
  Indicator.close(); //首先关闭所有的提示窗口
  var data = res.data;

  if (data.code == 4034) { //签名不合法
    tool.showToast('签名不合法。');
  } else if (data.code == 4033) { //token失效
    tool.showToast('登录已过期,自动登录中。');
    tool.removeUserInfo();
     location.hash = "#/login";
  } else {
    return data;
  }

}, function (err) {
  // alert(JSON.stringify(err));
  Indicator.close(); //首先关闭所有的提示窗口
  tool.showToast('请求数据失败,请稍后再试。');
});

安卓客户端拦截器

okhttp 是一个安卓平台的HTTP 客户端,其中包含一个网络拦截器(Network Interceptors)。网络状态码和业务状态码的截取都交给拦截器处理处理。

图片.png

设计倡导

这里我们重新梳理之前提出的三个问题,给出一些解决思路,同时总结一些经验

如何用Code码表明此次访问是连接成功的?

这里应该以HTTP 状态码为依据,主要有200, 401 ,表明请求是【触碰到关于的数据处理的业务部分了】如

HTTP/1.1 200 OK
{
  "code": 0,
  "message": "客户端已是最新版本",
  "data": {
    "code": 10
  },
  "debug_stack": []
}
HTTP/1.1 401 Unauthorized
Server: nginx

{
  "name": "Unauthorized",
  "message": "Token is expired",
  "code": 401,
  "status": 401,
  "type": "yii\\web\\HttpException"
}
如何用Code码表明此次访问达到了客户端预想的结果?

这里以业务状态码的数据为依据,获取到的就是真实的。Code可以用 0 表示。

{
  "code": 0,
  "message": "客户端已是最新版本",
  "data": {
    "code": 10
  },
  "debug_stack": []
}
客户端应该先接收HTTP 状态码还是业务状态码?

当然是先接收HTTP 状态码,其次是业务状态码,不混淆,也不能混淆。从软件分层的角度来说,接收HTTP 状态码在接收业务状态码的上层,通常由拦截器来做,比如token过期的401阻挡。

一般情况下,0表示成功,1表示业务操作失败。业务复杂时,需要维护多种业务状态码。下图是微信平台的业务状态码枚举,场景较多。

微信错误码.png

接口字段整齐

这里所说的字段整齐是指服务提供方给到的数据结构是完整的,最通用的,现在大部分接口格式如下

三个字段应该都存在,可以为空,避免NULL。

{
    "code": 200,
    "data": null,
    "message": "成功"
}

对于提供接口开发的服务者而言,code和message字段都会给出,存在异议的字段是data。更严谨的说法是 请求的资源描述中伙包含资源状态编码和描述信息,如message。

当data 没有数据时,有的工程师喜欢把data置为null,或者直接不返回data字段。这两种方式都不合理,都会增加调用方的判断成本,尤其是null,如果调用方写法不严谨的话,很容易引发程序异常。

接口总会有返回值,data字段就是实际的返回值,可以是空字符串,空数组,bool类型。

图灵奖得主Tony Hoare 曾经公开表达过null是一个糟糕的设计,null总是代表着不确定性。

null.jpeg

业务状态码不等于异常

业务状态码和异常是两个概念,切忌混淆。业务状态码指正常的业务处理结果的显示说明,而异常通常由于语法错误,数据缺失造成程序不能正常执行完成。不能通过业务状态码而屏蔽异常。

总结

本文从接口文档开始,引出了状态码的概念,细分为网络状态码和业务状态码。结合服务器端和客户端的编程角度,介绍了各自的使用场景。在分布式服务化的网络架构中,清晰的网络状态码和业务状态码有助于服务链路的跟踪和服务的链路跟踪,尤其是异常的定位和捕获。业务状态码应该趋于同一化,与网络状态码相互补充。

参考文档中给出一些资源,有兴趣的读者可以参考阅读。

参考文档

axios-interceptors-response-undefined

阅读 4.3k更新于 2月13日

推荐阅读
图南科技
用户专栏

分享,记录工作学习过程中的收获,心得,体会。主要涉及分布式系统设计,MoongoDB,消息队列,PHP技术栈...

750 人关注
16 篇文章
专栏主页
目录