HTTP响应头需设Content-Disposition为attachment模式并正确编码中文文件名,配合适当Content-Type;大文件须流式传输并校验路径、处理Range请求;实际路由中须补全路径净化、错误检查、超时控制等安全细节。
HTTP响应头必须设置Content-Disposition
不设这个头,浏览器会尝试渲染文件(比如PDF、JSON),而不是下载。关键是要用attachment模式,并正确编码中文文件名:
-
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")—— 仅适用于ASCII文件名 - 中文名需用
filename*=UTF-8''格式,例如:w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape(filename)) - 别漏掉
Content-Type,用http.DetectContentType或根据扩展名查表更稳妥,避免硬写"application/octet-stream"导致移动端无法识别
文件读取不能直接 ioutil.ReadFile
大文件(>10MB)用ioutil.ReadFile会吃光内存,应流式传输:
- 用
os.Open打开文件,再调用io.Copy(w, f),让内核负责缓冲 - 务必在
defer f.Close()前检查os.Open错误,否则空指针panic - 如果文件路径来自用户输入(如
r.URL.Query().Get("file")),必须做路径净化,防止../../../etc/passwd越界访问
支持断点续传要处理Range请求
浏览器下载中断后重试时会发Range: bytes=1000-,服务端不响应会导致重下整个文件:
- 检查
r.Header.Get("Range")是否非空 - 解析出
start偏移,用f.Seek(start, io.SeekStart) - 设置
206 Partial Content状态码和Content-Range头,例如:bytes 1000-9999/10000 - 注意:
Content-Length此时应为剩余字节数,不是整个文件大小
实际路由示例中容易忽略的细节
以下代码片段看似能跑,但上线后可能出问题: