不能靠c.multipartform一次性读完再校验,因其会提前消费全部r.body导致后续无法流式读取;必须用c.request.multipartreader()手动遍历part边读边校验总大小、类型与单文件限制。

不能靠 c.MultipartForm 一次性读完再校验——它会提前消费全部 r.Body,导致后续无法流式读取文件内容,上传直接失败。
为什么 c.FormFile 和 c.MultipartForm 会破坏多文件上传流程
Echo(以及 Gin、net/http)在调用 c.MultipartForm() 或 c.FormFile() 时,会隐式触发 r.ParseMultipartForm()。一旦触发,r.Body 就被完整读空,哪怕你只打算取一个文件,其余文件数据也已丢失。这对「上传多个文件 + 统一校验(如总大小、类型白名单、单文件尺寸)」是致命的。
常见错误现象:http: invalid Read on closed Body、EOF、前端显示“上传成功”但服务端只收到第一个文件。
- 必须在 handler 开头就禁用自动解析:
c.Request.ParseMultipartForm(0) - 不要调用
c.MultipartForm(),改用c.Request.MultipartReader()手动遍历 - 所有校验逻辑(总 size、扩展名、content-type)必须在流式读取过程中完成,不能等全部文件落地后再判断
手动遍历 multipart 并做统一校验的实操步骤
核心是用 multipart.NewReader() 拿到 reader,逐 part 解析,边读边统计、边校验、边丢弃或暂存。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
- 先调
c.Request.ParseMultipartForm(0)跳过框架默认解析 - 用
mr, err := c.Request.MultipartReader()获取原始 reader - 循环
mr.NextPart(),对每个 part 检查:part.FormName()是否为文件字段名(如"files")、part.Header.Get("Content-Type")是否在白名单内、part.Size是否超限 - 用
io.CopyN(ioutil.Discard, part, limit)控制单文件最大读取量,避免恶意大文件耗尽内存 - 累计已读总字节数,超过全局阈值(如 100MB)立即
http.Error(w, "total size exceeded", 413) - 校验通过后,才把 part 写入临时文件或对象存储
c.FormFile 能否用于多文件?什么情况下可以妥协使用
可以,但仅限于「单字段多文件」且「不校验总大小/不依赖文件内容哈希/不需并发控制」的轻量场景。例如前端用 <input type="file" name="files" multiple>,后端用:
for i := 0; ; i++ {
f, fh, err := c.FormFile(fmt.Sprintf("files[%d]", i))
if err == http.ErrMissingFile { break }
if err != nil { /* handle */ }
// 校验 fh.Size, fh.Header.Get("Content-Type") ...
}
⚠️ 注意:这本质仍是多次调用 ParseMultipartForm,性能差、不可控、不支持断点续传;若文件数多或体积大,极易触发内存 OOM 或超时。生产环境不推荐。
容易被忽略的边界点:文件名、路径、Content-Type 都不可信
前端传的 filename 在 part.FileName() 里,但它可能被篡改为 ../../etc/passwd 或空字符串;Content-Type 可被任意伪造(比如把木马改成 image/png)。真正的校验必须:
- 用
path.Base(part.FileName())提取干净文件名,拒绝含..或以.开头的 - 用
mimesniffer或filetype库读取前几百字节,比对 magic bytes 判断真实类型 - 扩展名白名单(如
[]string{".jpg", ".png", ".pdf"})要和真实类型双重校验,缺一不可 - 临时文件写入务必用
os.O_CREATE | os.O_EXCL,防止竞态覆盖
校验不是发生在“接收完之后”,而是发生在“读取每一字节的过程中”——这才是处理多文件上传真正可控的方式。










