grpc应用中的错误处理

最近在实际编码的项目中遇到一个小问题,在前端请求接口时,client端会去调用服务。

在HTTP请求时,服务端返回http状态码,客户端可以根据状态码判断一些处理结果,同时我们可以限定一些实际的业务状态码,来限定一些锚定的错误。

同样的,gRPC调用,client同样希望能够从server处获得准确的处理信息。发生错误时,gRPC会返回状态错误代码以及错误的相关消息,还可以通过挂载来携带更多更详细的自定义信息,携带的一切信息实现都是通用的。

gRPC状态码:

Code Number
OK 0
CANCELLED 1
UNKNOWN 2
INVALID_ARGUMENT 3
... ...

更详细的状态码可以从这里,或者源码中看到

gRPC中的错误

不处理错误

对于一个server端的返回值,例如这样一个代码:

golang 复制代码
func (s *Server) Login(ctx context.Context, req *proto.LoginReq) (*proto.LoginRes, error) {
	if req.Email == "" || req.Password == "" {
		return nil, errors.New("invalid request")
	}
        ...
}

在请求体中的Email或Password为空时报错,那用一个client来访问服务:

golang 复制代码
in := *proto.LoginReq{}
client := newClient()
res, err := client.Login(ctx, in) // 
if err != nil {
	fmt.Println(err)
        fmt.Printf("%T", err)
	return
}

我们会得到一个错误

vbscript 复制代码
*status.Error
rpc error: code = Unknown desc = invalid request

可以看到,服务端的错误,是通过一个类型为 *status.Error 的对象传递给客户端的,这样客户端拿到的error就是经过了gRPC处理过的错误,而不是我们原有的错误,先来看看 status.Error 这个结构体的详细用法吧

status.Error

可以找到相关源码文件:google.golang.org/grpc/status/status.go

golang 复制代码
// Error wraps a pointer of a status proto. It implements error and Status,
// and a nil *Error should never be returned by this package.
type Error struct {
	s *Status
}

func (e *Error) Error() string {
	return e.s.String()
}

// GRPCStatus returns the Status represented by se.
func (e *Error) GRPCStatus() *Status {
	return e.s
}

// Is implements future error.Is functionality.
// A Error is equivalent if the code and message are identical.
func (e *Error) Is(target error) bool {
	tse, ok := target.(*Error)
	if !ok {
		return false
	}
	return proto.Equal(e.s.s, tse.s.s)
}

关于Status的详情:

golang 复制代码
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"`
}

可以看出,status.Error基本上就是对Status以及我们实际抛出的error异常的一个聚合,那这样经过了gRPC的封装,我们无法直接将它与我们的一些自定义错误、哨兵error来进行等值判断或者断言操作了,当我们想向前端暴露一些具体错误信息时要如何做呢?

哨兵error

我们自定义一个error结构体以及定义一些哨兵error,结构体实现error interface,例如:

golang 复制代码
var (
	ErrNotFound       = CommonError{errCode: 1001, errMsg: "not found"}
	ErrPassword       = CommonError{errCode: 1002, errMsg: "wrong password"}
	ErrInvalidRequest = CommonError{errCode: 1003, errMsg: "invalid request"}
)

type CommonError struct {
	errCode uint32
	errMsg  string
}

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

// 返回给前端的错误信息
func (e CommonError) GetErrMsg() string {
	return e.errMsg
}

// 实现interface
func (e CommonError) Error() string {
	return fmt.Sprintf("ErrCode: %d, ErrMsg: %s", e.errCode, e.errMsg)
}

我们需要定义一个关于error的proto message,将错误信息封装成可序列化的,之后执行protoc命令生成相应代码:

proto 复制代码
syntax = "proto3";
package pb;
message Error {
  string code = 1;
  string message = 2;
}

在server与client通信过程中,我们需要将我们自定义的error转化为gRPC可处理的,这里我们通过拦截器实现:

golang 复制代码
// 流式拦截器
func StreamInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
	return errConvert(handler(srv, ss)) 
}

// 一元拦截
func UnaryInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
	resp, err = handler(ctx, req)
	return resp, errConvert(err)
}

// 错误转化,grpc错误解析
func errConvert(err error) error {
	if err == nil {
		return nil
	}
	causeErr := errors.Cause(err)
	pbError := &proto.Error{}
	if cerr, ok := causeErr.(CommonError); ok {
		pbError.Code = cerr.GetErrCode()
		pbError.Message = cerr.GetErrMsg()
	}
	st := status.New(codes.Internal, causeErr.Error())
	st, e := st.WithDetails(pbError)
	if e != nil {
		...
	}
	return st.Err()
}

在拦截器中,我们把真实的error转化为可序列化的 proto.Error,然后调用Status.WithDetails() 进行序列化,这样client就能够拿到一些定义的错误码和信息等:

golang 复制代码
cause := errors.Cause(err)
st, ok := status.FromError(cause)
if ok {
    details := st.Details()
    if details != nil && len(details) > 0 {
        if pbErr, ok := details[0].(*pb.Error); ok {
            return newRPCClientError(pbErr.Code, pbErr.Message, pbErr.Details)
        }
    }
}
return err

通过这个client端的拦截器,我们就可以得到具体的错误信息了!

简化但不知道有啥弊端的做法

由于Status的Detail是封装在list中的,不太愿意写麻烦的遍历过程,所以直接将错误码覆盖至status.code来进行传输:

server拦截器中

golang 复制代码
// 错误转化,grpc错误解析
func errConvert(err error) error {
	if err == nil {
		return nil
	}
	causeErr := errors.Cause(err)
	if cerr, ok := causeErr.(CommonError); ok {
		st := status.New(codes.Code(cerr.GetErrCode()), cerr.GetErrMsg())
    	return st.Err()
	}
	return err
}

client中

golang 复制代码
cause := errors.Cause(err)
st, ok := status.FromError(cause)
if ok {
        if uint32(st.Code()) <= 17 { // grpc的原生错误码
                PackFailWithMsg(ctx, http.StatusInternalServerError, uint32(st.Code()), st.Message())
                return
        } else { // 哨兵预定义错误
                PackFailWithMsg(ctx, http.StatusOK, uint32(st.Code()), st.Message())
                return
        }
}
PackFailWithMsg(ctx, http.StatusInternalServerError, uint32(500), err.Error())

在客户端层面对错误类型进行甄别,有个不好的地方就是不能同时携带多条报错信息,但是代码能精简一些!

大家可以自己去尝试一下!有什么更好的解决方案请告诉我!!!

相关推荐
2401_882727573 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
追逐时光者4 小时前
.NET 在 Visual Studio 中的高效编程技巧集
后端·.net·visual studio
大梦百万秋5 小时前
Spring Boot实战:构建一个简单的RESTful API
spring boot·后端·restful
斌斌_____5 小时前
Spring Boot 配置文件的加载顺序
java·spring boot·后端
路在脚下@5 小时前
Spring如何处理循环依赖
java·后端·spring
海绵波波1076 小时前
flask后端开发(1):第一个Flask项目
后端·python·flask
小奏技术7 小时前
RocketMQ结合源码告诉你消息量大为啥不需要手动压缩消息
后端·消息队列
AI人H哥会Java9 小时前
【Spring】控制反转(IoC)与依赖注入(DI)—IoC容器在系统中的位置
java·开发语言·spring boot·后端·spring
凡人的AI工具箱9 小时前
每天40分玩转Django:Django表单集
开发语言·数据库·后端·python·缓存·django
奔跑草-9 小时前
【数据库】SQL应该如何针对数据倾斜问题进行优化
数据库·后端·sql·ubuntu