cover

Go Echo 如何正确地处理错误

sorcererxw

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.ContextError 函数,在整个调用链上随时被调用!

// 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 的时候,一定记住将关心响应的配置在外层,关心错误的配置在内层,更加里面的才是业务相关。