统一响应中间件必须绕过BodyLimit等预读body中间件,否则c.Body()返回空;应为路由组显式挂载而非全局use,且响应包装逻辑须前置到写入前而非读取已发送响应。

统一响应中间件必须绕过 BodyLimit 和其他预读 body 的中间件
Fiber 默认对所有路由启用 BodyLimit、RequestID 等中间件,而它们会在 handler 执行前就调用 c.Body() 或 c.FormValue(),导致原始请求体被消费。一旦你后续在统一响应中间件里想解析 JSON/XML 或做鉴权,c.Body() 就会返回空或错误数据。
解决方法只有两个:
- 在注册统一响应中间件时,**不走全局
app.Use()链**,而是为特定路由组显式挂载:api := app.Group("/api"); api.Use(responseMiddleware) - 若必须全局启用,需确保它排在
BodyLimit之前(但不推荐——因为BodyLimit本身就需要读 body,顺序冲突)
响应中间件里别直接改 c.Response().Body() 字节流
很多人试图在 afterHandler 类型的中间件中,用 c.Response().Body() 拿到已写入的响应字节,再解码、包装、重写回去。这不可靠:Fiber 的 Response 内部使用 fasthttp 的缓冲池,Body() 返回的是只读快照,且可能已被压缩、分块或流式发送。
正确做法是:把统一格式逻辑前置到 handler 执行后、实际写响应前。典型结构如下:
func responseMiddleware(c *fiber.Ctx) error {
// 1. 先执行下游 handler
err := c.Next()
// 2. 检查是否是 API 响应(避免干扰静态文件、404 页面等)
if !strings.HasPrefix(c.GetRespHeaders()["Content-Type"][0], "application/json") {
return err
}
// 3. 获取原始响应数据(仅当 handler 返回了 JSON 类型数据)
if c.Response().StatusCode() >= 200 && c.Response().StatusCode() < 300 {
// 假设业务 handler 已调用 c.JSON(...),此时数据在缓冲区
// 更稳妥方式:让 handler 返回 struct,由中间件序列化
}
return err
}
更推荐的做法是:**不依赖已写响应,而是约定 handler 返回值类型**,比如 map[string]interface{} 或自定义 ResponseData 结构,由中间件统一 c.JSON() 输出。
handler 返回值需分层处理:success / error / raw
统一中间件不能一刀切地把所有返回都包进 {"code":0,"data":...,"msg":"ok"}。真实场景中你需要区分三种情况:
- 业务成功数据(如
return map[string]interface{}{"id": 123})→ 自动包成标准 success 结构 - 显式错误(如
return c.Status(400).JSON(map[string]string{"error": "missing field"}))→ 不重复包装,原样透传 - 原始响应对象(如
return c.SendString("plain text")或c.File(...))→ 跳过包装,防止破坏内容类型
判断依据只能是:c.Response().StatusCode() + c.Response().Header.ContentType() + 是否已写入 body。不要依赖返回值类型做反射判断——Fiber handler 签名固定为 error,返回值本身不可见。
嵌套中间件时 locals 传递容易失效
如果你在统一响应中间件里用了 c.Locals("request_id", ...),又在另一个日志中间件里读取它,要注意:Fiber 的 Locals 是请求上下文绑定的,但**只在同一个中间件链内有效**。如果某个中间件提前 return c.Next() 或 c.SendStatus(401),后续中间件根本不会执行,Locals 就没机会写入或读取。
真正安全的共享方式只有两种:
- 用
c.Context().Value()(标准 context.Value,跨中间件稳定) - 在最外层中间件注入
ctx.Value,后续所有中间件通过c.Context().Value(key)读取
别依赖 c.Locals 做跨中间件状态传递,尤其在有短路逻辑(如鉴权失败直接返回)的场景下,它几乎必然为空。


















