grpc中的错误处理

Golang
592
0
0
2022-11-16

0.1、索引

waterflow.link/articles/1665938704...

我们都知道当发起http请求的时候,服务端会返回一些http状态码,不管是成功还是失败。客户端可以根据服务端返回的状态码,判断服务器出现了哪些错误。

我们经常用到的比如下面这些:

  • 200:OK,请求成功
  • 204:NO CONTENT,此请求没有要发送的内容,但标头可能很有用。 用户代理可以用新的更新其缓存的资源头。
  • 400:Bad Request,由于被认为是客户端错误(例如,格式错误的请求语法、无效的请求消息帧或欺骗性请求路由),服务器无法或不会处理请求。
  • 404:Not Found,服务器找不到请求的资源。 在浏览器中,这意味着无法识别 URL。 在 API 中,这也可能意味着端点有效但资源本身不存在。

同样的,当我们调用 gRPC 调用时,客户端会收到带有成功状态的响应或带有相应错误状态的错误。 客户端应用程序需要以能够处理所有潜在错误和错误条件的方式编写。 服务器应用程序要求您处理错误并生成具有相应状态代码的适当错误。

发生错误时,gRPC 会返回其错误状态代码之一以及可选的错误消息,该消息提供错误条件的更多详细信息。 状态对象由一个整数代码和一个字符串消息组成,这些消息对于不同语言的所有 gRPC 实现都是通用的。

gRPC 使用一组定义明确的 gRPC 特定状态代码。 这包括如下状态代码:

  • OK:成功状态,不是错误。
  • CANCELLED:操作被取消,通常是由调用者取消的。
  • DEADLINE_EXCEEDED:截止日期在操作完成之前到期。
  • INVALID_ARGUMENT:客户端指定了无效参数。

详细的状态code、number和解释可以参考这里:github.com/grpc/grpc/blob/master/d...

1、grpc错误

之前的章节中我们写过关于简单搭建grpc的文章:waterflow.link/articles/1665674508...

我们在这个基础上稍微修改一下,看下下面的例子。

首先我们在服务端,修改下代码,在service的Hello方法中加个判断,如果客户端传过来的不是hello,我们我们将返回grpc的标准错误。像下面这样:

func (h HelloService) Hello(ctx context.Context, args *String) (*String, error) {
    time.Sleep(time.Second)
  // 返回参数不合法的错误 
    if args.GetValue() != "hello" {
        return nil, status.Error(codes.InvalidArgument, "请求参数错误")
    }
    reply := &String{Value: "hello:" + args.GetValue()}
    return reply, nil
}

我们客户端的代码像下面这样:

func unaryRpc(conn *grpc.ClientConn) {
    client := helloservice.NewHelloServiceClient(conn)
    ctx := context.Background()
    md := metadata.Pairs("authorization", "mytoken")
    ctx = metadata.NewOutgoingContext(ctx, md)
  // 调用Hello方法,并传入字符串hello
    reply, err := client.Hello(ctx, &helloservice.String{Value: "hello"})
    if err != nil {
        log.Fatal(err)
    }
    log.Println("unaryRpc recv: ", reply.Value)
}

我们开启下服务端,并运行客户端代码:

go run helloclient/main.go    
invoker request time duration:  1
2022/10/16 23:05:18 unaryRpc recv:  hello:hello

可以看到会输出正确的结果。现在我们修改下客户端代码:

func unaryRpc(conn *grpc.ClientConn) {
    client := helloservice.NewHelloServiceClient(conn)
    ctx := context.Background()
    md := metadata.Pairs("authorization", "mytoken")
    ctx = metadata.NewOutgoingContext(ctx, md)
  // 调用Hello方法,并传入字符串f**k
    reply, err := client.Hello(ctx, &helloservice.String{Value: "f**k"})
    if err != nil {
        log.Fatal(err)
    }
    log.Println("unaryRpc recv: ", reply.Value)
}

然后运行下客户端代码:

go run helloclient/main.go
invoker request time duration:  1
2022/10/16 23:14:13 rpc error: code = InvalidArgument desc = 请求参数错误
exit status 1

可以看到我们获取到了服务端返回的错误。

2、获取grpc错误类型

有时候客户端通过服务端返回的不同错误类型去做一些具体的处理,这个时候客户端可以这么写:

func unaryRpc(conn *grpc.ClientConn) {
    client := helloservice.NewHelloServiceClient(conn)
    ctx := context.Background()
    md := metadata.Pairs("authorization", "mytoken")
    ctx = metadata.NewOutgoingContext(ctx, md)
    reply, err := client.Hello(ctx, &helloservice.String{Value: "f**k"})
    if err != nil {
        fromError, ok := status.FromError(err)
        if !ok {
            log.Fatal(err)
        }
    // 判断服务端返回的是否是指定code的错误 
        if fromError.Code() == codes.InvalidArgument {
            log.Fatal("invalid arguments")
        }
    }
    log.Println("unaryRpc recv: ", reply.Value)
}

