跳到主要内容

05-错误处理

上节课我们学了如何校验请求参数,这节课我们来学习错误响应,什么是错误响应呢,我们来看下面一段代码:

func (s *AgentService) RegistryUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserReply, error) {
user, err := s.uc.Register(ctx, &biz.User{req.Name, req.Age})
if err != nil {
return nil, err
}
return &pb.CreateUserReply{
Result: "hello" + user.Name,
}, nil
}

上面的代码是在 service 层上的代码,当注册用户失败时,返回错误后前端接收到的响应是什么样子的呢?带着这个疑问我们开始这一节的学习!

默认错误处理

kratos 提供了一个默认的错误处理编码器,定义了接口出错时返回的格式,返回给前端的响应如下:

{
"code": 500,
"reason": "",
"message": "DELETE https://gitlab-cloud.sandload.com/api/v4/users/205: 404 {message: 404 User Not Found}",
"metadata": {}
}

我们看下 kratos 框架响应错误处理的流程:

  • 在注册路由时候,我们会处理响应失败时候会调用错误处理函数,如下:
// Handle registers a new route with a matcher for the URL path and method.
func (r *Router) Handle(method, relativePath string, h HandlerFunc, filters ...FilterFunc) {
next := http.Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
ctx := r.pool.Get().(Context)
ctx.Reset(res, req)
if err := h(ctx); err != nil {
// enn 是 Server 上的错误处理函数
r.srv.ene(res, req, err)
}
ctx.Reset(nil, nil)
r.pool.Put(ctx)
}))
next = FilterChain(filters...)(next)
next = FilterChain(r.filters...)(next)
r.srv.router.Handle(path.Join(r.prefix, relativePath), next).Methods(method)
}
  • ene 在 Server 初始化时被赋值为 DefaultErrorEncoder 函数:
// NewServer creates an HTTP server by options.
func NewServer(opts ...ServerOption) *Server {
srv := &Server{
network: "tcp",
address: ":0",
timeout: 1 * time.Second,
middleware: matcher.New(),
decVars: DefaultRequestVars,
decQuery: DefaultRequestQuery,
decBody: DefaultRequestDecoder,
enc: DefaultResponseEncoder,
// **************官方实现****************
ene: DefaultErrorEncoder,
// ******************************
strictSlash: true,
router: mux.NewRouter(),
}
for _, o := range opts {
o(srv)
}
srv.router.StrictSlash(srv.strictSlash)
srv.router.NotFoundHandler = http.DefaultServeMux
srv.router.MethodNotAllowedHandler = http.DefaultServeMux
srv.router.Use(srv.filter())
srv.Server = &http.Server{
Handler: FilterChain(srv.filters...)(srv.router),
TLSConfig: srv.tlsConf,
}
return srv
}

我们来看下 DefaultErrorEncoder 函数,这段代码的作用是将错误信息编码并通过 HTTP 响应发送给客户端。编码器根据请求的 Accept 头部字段选择合适的编码格式,然后将错误转换为该格式的数据,并将其包含在 HTTP 响应中返回给客户端。:

函数接受三个参数:

  • w http.ResponseWriter:用于写入 HTTP 响应的对象。
  • r *http.Request:包含 HTTP 请求的对象。
  • err error:要编码的错误。
  1. 首先,代码使用 errors.FromError(err) 将错误转换为自定义的错误类型。

  2. 接下来,代码调用 CodecForRequest 函数来获取适合请求的编码器,并将其存储在变量 codec 中。该函数根据请求的 "Accept" 头部字段来选择编码器。

  3. 然后,代码使用 codec.Marshal(se) 将自定义错误类型 se 编码为字节切片 body。这里使用的编码器将错误转换为特定格式的数据,例如 JSON 或 XML。

  1. 最后,代码将编码后的错误内容 body 写入响应体中,并通过 w.Write(body) 发送给客户端。
// DefaultErrorEncoder encodes the error to the HTTP response.
func DefaultErrorEncoder(w http.ResponseWriter, r *http.Request, err error) {
se := errors.FromError(err)
codec, _ := CodecForRequest(r, "Accept")
body, err := codec.Marshal(se)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", httputil.ContentType(codec.Name()))
w.WriteHeader(int(se.Code))
_, _ = w.Write(body)
}

错误转换

我们再深入细节,看下 kratos 如何 使用 errors.FromError(err) 将错误转换为自定义的错误类型。

// FromError try to convert an error to *Error.
// It supports wrapped errors.
func FromError(err error) *Error {
if err == nil {
return nil
}
if se := new(Error); errors.As(err, &se) {
return se
}
gs, ok := status.FromError(err)
if !ok {
return New(UnknownCode, UnknownReason, err.Error())
}
ret := New(
httpstatus.FromGRPCCode(gs.Code()),
UnknownReason,
gs.Message(),
)
for _, detail := range gs.Details() {
switch d := detail.(type) {
case *errdetails.ErrorInfo:
ret.Reason = d.Reason
return ret.WithMetadata(d.Metadata)
}
}
return ret
}

上面函数接受一个 error 类型的参数 err,表示要转换的错误。

  1. 首先,代码检查 err 是否为 nil,如果是,则返回 nil,表示没有错误。

  2. 接下来,代码使用 errors.As(err, &se) 尝试将 err 转换为 *Error 类型。errors.As 函数用于检查错误链中的每个错误是否可以转换为特定类型。如果转换成功,则返回转换后的错误。

  3. 如果无法将 err 转换为 *Error 类型,则代码尝试使用 status.FromError(err)err 转换为 gRPC 的状态对象。status.FromError 函数用于从错误中提取 gRPC 的状态对象。如果转换成功,代码将使用 gRPC 状态对象的信息创建一个新的 *Error 对象,并返回。

  4. 如果无法将 err 转换为 gRPC 的状态对象,代码将创建一个新的 *Error 对象,并将错误代码、错误原因以及 err.Error() 的值作为参数传递给 New 函数。

  5. 接下来,代码遍历 gRPC 状态对象的详情(details),通过类型断言判断每个详情的类型。如果类型是 *errdetails.ErrorInfo,则将该详情的 Reason 设置为 ret.Reason,并返回带有该详情的元数据(Metadata)的 ret

  6. 最后,如果没有匹配到 *errdetails.ErrorInfo 类型的详情,函数将返回 ret,即带有错误代码、未知原因和 gRPC 状态对象的消息的 *Error 对象。

自定义错误处理

假设我们错误返回前端要求我们只返回 errCode ,改造如下图:

设计哲学

kratos 要求我们对于尽量返回给前端 kratos 自己定义的错误结构:

// Error is a status error.
type Error struct {
Status
cause error
}

type Status struct {
Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"`
Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"`
Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
Metadata map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}

我们之前用 error proto 插件生成的实际上就是 Reason ,方便我们后续使用:


ErrUserAlreadyExist = errors.New(500, v1.ErrorReason_USER_ALREADY_EXIST.String(), "user already exist")

// Is matches each error in the chain with the target value.
func (e *Error) Is(err error) bool {
if se := new(Error); errors.As(err, &se) {
return se.Code == e.Code && se.Reason == e.Reason
}
return false
}