labstack/echo 是 Go 生态内被广为使用的 Web 框架。相比其他知名框架,echo 特点之一是 handler 上通过返回值抛出 error,而不是使用 context 传递 error:
e.GET("/ping", func(c echo.Context) error {
err := doSomthing()
if err != nil {
return err
}
return c.NoContent(http.StatusOK)
})
在以返回值抛出错误的 Go 编程范式下,这种方式无疑更加方便自然。但是很多情况下一个错误并不是程序错误,而是用户输入错误,如果不对错误处理加以控制,会使所有错误都被认为是 HTTP/500(内部错误)。所以能否正确处理 handler 抛出的 error ,决定了最终响应能否符合预期。
想要在 echo 中正确地处理错误,首先需要理解 echo 中一个 error 会经历什么:
// MiddlewareFunc defines a function to process middleware.
MiddlewareFunc func(HandlerFunc) HandlerFunc
// HandlerFunc defines a function to serve HTTP requests.
HandlerFunc func(Context) error
// HTTPErrorHandler is a centralized HTTP error handler.
HTTPErrorHandler func(error, Context)
func applyMiddleware(h HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc {
for i := len(middleware) - 1; i >= 0; i-- {
h = middleware[i](h)
}
return h
}
// ServeHTTP implements `http.Handler` interface, which serves HTTP requests.
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var c *echo.Context
e.findRouter(r.Host).Find(r.Method, GetPath(r), c)
h = c.Handler()
h = applyMiddleware(h, e.middleware...)
if err := h(c); err != nil {
e.HTTPErrorHandler(err, c)
}
}
上面代码展示了 echo 处理请求的核心工作流程。可以看到,在 middleware-handler 的洋葱模型之外,echo 使用了 HTTPErrorHandler 处理最终的错误。正如 echo 文档中所言:
Echo advocates for centralized HTTP error handling by returning error from middleware and handlers. Centralized error handler allows us to log errors to external services from a unified location and send a customized HTTP response to the client.
这个机制给所有错误提供了一个兜底的方案,保证所有错误都能被有效处理。
这里肯定会有人产生疑问:为什么要单独拎出一个 Error Handler,直接编写一个 Error Handle Middleware 包在整个洋葱圈外面就好了:
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
err := next(c)
return handleError(err, c)
}
})
没错,这样也可以处理错误,甚至更加简洁,但是 echo 的 HTTPErrorHandler 可不是简简单单地处理最终错误,它可以通过 echo.Context 的 Error 函数,在整个调用链上随时被调用!
// Error invokes the registered HTTP error handler. Generally used by middleware.
func (c *context) Error(err error) {
c.echo.HTTPErrorHandler(err, c)
}
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
err := next(c)
if err!=nil {
c.Error(err)
}
return
}
})
可是为什么要这么做呢?这里我们就需要看看 HTTPErrorHandler 到底干了什么。HTTPErrorHandler 本身只是一个函数类型,可以由使用者自定义,不过 echo 默认提供了一个 DefaultHTTPErrorHandler,我们以它作为参考:
// DefaultHTTPErrorHandler is the default HTTP error handler. It sends a JSON response
// with status code.
func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
he, ok := err.(*HTTPError)
if !ok {
he = &HTTPError{
Code: http.StatusInternalServerError,
Message: http.StatusText(http.StatusInternalServerError),
}
}
// Send response
if !c.Response().Committed {
err = c.JSON(he.Code, map[string]string{"message":he.message})
if err != nil {
e.Logger.Error(err)
}
}
}
通过源码可以看到,DefaultHTTPErrorHandler 根据 error 的具体类型,直接写入了相应的 response。我们都知道,http response 写入之后就会关闭,在手动调用之后 context.Error ,后面的中间件将不能再改变响应体。
回到问题本身,为什么需要手动调用 context.Error ?因为只有真正调用 HTTPErrorHandler 之后,最终响应的状态才能被确定下来。很多时候,服务监控需要用到记录最终的响应状态,如果一路向上抛错误,将没有机会获取最终的状态,所以当遇到错误的时候,需要主动执行 context.Error。
// LoggerWithConfig returns a Logger middleware with config.
// See: `Logger()`.
func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) {
req := c.Request()
res := c.Response()
if err = next(c); err != nil {
c.Error(err)
}
if _, err = config.template.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) {
switch tag {
case "status":
n := res.Status
s := config.colorer.Green(n)
switch {
case n >= 500:
s = config.colorer.Red(n)
case n >= 400:
s = config.colorer.Yellow(n)
case n >= 300:
s = config.colorer.Cyan(n)
}
return buf.WriteString(s)
}
return 0, nil
}); err != nil {
return
}
}
}
}
以上是 echo 提供的 Logger 中间件,可以看到如果 Logger 取得了错误,会通过 c.Error 来确定最终响应,并读取响应状态码,并在最后返回 nil 的 error,防止下一级 middleware 再一次处理同一个 error。
正是因为某些 middleware 不得不执行 context.Error 导致 error 将无法继续往下传递,以及 response 关闭,在使用 middleware 的时候需要格外注意顺序。根据我的经验,可以将 middleware 分为两种:
- 关心错误
这种 middleware 需要对错误本身进行处理,比如转换特定错误为 HTTPError、使用 Sentry 上报错误栈。
- 关心响应
这种 middleware 需要监控响应体的状态,它们本身对 error 不感兴趣,但是碰到 err≠nil 的情况,会主要调用 HTTPErrorHandler 以确定最终响应,并且最终会返回一个 nil 的 error。典型如 Prometheus 和 Logger。
(当然还有一些 middleware 既不关心错误也不关心响应,它们并不在本文讨论范围)
所以我们在为服务配置 middleware 的时候,一定记住将关心响应的配置在外层,关心错误的配置在内层,更加里面的才是业务相关。