gin 不支持断点续传,需手动解析 range 头:用 c.request.header.get("range") 获取并调用 http.parserange,校验结果长度为 1、start≥0、end≤文件大小,否则返回 416。

Gin 无法自动支持断点续传,必须手动解析 Range 头、校验范围、精确读取并返回 206 状态。 它不像 http.ServeContent 那样内置逻辑,所有头、seek、长度控制都得自己写死,漏一步客户端就直接重下或失败。
怎么手动解析和校验 Range 请求头
Go 标准库提供 http.ParseRange,但 Gin 完全不调用它。你得显式取头、解析、判空、再校验数值合法性:
- 用
c.Request.Header.Get("Range")拿原始字符串,传给http.ParseRange - 结果是
[]http.HTTPRange,长度必须为1;为空或长度 ≠ 1 就直接返回416 - 检查
start >= 0、end 、<code>start ,任一不满足也返回 <code>416 - 注意非法格式如
bytes=-1024或bytes=a-b会导致http.ParseRange返回空切片,必须提前判断
为什么不能用 file.Seek + io.Copy 直接发
io.Copy 不知道你要发多少字节,它会从当前 offset 一直读到 EOF —— 这完全违背 Range: bytes=1024-2047 的语义(只该发 1024 字节):
- 必须用
io.CopyN(w, file, length),其中length = end - start + 1 -
io.CopyN在文件提前结束时返回io.ErrUnexpectedEOF,这不是错误,而是正常行为;此时应按实际读取字节数设Content-Length - 绝对不要用
bufio.Reader包裹文件句柄,它的内部 buffer 会破坏Seek的位置精度,导致跳转错位、内容错乱
哪些 HTTP Header 必须显式设置
手写 handler 时没有默认头,漏任何一个都可能让浏览器放弃续传逻辑:
-
w.Header().Set("Accept-Ranges", "bytes")—— 没这行,客户端下次就不会发Range头 -
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size))—— 格式必须严格:不能多空格、不能少斜杠、不能用%v替代%d -
w.Header().Set("Content-Length", strconv.FormatInt(length, 10))—— 必须等于实际响应体字节数;Safari/iOS 对这个头极其敏感,不一致就直接拒载 -
w.WriteHeader(206)—— 必须在写 body 前调用;不能依赖中间件或c.Abort()后补发,否则状态码已发成200就覆写不了
最易被忽略的是 Content-Length 和实际响应体长度的一致性,以及 Accept-Ranges 头的缺失——这两点会让整个断点逻辑静默失效,连错误提示都没有。











