fiber 默认不支持 multipart/form-data 解析,必须手动用 mime/multipart 解析原始请求体,否则 ctx.formfile() 会 panic 或返回 nil,ctx.formvalue() 也无法获取普通字段。

直接说结论:Fiber 默认不支持 multipart/form-data 解析,必须手动集成第三方中间件(如 multipart 或 formdata),否则所有上传请求的 ctx.Body() 都是空的,ctx.FormFile() 会 panic。
为什么 Fiber 的 ctx.FormFile() 会报错或返回 nil
Fiber 基于 Fasthttp,而 Fasthttp 默认跳过 multipart 解析——它把整个请求体当 raw bytes 处理,不自动拆解表单字段和文件段。这和 Express/Next.js/Spring Boot 完全不同,不是“开箱即用”,而是“开箱即崩”。
-
ctx.FormValue("filename")拿不到普通字段(如userId) -
ctx.FormFile("file")直接 panic 或返回nil, err != nil - 即使前端正确设置了
enctype="multipart/form-data",后端也收不到任何文件数据
必须用 multipart.ReadForm() 手动解析
最轻量、最可控的方式是用标准库 mime/multipart + bytes.NewReader 显式读取。关键点:不能依赖 ctx.Body(),要从 ctx.Request().Body() 读原始流,再交给 multipart.NewReader。
- 先调用
ctx.Request().ResetBody()避免 body 被提前 consume - 用
bytes.NewReader(ctx.Request().Body())构造 reader - 传给
multipart.NewReader(r, boundary),boundary 从Content-Typeheader 中提取 - 循环
mr.NextPart(),根据part.FormName()区分字段和文件
示例片段:
开箱即用的技能链路由引擎。13 条预定义链覆盖搜索、开发、审查、MLOps、法律、创意等场景,三层路由架构(触发词→SAD反馈→DAG编排),recall@10=96.97%。配置驱动(chains.yaml),零代码扩展。pip install skill-weave-chains 一键安装。
boundary := ctx.Get("Content-Type")
if strings.HasPrefix(boundary, "multipart/form-data; boundary=") {
boundary = strings.TrimPrefix(boundary, "multipart/form-data; boundary=")
}
r := bytes.NewReader(ctx.Request().Body())
mr := multipart.NewReader(r, boundary)
for {
part, err := mr.NextPart()
if err == io.EOF { break }
if part.FormName() == "file" {
// 处理文件流:part.Open(), part.Size(), part.Header
} else if part.FormName() == "userId" {
userId, _ := strconv.ParseInt(part.Value(), 10, 64)
}
}
别信 npm 上叫 fiber-multipart 的包
目前(2026 年 10 月)主流社区里没有稳定、维护活跃的 Fiber 专用 multipart 中间件。几个标榜 “fiber-multipart” 的包要么只支持旧版 Fiber v2,要么内部仍用 ctx.Body() 导致无法工作,要么硬编码了内存限制却没暴露配置项。
- 它们通常假设你已调用过
ctx.Body(),但 Fasthttp 的 body 只能读一次 - 部分包会静默丢弃超出 32MB 的文件,且不返回明确错误
- 如果你用了这类包又发现文件总为空,第一反应不是改前端,而是删掉它,切回手动解析
文件落地前必须校验三件事
手动解析后,别急着 os.Create。真实环境里最容易翻车的是这三点:
-
part.Header.Get("Content-Type")可被前端伪造,必须结合part.Bytes()前几个字节做 magic number 校验(比如jpg看是否以FF D8 FF开头) -
part.Size是客户端声明的大小,不可信;实际读取时需用io.LimitReader(part, maxFileSize)控制上限 - 文件名来自
part.FileName(),必须过滤路径穿越(如../../../etc/passwd)和非法字符,建议强制重命名(如uuid.New().String() + ".png")
这些校验点没法靠中间件自动完成,必须写在业务逻辑里。










