18 错误处理(上):如何设计一套科学的错误码?

你好,我是孔令飞。今天我们来聊聊如何设计业务的错误码。

现代的软件架构,很多都是对外暴露RESTful API接口,内部系统通信采用RPC协议。因为RESTful API接口有一些天生的优势,比如规范、调试友好、易懂,所以通常作为直接面向用户的通信规范。

既然是直接面向用户,那么首先就要求消息返回格式是规范的;其次,如果接口报错,还要能给用户提供一些有用的报错信息,通常需要包含Code码(用来唯一定位一次错误)和Message(用来展示出错的信息)。这就需要我们设计一套规范的、科学的错误码。

这一讲,我就来详细介绍下,如何设计一套规范的、科学的错误码。下一讲,我还会介绍如何提供一个errors包来支持我们设计的错误码。

期望错误码实现的功能

要想设计一套错误码,首先就得弄清我们的需求。

RESTful API是基于HTTP协议的一系列API开发规范,HTTP请求结束后,无论API请求成功或失败,都需要让客户端感知到,以便客户端决定下一步该如何处理。

为了让用户拥有最好的体验,需要有一个比较好的错误码实现方式。这里我介绍下在设计错误码时,期望能够实现的功能。

第一个功能是有业务Code码标识。

因为HTTP Code码有限,并且都是跟HTTP Transport层相关的Code码,所以我们希望能有自己的错误Code码。一方面,可以根据需要自行扩展,另一方面也能够精准地定位到具体是哪个错误。同时,因为Code码通常是对计算机友好的10进制整数,基于Code码,计算机也可以很方便地进行一些分支处理。当然了,业务码也要有一定规则,可以通过业务码迅速定位出是哪类错误。

第二个功能,考虑到安全,希望能够对外对内分别展示不同的错误信息。

当开发一个对外的系统,业务出错时,需要一些机制告诉用户出了什么错误,如果能够提供一些帮助文档会更好。但是,我们不可能把所有的错误都暴露给外部用户,这不仅没必要,也不安全。所以也需要能让我们获取到更详细的内部错误信息的机制,这些内部错误信息可能包含一些敏感的数据,不宜对外展示,但可以协助我们进行问题定位。

所以,我们需要设计的错误码应该是规范的,能方便客户端感知到HTTP是否请求成功,并带有业务码和出错信息。

常见的错误码设计方式

在业务中,大致有三种错误码实现方式。我用一次因为用户账号没有找到而请求失败的例子,分别给你解释一下:

第一种方式,不论请求成功或失败,始终返回200 http status code,在HTTP Body中包含用户账号没有找到的错误信息。

例如Facebook API的错误Code设计,始终返回200 http status code:

{
  "error": {
    "message": "Syntax error \"Field picture specified more than once. This is only possible before version 2.1\" at character 23: id,name,picture,picture",
    "type": "OAuthException",
    "code": 2500,
    "fbtrace_id": "xxxxxxxxxxx"
  }
}

采用固定返回200 http status code的方式,有其合理性。比如,HTTP Code通常代表HTTP Transport层的状态信息。当我们收到HTTP请求,并返回时,HTTP Transport层是成功的,所以从这个层面上来看,HTTP Status固定为200也是合理的。

但是这个方式的缺点也很明显:对于每一次请求,我们都要去解析HTTP Body,从中解析出错误码和错误信息。实际上,大部分情况下,我们对于成功的请求,要么直接转发,要么直接解析到某个结构体中;对于失败的请求,我们也希望能够更直接地感知到请求失败。这种方式对性能会有一定的影响,对客户端不友好。所以我不建议你使用这种方式。

第二种方式,返回http 404 Not Found错误码,并在Body中返回简单的错误信息。

例如:Twitter API的错误设计,会根据错误类型,返回合适的HTTP Code,并在Body中返回错误信息和自定义业务Code。

HTTP/1.1 400 Bad Request
x-connection-hash: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
set-cookie: guest_id=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Date: Thu, 01 Jun 2017 03:04:23 GMT
Content-Length: 62
x-response-time: 5
strict-transport-security: max-age=631138519
Connection: keep-alive
Content-Type: application/json; charset=utf-8
Server: tsa_b

{"errors":[{"code":215,"message":"Bad Authentication data."}]}

这种方式比第一种要好一些,通过http status code可以使客户端非常直接地感知到请求失败,并且提供给客户端一些错误信息供参考。但是仅仅靠这些信息,还不能准确地定位和解决问题。

第三种方式,返回http 404 Not Found错误码,并在Body中返回详细的错误信息。

例如:微软Bing API的错误设计,会根据错误类型,返回合适的HTTP Code,并在Body中返回详尽的错误信息。

HTTP/1.1 400
Date: Thu, 01 Jun 2017 03:40:55 GMT
Content-Length: 276
Connection: keep-alive
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/10.0
X-Content-Type-Options: nosniff

