应分层处理错误:业务错误显式返回对应状态码,panic恢复需在handler内手动defer-recover确保上下文,全局中间件仅兜底并透传原始状态码,ResponseWrapper必须检查c.Response().Written()。

fiber.App.Use() 不是统一错误格式的正确位置
直接在 app.Use() 里拦截所有请求并强行返回 {"code":xxx,"message":"..."},会把 404、401、500 这类有明确语义的 HTTP 状态码全部抹平。浏览器或调用方无法区分“资源不存在”和“业务校验失败”,前端路由、重试逻辑、监控告警都会失准。
真正该做的,是分层处理:
- 业务错误(如参数不合法、权限不足):主动调用
c.Status(400).JSON(Error(1002, "用户名已存在")) - 系统级 panic 或未捕获异常:用
app.Use()捕获,记录日志后返回固定兜底响应,但保留原始状态码(比如 panic 后仍返回 500) - JWT 校验失败等中间件错误:依赖
fiber-jwt自带行为,不要覆盖其401响应体
panic 恢复必须包裹在每个 handler 内部
Fiber 不像 Gin 那样默认 recover 所有 handler panic。如果你只靠 app.Use() 兜底,一旦某个 handler panic,它会跳过所有后续中间件(包括你写的日志、鉴权),直接落到 app.Use(),导致上下文信息丢失(比如 c.Path()、c.IP() 都拿不到)。
安全做法是:在关键 handler 里手动加 defer-recover:
app.Get("/user/:id", func(c *fiber.Ctx) error {
defer func() {
if r := recover(); r != nil {
log.Errorw("handler panic", "path", c.Path(), "err", r)
c.Status(500).JSON(fiber.Map{"code": 500, "message": "服务暂时不可用"})
}
}()
// 正常业务逻辑
id := c.Params("id")
user, err := db.FindUser(id)
if err != nil {
return c.Status(404).JSON(fiber.Map{"code": 404, "message": "用户不存在"})
}
c.Locals("response_data", user)
return c.Next()
})
注意:这里不是为了“吞掉 panic”,而是确保能拿到完整请求上下文再记录日志。
全局错误中间件只做日志+基础响应,不改状态码
这个 app.Use() 中间件只承担最后兜底职责,不能替代业务层的显式错误构造:
- 它只处理未被任何 handler 拦截的 panic 和未调用
c.SendStatus()就结束的异常路径 - 必须调用
c.Response().StatusCode()获取当前状态码,原样透传,不要硬写成 200 - 不要往
c.Locals里塞数据再走统一包装——此时 response body 可能已部分写出,c.JSON()会 panic
示例:
app.Use(func(c *fiber.Ctx) error {
err := c.Next()
if err != nil {
statusCode := c.Response().StatusCode()
if statusCode == 0 {
statusCode = 500
}
log.Errorw("global error handler", "status", statusCode, "err", err, "path", c.Path())
c.Status(statusCode).JSON(fiber.Map{
"code": statusCode,
"message": "服务暂时不可用",
})
}
return nil
})
ResponseWrapper 中间件必须检查 Response.Written()
你写的 ResponseWrapper 如果没判断 c.Response().Written(),会在重定向、文件下载、流式响应等场景下 panic —— 因为这些操作已经写了 header/body,再调 c.JSON() 会报 "body write after headers sent"。
正确写法:
func ResponseWrapper() fiber.Handler {
return func(c *fiber.Ctx) error {
if c.Response().StatusCode() == 200 && !c.Response().Written() {
data := c.Locals("response_data")
return c.JSON(200, Success(data))
}
return c.Next()
}
}
关键点:
-
!c.Response().Written()是硬性守门员,缺一不可 - 只对 200 成功路径生效,4xx/5xx 走各自 handler 的显式响应
- 别指望它能覆盖所有情况——上传、sse、chunked response 都得单独处理
最易被忽略的是:Fiber 的 c.Redirect()、c.SendFile()、c.Stream() 都会提前写 response,它们和 ResponseWrapper 天然互斥,必须绕开或前置注册。











