grpc错误处理

0.1、索引

https://waterflow.link/articles/1665938704477

我们都知道当发起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和解释可以参考这里:https://github.com/grpc/grpc/...

1、grpc错误

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

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

首先我们在服务端,修改下代码,在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中实现自定义错误的方式,大家可以自己写下试试吧。

15 声望
0 粉丝
0 条评论
推荐阅读
golang中的错误处理
0.1、索引[链接]1、panic当我们执行panic的时候会结束下面的流程: {代码...} {代码...} 但是panic也是可以捕获的,我们可以使用defer和recover实现: {代码...} {代码...} 那什么时候适合panic呢?在 Go 中,pan...

liuyuede阅读 628

封面图
写给go开发者的gRPC教程-通信模式
本篇为【写给go开发者的gRPC教程系列】第二篇第一篇:protobuf基础第二篇:通信模式上一篇介绍了如何编写 protobuf 的 idl,并使用 idl 生成了 gRPC 的代码,现在来看看如何编写客户端和服务端的代码Simple RPC (...

liangwt1阅读 827

封面图
从 await-to-js 到 try-run-js
之前在做 code review 时候发现有同事使用 try catch 包装了一堆异步代码,于是个人就觉得很奇怪,难道不应该只 catch 可能出问题的代码吗?同事告诉我说 try catch 太细的话会出现内外作用域不一致,需要提前声...

jump__jump阅读 953

go精通protobuf连载一:安装protobuf与protoc-gen-go
protobuf是一种与语言无关、与平台无关的可扩展的插件,用于序列化结构化数据。只需要定义一下protobuf结构的文件 .proto 然后就可以使用protoc 命令生成对应的编程语言的结构的文件。

海生阅读 818

写给go开发者的gRPC教程-protobuf基础
序列化协议。gRPC使用protobuf,首先使用protobuf定义服务,然后使用这个文件来生成客户端和服务端的代码。因为pb是跨语言的,因此即使服务端和客户端语言并不一致也是可以互相序列化和反序列化的

liangwt阅读 806评论 1

封面图
protocol-buffers namespace conflict
在运行grpc服务,加载*.pb.go时可能会报冲突错误,如文件名命名冲突:其实针对文件名冲突的错误处理开发者有移除过"文件冲突检测":[链接]后来发现有问题又加上了"文件冲突检测":[链接]

AVOli阅读 790

Grpc使用buf.build 快速编译
本文通过实例来讲解使用buf来快速的编译proto文件,不需要再用protoc命令加各种参数来编译proto文件。事先需要安装buf, 安装方法请参考官网installation我们先建立目录结构auth.proto {代码...} 进入proto文件夹...

这个名字好长阅读 450

15 声望
0 粉丝
宣传栏