{"SearchResponse":{"Version":"2.2","Query":{"SearchTerms":"api error codes"},"Errors":[{"Code":1001,"Message":"Required parameter is missing.","Parameter":"SearchRequest.AppId","HelpUrl":"http\u003a\u002f\u002fmsdn.microsoft.com\u002fen-us\u002flibrary\u002fdd251042.aspx"}]}}

这是我比较推荐的一种方式,既能通过http status code使客户端方便地知道请求出错,又可以使用户根据返回的信息知道哪里出错,以及如何解决问题。同时,返回了机器友好的业务Code码,可以在有需要时让程序进一步判断处理。

错误码设计建议

综合刚才讲到的,我们可以总结出一套优秀的错误码设计思路:

这里其实还有两个功能点需要我们实现:业务Code码设计,以及请求出错时,如何设置http status code

接下来,我会详细介绍下如何实现这两个功能点。

业务Code码设计

要解决业务Code码如何设计这个问题,我们先来看下为什么要引入业务Code码。

在实际开发中,引入业务Code码有下面几个好处:

if err == code.ErrBind {
    ...
}

这里要注意,业务Code码可以是一个整数,也可以是一个整型字符串,还可以是一个字符型字符串,它是错误的唯一标识。

通过研究腾讯云、阿里云、新浪的开放API,我发现新浪的API Code码设计更合理些。所以,我参考新浪的Code码设计,总结出了我推荐的Code码设计规范:纯数字表示,不同部位代表不同的服务,不同的模块。

错误代码说明:100101

通过100101可以知道这个错误是服务 A数据库模块下的记录没有找到错误

你可能会问:按这种设计,每个模块下最多能注册100个错误,是不是有点少?其实在我看来,如果每个模块的错误码超过100个,要么说明这个模块太大了,建议拆分;要么说明错误码设计得不合理,共享性差,需要重新设计。

如何设置HTTP Status Code

Go net/http包提供了60个错误码,大致分为如下5类:

可以看到HTTP Code有很多种,如果每个Code都做错误映射,会面临很多问题。比如,研发同学不太好判断错误属于哪种http status code,到最后很可能会导致错误或者http status code不匹配,变成一种形式。而且,客户端也难以应对这么多的HTTP错误码。

所以,这里建议http status code不要太多,基本上只需要这3个HTTP Code:

如果觉得这3个错误码不够用,最多可以加如下3个错误码:

将错误码控制在适当的数目内,客户端比较容易处理和判断,开发也比较容易进行错误码映射。

IAM项目错误码设计规范

接下来,我们来看下IAM项目的错误码是如何设计的。

Code 设计规范

先来看下IAM项目业务的Code码设计规范,具体实现可参考internal/pkg/code目录。IAM项目的错误码设计规范符合上面介绍的错误码设计思路和规范,具体规范见下。

Code 代码从 100001 开始,1000 以下为 github.com/marmotedu/errors 保留 code。

错误代码说明:100001

服务和模块说明

- 通用说明所有服务都适用的错误,提高复用性,避免重复造轮子。

错误信息规范说明

这里你需要注意,错误信息是直接暴露给用户的,不能包含敏感信息。

IAM API接口返回值说明

如果返回结果中存在 code 字段,则表示调用 API 接口失败。例如:

{
  "code": 100101,
  "message": "Database error",
  "reference": "https://github.com/marmotedu/iam/tree/master/docs/guide/zh-CN/faq/iam-apiserver"
}

上述返回中 code 表示错误码,message 表示该错误的具体信息。每个错误同时也对应一个 HTTP 状态码。比如上述错误码对应了 HTTP 状态码 500(Internal Server Error)。另外,在出错时,也返回了reference字段,该字段包含了可以解决这个错误的文档链接地址。

关于IAM 系统支持的错误码,我给你列了一个表格,你可以看看:

总结

对外暴露的API接口需要有一套规范的、科学的错误码。目前业界的错误码大概有3种设计方式,我用一次因为用户账号没有找到而请求失败的例子,给你做了解释:

这一讲,我参考这3个错误码设计,给出了自己的错误码设计建议:错误码包含HTTP Code和业务Code,并且业务Code会映射为一个HTTP Code。错误码也会对外暴露两种错误信息,一种是直接暴露给用户的,不包含敏感信息的信息;另一种是供内部开发查看,定位问题的错误信息。该错误码还支持返回参考文档,用于在出错时展示给用户,供用户查看解决问题。

建议你重点关注我总结的Code码设计规范:纯数字表示,不同部位代表不同的服务,不同的模块。

比如错误代码100101,其中10代表服务;中间的01代表某个服务下的某个模块;最后的01代表模块下的错误码序号,每个模块可以注册100个错误。

课后练习

  1. 既然错误码是符合规范的,请思考下:有没有一种Low Code的方式,根据错误码规范,自动生成错误码文档呢?
  2. 思考下你还遇到过哪些更科学的错误码设计。如果有,也欢迎在留言区分享交流。

欢迎你在留言区与我交流讨论,我们下一讲见。