http 请求体只能读取一次,因其底层为单向 io.readcloser 流,读完即达 eof,无法重置;直接两次 r.body.read() 会导致第二次返回 0 字节或空数据,引发 md5 校验失败、multipart 解析异常等问题。

为什么直接用 http.Request.Body 读两次会失败
文件秒传依赖客户端上传前计算的 MD5,服务端需比对——但很多人一上来就 r.Body.Read() 两次,发现第二次读不到数据。这是因为 http.Request.Body 是单向流,底层是 io.ReadCloser,读完即 EOF,不会自动重置。
常见错误现象:md5.Sum(nil) 结果全零、io.Copy 返回 0 字节、后续解析 multipart 表单失败。
- 必须在读取 body 前决定:是先校验 MD5,还是先解析表单字段(如文件名、用户 ID)
- 若需同时处理表单字段和文件内容,不能靠反复读
r.Body,得用io.TeeReader或缓存到内存/临时文件 - 大文件场景下,内存缓存风险高;小文件(bytes.Buffer 中转
如何在 Echo 中安全提取并校验客户端传来的 MD5
客户端通常把 MD5 放在 header(如 X-File-MD5)或 form field(如 file_md5)里。Echo 不会自动解析 header 中的哈希值,得手动取;若走 form,则需先调用 c.MultipartForm() 或 c.FormValue(),但注意:这会触发 ParseMultipartForm,可能提前消费 body。
推荐做法是统一从 header 读 MD5,避免与 form 解析耦合:
// 示例:从 header 提取客户端 MD5
clientMD5 := c.Request().Header.Get("X-File-MD5")
if clientMD5 == "" {
return echo.NewHTTPError(http.StatusBadRequest, "missing X-File-MD5 header")
}
// 校验格式(32位十六进制)
if len(clientMD5) != 32 || !regexp.MustCompile("^[a-fA-F0-9]{32}$").MatchString(clientMD5) {
return echo.NewHTTPError(http.StatusBadRequest, "invalid MD5 format")
}
- 不要信任前端传的任何字段,header 同样需校验格式,防止空字符串或非法字符干扰后续逻辑
- 如果必须从 form 取(比如老协议),务必在
c.MultipartForm()前用io.TeeReader把 body 写入 hasher,否则 form 解析后 body 已空 - 注意 Echo 的
c.File/c.FormFile会隐式调用ParseMultipartForm,触发 body 消费
用 io.TeeReader 边读边算 MD5,不额外拷贝内存
核心技巧:把原始 http.Request.Body 包一层 io.TeeReader,让它在每次读取时同步写入 hash.Hash,这样后续业务逻辑(如保存文件、解析 form)仍可用原 body 流,且 MD5 已算完。
// 创建 hasher
h := md5.New()
// 构造 tee reader:读 r.Body 的同时写入 h
tr := io.TeeReader(c.Request().Body, h)
// 替换 body,后续所有读操作都经由 tr
c.Request().Body = ioutil.NopCloser(tr)
// 此时可放心调用 c.FormFile() 或 c.MultipartForm()
file, err := c.FormFile("file")
if err != nil {
return err
}
// 打开文件流(实际读的是 tee reader,自动更新 hash)
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
// 用 io.Copy 完成最终读取(此时 h 已完成计算)
io.Copy(ioutil.Discard, src)
// 获取结果
actualMD5 := hex.EncodeToString(h.Sum(nil))
-
io.TeeReader是零拷贝关键,它不缓存数据,只做“分发”,性能无损 - 必须用
ioutil.NopCloser(tr)封装,因为http.Request.Body要求是io.ReadCloser - 别在
TeeReader后再手动h.Write(),会导致重复哈希 - 如果用
multipart.Reader手动解析(比如多文件场景),同样要基于tr构造,而非原始 body
秒传判定后如何跳过文件存储
MD5 匹配成功,就该直接返回已有文件 URL,不再写磁盘。但要注意:Echo 的 c.FormFile() 已打开文件句柄,即使你不读它,也得显式 Close(),否则可能泄漏 fd。
典型漏点:只判断 MD5 相等就 return c.JSON(...),忘了关掉 file.Open() 返回的 src。
- 秒传响应应包含文件元信息(如 size、url、upload_time),这些需查数据库或 Redis,不能只靠 MD5
- 若用本地路径做秒传 key,注意不同客户端可能用不同编码或大小写,建议统一转小写+标准化路径
- 并发上传同一文件时,MD5 校验成功但 DB 还没写入元数据,可能造成“查不到记录”假阴性——加一层缓存(如
sync.Map存正在入库的 MD5)可缓解
真正难的不是算 MD5,而是确保整个请求生命周期里 body 只被读一次、hash 和业务逻辑不打架、以及秒传命中时资源清理干净。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











