本文介绍使用 go 构建下载服务时,如何安全、可靠地将用户下载速度限制为 100kb/s,重点解决手动设置响应头导致的“corrupted content error”问题,并推荐基于 `http.servecontent` + 限速 reader 的标准实践方案。
在 Go 中实现带速率限制的文件下载,直接操作 http.ResponseWriter 并手动设置 Content-Length 等头部是高危做法——尤其是当配合第三方限速 Reader(如 golang.org/x/time/rate 或 github.com/juju/ratelimit)使用时,极易因响应体长度与声明不一致、缺少范围请求(Range)支持或未处理客户端断连,触发浏览器“Corrupted Content Error”。
✅ 正确解法:将限速逻辑下沉至读取层,交由 http.ServeContent 统一管理响应。该函数自动:
- 设置正确的 Content-Type、Content-Length 和 Last-Modified
- 支持 If-Modified-Since 缓存协商
- 完整处理 Range 请求(断点续传)
- 兼容 ETag 验证(需手动设置 w.Header().Set(“ETag”, …))
以下是一个生产就绪的限速下载示例:
package main
import (
"io"
"net/http"
"os"
"path/filepath"
"time"
"github.com/juju/ratelimit"
)
// limitedReadSeeker 包装 io.ReadSeeker,对 Read 操作施加速率限制
type limitedReadSeeker struct {
io.ReadSeeker
limiter io.Reader
}
func (lrs *limitedReadSeeker) Read(p []byte) (int, error) {
return lrs.limiter.Read(p)
}
// newLimitedReadSeeker 创建限速的 ReadSeeker
func newLimitedReadSeeker(rs io.ReadSeeker, bucket *ratelimit.Bucket) io.ReadSeeker {
return &limitedReadSeeker{
ReadSeeker: rs,
limiter: ratelimit.Reader(rs, bucket),
}
}
func serveFile(w http.ResponseWriter, r *http.Request) {
fileID := r.URL.Query().Get("fileID")
if fileID == "" {
http.Error(w, "missing fileID", http.StatusBadRequest)
return
}
// ✅ 安全解析文件路径(务必防御路径遍历!)
path := filepath.Join("../../bin/files", fileID+".txt")
if !isSafePath(path) { // 实现见下方说明
http.Error(w, "invalid file path", http.StatusForbidden)
return
}
file, err := os.Open(path)
if err != nil {
http.NotFound(w, r)
return
}
defer file.Close()
fi, err := file.Stat()
if err != nil {
http.Error(w, "failed to stat file", http.StatusInternalServerError)
return
}
// ⚙️ 限速配置:100KB/s = 102400 bytes/sec
const rate = 100 * 1024
bucket := ratelimit.NewBucketWithRate(float64(rate), int64(rate))
// ? 将原始文件包装为限速的 ReadSeeker
limitedReader := newLimitedReadSeeker(file, bucket)
// ? 交由 ServeContent 全权处理 HTTP 响应(含 Range、缓存、头信息)
http.ServeContent(w, r, filepath.Base(path), fi.ModTime(), limitedReader)
}
// isSafePath 是基础路径安全检查(生产环境建议用更严格的白名单或沙箱)
func isSafePath(p string) bool {
abs, err := filepath.Abs(p)
if err != nil {
return false
}
root, _ := filepath.Abs("../../bin/files")
return strings.HasPrefix(abs, root)
}
func main() {
http.HandleFunc("/download", serveFile)
http.ListenAndServe(":8080", nil)
}
? 关键注意事项: