最近在实际编码的项目中遇到一个小问题,在前端请求接口时,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())
在客户端层面对错误类型进行甄别,有个不好的地方就是不能同时携带多条报错信息,但是代码能精简一些!
大家可以自己去尝试一下!有什么更好的解决方案请告诉我!!!