HTTP与gRPC的错误码

289 阅读6分钟

HTTP错误码

协议上,HTTP 需要一个响应码;业务上,需要有一个 Code 错误码标识业务出错

请求即成功

假设查询一个用户数据的返回:

type user struct {
  id string,
  name string,
  token string,
}

请求成功时:HTTP Status Code: 200 OK

{
    "code": 0,
    "message": "success",
    "data": {
        "id": "1",
        "name": "admin",
        "token": "xxxxxxxx.xxxxxxxx.xxxxxxxx"
    }
}

请求失败时:HTTP Status Code: 200 OK

{
    "code": 010023,
    "message": "user not found",
    "data": {}
}

可以看到,只要服务器不崩,响应返回的HTTP状态码都是200 OK,至于业务的数据正确与否,通过自定义的错误码区别。

错误固定在某个状态码

当然有人也觉得明明业务数据出错了为什么要返回200 OK状态码,不能这样干,于是把出错时固定在某个常用的状态,比如 404 Not Found500 Internal Server Error

请求失败时:HTTP Status Code: 500 Internal Server Error

或者是 HTTP Status Code: 404 Not Found

又或者是其他某个状态

{
    "code": 010032,
    "message": "user not found",
    "data": {}
}

错误映射HTTP状态码

更甚者,这样也不行啊,业务出错了就一个固定的状态码,也识别不了大概什么东西出错了,是请求数据没填,格式不对;还是数据库没有指定条件用户数据;还是服务器代码就有bug跑飞了......

所以呢有大聪明就给业务数据出错的可能性分一分类,映射到HTTP的所有状态码当中,这样根据响应的状态码就可以大概区分一下是什么出错了。

比如用户ID不存在:

HTTP Status Code: 404 Not Found

{
    "code": 010032,
    "message": "user not found",
    "data": {}
}

比如请求参数不正确

HTTP Status Code: 400 Bad Request

{
    "code": 010033,
    "message": "id must be typeof String",
    "data": {}
}

比如服务对于查找的接口还没实现

HTTP Status Code: 501 Not Implemented

{
    "code": 010043,
    "message": "programmer is taking a vacation, no service provides.",
    "data": {}
}

可以参考一下 HTTP Status Codes | HTTP Status Code

这样一来,前端有时候不必解析出具体的业务错误码就可以大致显示什么出错了,毕竟HTTP本身的错误码就代表着某一类的错误,毕竟普通人谁去深究出什么错,还能偷懒少些的代码。

gRPC的错误码

当然 gRPC 对外提供 HTTP 服务也有如此设计,选择的还是第三种映射方案,不同的 Code 对应着业务数据的错误,返回时会根据不同的 Code 映射到不同的 HTTP Status Code

比如 gRPC 错误码的格式在 Go 中是这样的:

type Status struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    // The status code, which should be an enum value of
    // [google.rpc.Code][google.rpc.Code].
    Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
    // A developer-facing error message, which should be in English. Any
    // user-facing error message should be localized and sent in the
    // [google.rpc.Status.details][google.rpc.Status.details] field, or localized
    // by the client.
    Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
    // A list of messages that carry the error details.  There is a common set of
    // message types for APIs to use.
    Details []*anypb.Any `protobuf:"bytes,3,rep,name=details,proto3" json:"details,omitempty"`
}

其中 Code 字段就是状态码,只定义了16种状态码,这也是业务定义的所有状态。按照 Google API 的设计指南的原文是这样的

Google API 必须使用 google.rpc.Code 定义的规范错误代码。单个 API 避免定义其他错误代码,因为开发人员不太可能编写用于处理大量错误代码的逻辑。参考信息:每次 API 调用平均处理 3 个错误代码意味着大多数应用逻辑仅用于错误处理,这并不利于良好的开发者体验。

原文地址

状态码参考 Status Codes | gRPChttps://cloud.google.com/apis/design?hl=zh-cn

转换 HTTP Status Code

那么 gRPC-gateway 会对 gRPC 服务返回的 Proto 数据转换成 JSON ,最后由 HTTP 返回,那么就需要将 Code 映射成 HTTP Status Code

状态码对 HTTP Status Code 的映射 googleapis/google/rpc/code.proto at master · googleapis/googleapis

统一异常

HTTP错误响应

想当年还在Spring开发后台的时候,对于HTTP接口的访问后无论失败或者成功都会有一个统一的格式返回。

{
    "code": xxxxxx,
    "message": "......",
    "data": {}
}

其中 Code 代码业务异常,然后又会有诸如以下的状态码规则设计:

业务应用异常码
2位2位2位或4位

最后就按照规则比如:000000=>Success000001=>Server Interal Error010101 => 用户不存在 之类的设计。

我不去评判这种设计好还是不好,毕竟我接触过的系统都是这样设计的,也很符合规范,但是实际开发的时候很操蛋的事情就会发生

  • 如果没有架构师去评估整个系统的所有异常码的交集,你会发现,为什么不同的状态码会对应着同一个错误信息。

在SSO的应用中,010101=>该用户不存在

在其他有关用户查询的模块的时候,010201=>该用户不存在

......

然后你会悲催的发现,因为后端由不同的人开发,异常码也是根据个人喜好,前端调用的API涉及的应用越多,不同的状态码对应的同一个信息的情况就越多

  • 比如设计的最开始把有交集的异常码放到公共模块,这样会不会好点?然后你发现迭代几个版本之后,又回到上面的循环当中
  • 当你想改变这种循环的时候,不好意思,代码都跑在生产服务器,还有依赖你服务的服务在跑,你敢动吗?敢动吗?敢吗?

gRPC的错误响应

然后我们看一看 gRPC 的错误返回,比如我们的 Echo 服务

func (srv *EchoService) Echo(ctx context.Context, req *pb.EchoRequest) (*pb.EchoResponse, error)
  • 没有错误,返回业务数据

  • 有错误,返回 error

和 HTTP 统一结构比,把数据分开了两部分,正确的时候是数据,错误的时候是 error ,通常用 Status 去表示

type Status struct {
    // The status code, which should be an enum value of
    // [google.rpc.Code][google.rpc.Code].
    Code int32
    // A developer-facing error message, which should be in English. Any
    // user-facing error message should be localized and sent in the
    // [google.rpc.Status.details][google.rpc.Status.details] field, or localized
    // by the client.
    Message string
    // A list of messages that carry the error details.  There is a common set of
    // message types for APIs to use.
    Details []*anypb.Any
}

总结

对于HTTP的接口的返回数据结构,通常会设置如下形式

{
    "code": xxxxxx,
    "message": "......",
    "data": {}
}

当对于gRPC来说,返回数据分成了两个部分,所以没办法和HTTP一样返回统一的数据结构

func xxx(ctx context.Context, req *pb.Request) (data *pb.Response, err error)

我们也知道 gRPC-gateway 就是冒充的 gRPC 客户端,因此需要在返回 HTTP 数据的时候把 响应数据或者错误打包成统一结构

然后我们参考 gRPC 的错误表示结构 Status ,其中的 Details 字段是个 Any 类型,其实我更希望是个 map 结构,错误码就可以表示成

{
    "code": xxxxxx,
    "message": "......",
    "data": {},
    "details": [
        "reference": "https://domain.help.com/xxx.md"
        ......
    ]
}

这样可以把帮助文档或者啥的其他信息存放在 Details 字段。

针对 Code 错误码,会选择只允许定义公共的异常码,单个应用的错误码只使用 Unknown ,由迭代时协商哪些允许抽取到公共定义上,错误额外信息由应用写入 details 上说明