gin中间件中不能调c.formfile或c.multipartform,因其隐式触发r.parsemultipartform导致r.body被读空,使分片二进制数据丢失;必须在开头调用r.parsemultipartform(0)禁用解析,并手动流式处理raw body。

为什么 Gin 中间件里不能调 c.FormFile 或 c.MultipartForm
因为这些方法会隐式触发 r.ParseMultipartForm,而该操作会一次性读空 r.Body——分片上传的原始二进制数据就没了。后续无论你用 io.Copy 还是 io.ReadAt 都只能得到 EOF。这不是 bug,是 multipart 解析的固有行为:它把整个请求体当表单解析,不区分“文件字段”和“分片流”。
常见错误现象包括:
- 分片写入为空文件,大小始终为 0
-
io.CopyN突然返回n=0, err=EOF - 同一请求中先调
c.FormFile再读 body,后者无数据
所以必须在中间件或 handler 开头就执行:c.Request.ParseMultipartForm(0),传 0 表示跳过解析。注意:这个调用本身不消费 body,只是禁用后续自动解析逻辑。
如何在中间件中安全接管 raw body 分片流
分片请求的 Content-Type 应为 application/octet-stream,不是 multipart/form-data。Gin 默认不处理这种 raw body,需手动接管并加防护。
- 限流防攻击:用
http.MaxBytesReader包裹c.Request.Body,例如限制单片 ≤5MB —— 必须在ParseMultipartForm(0)之前设置 - 精确读取:用
io.LimitReader(c.Request.Body, expectedSize),避免因网络抖动多读或少读 - 并发写保护:临时文件路径要带唯一标识(如
upload_id/chunk_index),打开时加os.O_EXCL标志防止覆盖 - 元信息来源:禁止从 form 获取,改用
c.Request.URL.Query().Get("upload_id")或c.Request.Header.Get("X-Upload-ID")
中间件注册顺序对分片上传的关键影响
如果你在全局注册了日志、鉴权等中间件,它们可能无意中触发了 ParseMultipartForm —— 比如某些日志中间件会调 c.PostForm 打印表单内容,或鉴权中间件读 c.Request.Body 做签名验证。
- 分片上传路由必须使用独立路由组,例如
api := r.Group("/upload") - 只在该组上注册必要中间件,且确保第一个是「禁用 multipart 解析 + 设置 MaxBytesReader」的专用中间件
- 避免使用
gin-contrib/multiform等第三方中间件,它们内部常调用ParseMultipartForm - 若必须复用通用中间件,检查其源码是否访问
c.Request.FormValue或c.Request.MultipartForm
分片合并与校验不能放在中间件里做
中间件职责是预处理请求、统一拦截和上下文增强,不是业务逻辑执行点。分片接收完成后,应由 handler 触发合并、MD5 校验、OSS 上传等动作。
- 中间件只负责:限流、解析 URL 参数、流式写入临时分片、记录分片接收状态
- 不要在中间件里调
os.Open读所有分片再合并 —— 这会阻塞整个中间件链 - 合并失败应返回明确错误(如
400 Bad Request+"missing_chunk: 3"),而不是在中间件里 panic 或重定向 - 若需异步合并,用
go func() { ... }()启动协程,但注意c.Copy()复制上下文,避免 data race
最易被忽略的一点:分片路径设计必须支持原子性。比如用 /tmp/uploads/{upload_id}/{chunk_index}.bin 而不是 /tmp/uploads/{upload_id}/chunks 目录,前者可通过 os.Rename 实现合并后原子替换,后者在并发写时容易出现目录竞争。