我们可以看下status.FromError的返回结果:

  • 如果 err 是由这个包产生的或者实现了方法 GRPCStatus() *Status,返回相应的状态。
  • 如果 err 为 nil,则返回带有代码的状态。OK 并且没有消息。
  • 否则,err 是与此包不兼容的错误。 在这个情况下,返回一个 Status 结构是 code.Unknown 和 err 的 Error() 消息,并且ok为false。

我们重新执行下客户端代码:

go run helloclient/main.go
invoker request time duration:  1
2022/10/16 23:26:11 invalid arguments
exit status 1

可以看到,当服务端返回的是codes.InvalidArgument错误时,我们重新定义了错误。

3、获取grpc错误更详细的信息

当我们服务端返回grpc错误时,我们想带上一些自定义的详细错误信息,这个时候就可以像下面这样写:

func (h HelloService) Hello(ctx context.Context, args *String) (*String, error) {
    time.Sleep(time.Second)
    if args.GetValue() != "hello" {
        errorStatus := status.New(codes.InvalidArgument, "请求参数错误")
        details, err := errorStatus.WithDetails(&errdetails.BadRequest_FieldViolation{
            Field:       "string.value",
            Description: fmt.Sprintf("expect hello, get %s", args.GetValue()),
        })
        if err != nil {
            return nil, errorStatus.Err()
        }
        return nil, details.Err()
    }
    reply := &String{Value: "hello:" + args.GetValue()}
    return reply, nil
}

我们重点看下WithDetails方法:

  • 该方法传入一个proto.Message类型的数组,Message是一个protocol buffer的消息
  • 返回一个新Status,并将提供的详细信息消息附加到Status
  • 如果遇到任何错误,则返回 nil 和遇到的第一个错误

然后我们修改下客户端代码:

func unaryRpc(conn *grpc.ClientConn) {
    client := helloservice.NewHelloServiceClient(conn)
    ctx := context.Background()
    md := metadata.Pairs("authorization", "mytoken")
    ctx = metadata.NewOutgoingContext(ctx, md)
    reply, err := client.Hello(ctx, &helloservice.String{Value: "f**k"})
    if err != nil {
        fromError, ok := status.FromError(err)
        if !ok {
            log.Fatal(err)
        }
        if fromError.Code() == codes.InvalidArgument {
      // 获取错误的详细信息,因为详细信息返回的是数组,所以这里我们需要遍历 
            for _, detail := range fromError.Details() {
                detail = detail.(*proto.Message)
                log.Println(detail)
            }
            log.Fatal("invalid arguments")
        }
    }
    log.Println("unaryRpc recv: ", reply.Value)
}

接着重启下服务端,运行下客户端代码:

go run helloclient/main.go
invoker request time duration:  1
2022/10/16 23:58:51 field:"string.value"  description:"expect hello, get f**k"
2022/10/16 23:58:51 invalid arguments
exit status 1

可以看到详细信息打印出来了。

4、定义标准错误之外的错误

现实中我们可能会有这样的要求:

  • 当grpc服务端是自定义错误时,客户端返回自定义错误
  • 当grpc服务端返回的是标准错误时,客户端返回系统错误

我们可以创建一个自定义测错误类:

package xerr

import (
    "fmt"
)

/**
常用通用固定错误
*/
type CodeError struct {
    errCode uint32
    errMsg  string
}

//返回给前端的错误码
func (e *CodeError) GetErrCode() uint32 {
    return e.errCode
}

//返回给前端显示端错误信息
func (e *CodeError) GetErrMsg() string {
    return e.errMsg
}

func (e *CodeError) Error() string {
    return fmt.Sprintf("ErrCode:%d,ErrMsg:%s", e.errCode, e.errMsg)
}

然后grpc服务端实现一个拦截器,目的是把自定义错误转换成grpc错误:

func LoggerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {

    resp, err = handler(ctx, req)
    if err != nil {
        causeErr := errors.Cause(err)                // err类型 
        if e, ok := causeErr.(*xerr.CodeError); ok { //自定义错误类型

            //转成grpc err
            err = status.Error(codes.Code(e.GetErrCode()), e.GetErrMsg())
        } 

    }

    return resp, err
}

然后客户端处理错误代码的部分修改如下:

//错误返回

        causeErr := errors.Cause(err)                // err类型 
        if e, ok := causeErr.(*xerr.CodeError); ok { //自定义错误类型 
            //自定义CodeError
            errcode = e.GetErrCode()
            errmsg = e.GetErrMsg()
        } else {
            errcode := uint32(500)
            errmsg := "系统错误"
        }

其中用到的errors.Cause的作用就是递归获取根错误。

这其实就是go-zero中实现自定义错误的方式,大家可以自己写下试试吧。