grpc中的錯誤處理

slowquery發表於2022-10-17

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中實現自定義錯誤的方式,大家可以自己寫下試試吧。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章