Echo 的 HTTPErrorHandler 不捕获中间件 panic,因 panic 绕过错误处理器直接由 Go runtime 处理为 500 响应;需用 defer/recover 中间件捕获 panic 并调用 c.Error() 触发统一错误处理。

为什么 echo.HTTPErrorHandler 不会捕获中间件 panic?
因为 Echo 的错误处理器只处理路由处理器(handler)中显式调用 c.Error() 或返回 error 的情况,而中间件里直接 panic 会绕过它——默认 panic 由 Go runtime 捕获并写入 HTTP response 为 500,但无堆栈、无自定义逻辑。
真正要统一捕获所有 panic(包括中间件和 handler),必须替换底层的 echo.HTTPErrorHandler 实现,并配合 recover() 手动拦截。
- 默认行为:中间件 panic →
http.Error(w, "Internal Server Error", 500),不走HTTPErrorHandler - 正确做法:在最外层中间件(或通过
e.Use()注册的顶层中间件)里用defer + recover捕获 panic,再交由e.HTTPErrorHandler处理 - 注意:
echo.New().HTTPErrorHandler是只读字段,不能直接赋值;必须在初始化后显式设置
怎么写一个带 panic 恢复的全局错误中间件?
核心是用 defer 在每个请求生命周期开始时注册恢复逻辑,把 panic 转成 error 后交由 Echo 的错误处理器统一响应。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
func RecoverMiddleware() echo.MiddlewareFunc {
return func(next echo.Handler) echo.Handler {
return echo.HandlerFunc(func(c echo.Context) error {
defer func() {
if r := recover(); r != nil {
// 构造标准 error,触发 HTTPErrorHandler
err := fmt.Errorf("panic recovered: %v", r)
c.Error(err)
}
}()
return next.ServeHTTP(c.Response(), c.Request())
})
}
}
- 必须放在
e.Use(RecoverMiddleware())中,且应是第一个中间件(越早越能覆盖 panic) -
c.Error()会触发e.HTTPErrorHandler,所以后续只需定制这个处理器即可 - 不要在
defer里直接调用c.JSON()或写响应体——这会绕过错误处理器,破坏统一性
如何定制 e.HTTPErrorHandler 实现结构化错误响应?
设置 e.HTTPErrorHandler 后,所有 c.Error() 和显式返回的 error 都会进这里,包括上面 recover 转来的 panic 错误。
e.HTTPErrorHandler = func(err error, c echo.Context) {
code := http.StatusInternalServerError
msg := "Internal Server Error"
// 区分业务错误与系统错误(比如自定义 error 类型)
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
msg = he.Message
} else if strings.Contains(err.Error(), "panic recovered") {
code = http.StatusInternalServerError
msg = "Service unavailable"
// 可选:记录完整 panic stack(需从 recover 获取原始 panic 值,见上一节增强版)
}
c.JSON(code, map[string]interface{}{
"code": code,
"message": msg,
"timestamp": time.Now().Unix(),
})
}
- 务必检查
err是否为*echo.HTTPError,否则会丢失c.String(404, "...")等调用的原始状态码 - panic 错误默认没有上下文信息,如需打印 stack,得在 recover 中用
debug.PrintStack()或runtime.Stack()捕获并存入 error(例如包装成fmt.Errorf("%w\n%v", err, stack)) - 不要在
HTTPErrorHandler里再调用c.Error(),会无限递归
为什么 panic 恢复后日志里看不到原始堆栈?
因为 recover() 只返回 panic 的 value(比如 "index out of range"),不带调用栈。Go 的 runtime 默认也不在 HTTP 错误响应中输出 stack,除非你主动抓取。
- 解决方法:在
defer的recover()分支里立即调用debug.PrintStack(),或用runtime.Stack(buf, false)获取字符串 - 建议把 stack 存进自定义 error 类型,比如
type PanicError struct { Msg string; Stack string },再让HTTPErrorHandler提取输出 - 生产环境慎用完整 stack 输出到客户端,至少做开关控制(如仅 dev 环境开启)
真正的难点不在加 recover,而在于 panic 发生时,echo.Context 可能已部分失效(比如响应头已写、body 已 flush),所以恢复逻辑必须足够轻量,避免二次 panic 或写冲突。

















