c.fileattachment() 专用于强制下载 pdf,自动设置 content-type 和 content-disposition;c.file() 则默认内联显示。需确保文件扩展名小写为 .pdf,路径安全且存在,大文件应流式传输。

c.File() 和 c.FileAttachment() 都能响应 PDF,但行为完全不同
浏览器对 PDF 的处理很特殊:默认会内联显示(在标签页中打开),而不是弹出下载框。如果你只是想让用户看 PDF,c.File() 足够;但若目标是“下载 PDF 文件”,必须用 c.FileAttachment() 或手动设置 Content-Disposition 头。
常见错误现象:
- 调用
c.File("./docs/report.pdf")后,PDF 在浏览器里直接打开,用户找不到“另存为”入口 - 用
c.Header("Content-Disposition", "attachment; filename=report.pdf")却忘了调用c.DataFromReader或c.File,结果返回空响应或 500
实操建议:
- 优先使用
c.FileAttachment("./docs/report.pdf", "年度报告-2026.pdf")—— 它自动设好Content-Type: application/pdf和Content-Disposition: attachment,且第二个参数支持 UTF-8 中文名(Gin v1.9+) - 路径必须可读且存在,Gin 不会帮你创建父目录;建议加一层
os.Stat()检查,避免stat ./docs/report.pdf: no such file or directory - 不要依赖相对路径如
./docs/,部署时工作目录可能不是项目根目录;改用filepath.Join(os.Getenv("APP_ROOT"), "docs", "report.pdf")或硬编码绝对路径
为什么 Content-Type 有时是 text/plain?
Gin 的 c.File() 和 c.FileAttachment() 内部调用 http.ServeContent,它依赖 mime.TypeByExtension() 推断类型。如果文件后缀不是 .pdf(比如叫 report 或 report.PDF),就可能 fallback 到 text/plain,导致浏览器拒绝渲染。
实操建议:
- 确保文件扩展名小写且为
.pdf;Gin 不做大小写归一化 - 不想依赖后缀?用
c.Data手动指定:data, _ := os.ReadFile(filepath) c.Data(http.StatusOK, "application/pdf", data)
(适合小文件;大文件别这么干,会吃光内存) - 生成动态 PDF(如用 gofpdf)时,直接用
c.DataFromReader,绕过文件系统:c.DataFromReader(http.StatusOK, size, "application/pdf", reader, nil)
中文文件名在 Chrome/Firefox 下乱码?
直接传中文名给 c.FileAttachment() 在 Gin v1.9.1+ 是安全的,它内部做了 RFC 5987 编码。但老版本(如 v1.8.x)或某些代理(Nginx 默认配置)会截断或破坏 header。
实操建议:
- 升级 Gin 至
v1.9.1+,然后放心传"财报-2026年Q2.pdf" - 若无法升级,降级方案:用 ASCII 文件名(如
"report-2026-q2.pdf"),并在响应体里附带说明文本 - Nginx 前置时,确认没开启
underscores_in_headers on(它会导致 header 解析失败)
大 PDF(>50MB)下载卡死或超时?
默认的 c.FileAttachment() 是同步读取整个文件进内存再写入 response writer,遇到大文件容易 OOM 或触发 HTTP 超时(尤其是用 Nginx 时,默认 proxy_read_timeout 是 60 秒)。
实操建议:
- 改用
c.Stream+io.Copy流式传输:f, _ := os.Open(filepath) defer f.Close() c.Stream(func(w io.Writer) bool { io.Copy(w, f) return false }) - 务必在
Stream前手动设置Content-Length和Content-Disposition,否则浏览器无法显示进度条 - 后端服务和反向代理(如 Nginx)都要调大超时时间:
proxy_read_timeout 300、read_timeout 300s
最易被忽略的一点:无论用哪种方式,都得确认文件路径不包含用户输入拼接——c.Param("filename") 直接拼进 filepath.Join() 是典型的路径遍历漏洞。宁可白名单校验,也不要信任任何客户端传来的文件名。











