httperrorhandler 捕不到 panic 是因为其仅处理显式 error,不接管 goroutine 内 panic;需用 recover 中间件捕获并调 c.error() 触发它,且须在自定义 httperrorhandler 之后注册。

为什么 HTTPErrorHandler 捕不到 panic?
Echo 的 HTTPErrorHandler 只处理显式调用 c.Error() 或中间件返回的 error,不接管 goroutine 内部 panic。一旦 handler 里发生未捕获 panic(比如空指针解引用、切片越界),默认会触发 http.DefaultServeMux 的兜底逻辑,返回 500 且不走你配的日志流程。
解决办法是加一层 recover 中间件,放在所有业务中间件之前:
func Recover() echo.MiddlewareFunc {
return func(next echo.Handler) echo.Handler {
return echo.HandlerFunc(func(c echo.Context) error {
defer func() {
if r := recover(); r != nil {
err, ok := r.(error)
if !ok {
err = fmt.Errorf("%v", r)
}
c.Error(err) // 这行才真正触发 HTTPErrorHandler
}
}()
return next.ServeHTTP(c)
})
}
}
- 必须在
e.Use(echo.Recover())之前注册自定义HTTPErrorHandler,否则 recover 后的c.Error()不会被你的逻辑捕获 - panic 信息默认不带堆栈,如需完整堆栈,用
debug.PrintStack()单独记录,不要依赖err.Error() - 注意:goroutine 里起的异步任务(如
go func(){}())发生的 panic,这个中间件捕不到,得在 goroutine 内部自己 recover
如何让 HTTPErrorHandler 同时写日志并返回结构化响应?
直接在 HTTPErrorHandler 里调 log.Printf 或 zerolog.Error().Msg() 是常见做法,但容易漏掉关键上下文 —— 比如请求 ID、路径、客户端 IP、耗时。Echo 的 c.Request().Context() 默认不带这些,得靠中间件注入。
推荐在日志中间件中把字段塞进 context,再在错误处理器里取:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
e.HTTPErrorHandler = func(err error, c echo.Context) {
status := http.StatusInternalServerError
if he, ok := err.(*echo.HTTPError); ok {
status = he.Code
}
req := c.Request()
logger := zerolog.Ctx(req.Context()).With().
Str("method", req.Method).
Str("path", req.URL.Path).
Int("status", status).
Logger()
logger.Err(err).Msg("http error")
c.JSON(status, map[string]string{
"error": err.Error(),
})
}
- 确保你在
e.Use(yourLoggingMiddleware)里已用log.WithContext(c.Request().Context())把 logger 绑定到 context,否则zerolog.Ctx()拿不到实例 - 不要在
HTTPErrorHandler里调c.String()或c.HTML(),它们可能绕过 Content-Type 设置;统一用c.JSON()或c.JSONBlob()控制输出格式 - 如果用了
echo.HTTPError(比如echo.NewHTTPError(http.StatusUnauthorized, "token expired")),它的Error()方法返回的是"status unauthorized: token expired",不是原始消息 —— 取he.Message字段才是你传的字符串
404 和 405 错误为什么没进 HTTPErrorHandler?
Echo 默认把路由未匹配(404)和方法不被允许(405)当作“非错误”,直接返回响应,不调用 HTTPErrorHandler。这跟很多人的直觉不符 —— 毕竟它们也是异常响应。
要统一处理,得手动注册 NotFoundHandler 和 MethodNotAllowedHandler:
e.NotFoundHandler = func(c echo.Context) {
c.Error(echo.NewHTTPError(http.StatusNotFound, "route not found"))
}
e.MethodNotAllowedHandler = func(c echo.Context) {
c.Error(echo.NewHTTPError(http.StatusMethodNotAllowed, "method not allowed"))
}
- 这两个 handler 里的
c.Error()会正常进入你配置的HTTPErrorHandler,所以日志、结构化响应逻辑复用即可 - 别忘了在
HTTPErrorHandler里判断err类型:404/405 通常不是*echo.HTTPError,而是 Echo 内部的未导出错误类型,建议统一用echo.IsHTTPError(err)判断 - 如果你用了
e.HideBanner = true,404 响应体默认是纯文本,设成c.JSON()后记得同步改Content-Type,不然前端可能解析失败
日志字段里要不要记请求 Body?
不要。Body 是 io.ReadCloser,读一次就 EOF,后续 handler(比如绑定 JSON)会失败。想记原始请求内容,只能在最前面中间件用 io.ReadAll(c.Request().Body) 读出来,再用 bytes.NewReader() 放回去 —— 但代价高、有内存风险,尤其上传大文件时。
- 生产环境建议只记
c.Request().URL.Query()和c.ParamNames()/c.ParamValues(),覆盖大部分调试需要 - 真要审计 Body,用专门的审计中间件,在特定路径(如
/api/admin)启用,并限制最大长度(http.MaxBytesReader) - 敏感字段(密码、token)必须在日志前过滤,Echo 不提供自动脱敏,得自己写正则或用
strings.ReplaceAll()处理日志字符串
错误处理链条里最容易被跳过的环节,是 panic 发生在中间件之后、handler 执行之前(比如在 Bind() 解析时),那个位置既不在 Recover() 覆盖范围,也不进 HTTPErrorHandler —— 得靠 e.Debug = true 开启 Echo 内置 debug 日志临时定位,再针对性补 recover。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










