17370845950

如何在 Martini 中全局捕获 panic 并优雅恢复

martini 不支持直接用 `http.handler` 类型中间件,需改用 `martini.context` 和 `c.next()` 实现 panic 全局恢复;推荐优先使用内置的 `martini.recovery`,或自定义 `recoverwrap` 中间件并确保其注册顺序最靠前。

Martini 的中间件机制与标准 net/http 不同:它基于依赖注入和上下文链式调用(c.Next()),而非 HTTP handler 包装。因此,将 RecoverWrap 声明为 func(http.Handler) http.Handler 会导致编译失败——M

artini 的 injector 无法识别该签名,也无法将其注入到路由执行流程中。

正确做法是定义一个接收 martini.Context 和 http.ResponseWriter 的函数,并在 defer 中调用 recover(),最后显式调用 c.Next() 触发后续处理:

func RecoverWrap(c martini.Context, w http.ResponseWriter) {
    defer func(w http.ResponseWriter) {
        if r := recover(); r != nil {
            var err error
            switch v := r.(type) {
            case string:
                err = errors.New(v)
            case error:
                err = v
            default:
                err = errors.New("unknown panic value")
            }
            http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            // 可选:记录日志,例如 log.Printf("Panic recovered: %+v", err)
        }
    }(w)
    c.Next()
}

⚠️ 关键注意事项

  • 必须将 RecoverWrap 通过 m.Use(RecoverWrap) 最早注册(即在任何其他中间件或路由之前),否则在其之前 panic 的代码将无法被捕获;
  • c.Next() 是核心:它代表“继续执行后续中间件和最终路由处理器”,所有 panic 都应发生在 c.Next() 调用期间;
  • 不要尝试在 RecoverWrap 内部调用 h.ServeHTTP(...)——Martini 没有 http.Handler 入参,也不走标准 HTTP handler 链。

实际上,Martini 已内置成熟实现:martini.Recovery。它不仅捕获 panic,还支持可选的日志输出(默认启用)和自定义错误响应。推荐直接使用:

m := martini.Classic()
m.Use(martini.Recovery()) // ✅ 开箱即用,生产环境首选
m.Get("/", func() {
    panic("boom!")
})

如需定制错误响应(如返回 JSON 错误),可组合 martini.Recovery 与自定义 martini.Logger,或封装自己的 recovery 中间件——但务必继承其安全逻辑(如禁止暴露 panic 堆栈给客户端)。总之,避免重复造轮子;优先使用官方 Recovery,仅在有特殊需求时才扩展。