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
:要编码的错误。
首先,代码使用
errors.FromError(err)
将错误转换为自定义的错误类型。接下来,代码调用
CodecForRequest
函数来获取适合请求的编码器,并将其存储在变量codec
中。该函数根据请求的 "Accept" 头部字段来选择编码器。然后,代码使用
codec.Marshal(se)
将自定义错误类型se
编码为字节切片body
。这里使用的编码器将错误转换为特定格式的数据,例如 JSON 或 XML。
- 最后,代码将编码后的错误内容
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
,表示要转换的错误。
首先,代码检查
err
是否为nil
,如果是,则返回nil
,表示没有错误。接下来,代码使用
errors.As(err, &se)
尝试将err
转换为*Error
类型。errors.As
函数用于检查错误链中的每个错误是否可以转换为特定类型。如果转换成功,则返回转换后的错误。如果无法将
err
转换为*Error
类型,则代码尝试使用status.FromError(err)
将err
转换为 gRPC 的状态对象。status.FromError
函数用于从错误中提取 gRPC 的状态对象。如果转换成功,代码将使用 gRPC 状态对象的信息创建一个新的*Error
对象,并返回。如果无法将
err
转换为 gRPC 的状态对象,代码将创建一个新的*Error
对象,并将错误代码、错误原因以及err.Error()
的值作为参数传递给New
函数。接下来,代码遍历 gRPC 状态对象的详情(details),通过类型断言判断每个详情的类型。如果类型是
*errdetails.ErrorInfo
,则将该详情的Reason
设置为ret.Reason
,并返回带有该详情的元数据(Metadata)的ret
。最后,如果没有匹配到
*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
}