Context.File() 不设 Content-Disposition,需手动设置 filename*(UTF-8 编码)并配 application/octet-stream 类型;大文件应优先用 Context.Stream() 流式传输,配合超时调优与正确前端 blob 下载方案。

Context.File() 直接返回文件但不设 Content-Disposition
用 Context.File() 可以快速把本地文件发给客户端,但它默认只设置 Content-Type 和状态码 200,**不会加 Content-Disposition: attachment**,浏览器就直接在页内打开(比如 PDF、图片),而不是弹出下载对话框。
常见错误现象:调用 c.File("report.pdf") 后页面显示 PDF 内容,用户得手动「另存为」——这不是真正意义上的附件下载。
- 必须手动设置
Content-Disposition响应头,且值要带filename=参数(注意编码) -
Content-Type建议显式设为application/octet-stream,避免浏览器按内容猜测类型导致意外行为 - 如果文件名含中文,
filename*参数要用 UTF-8 编码 +utf-8''前缀(RFC 5987),否则部分浏览器(如 Chrome)可能乱码或截断
Context.Stream() + 自定义 Header 实现可靠下载
更可控的方式是用 Context.Stream() 手动读取文件并写入响应体,同时精确控制所有 header。这能避开 File() 的隐式行为,也便于加日志、限速或动态生成文件流。
使用场景:需要强制下载、文件名动态拼接、或文件来自非本地路径(如 S3 流、加密解密后的内容)。
- 先调用
c.Response().Header().Set("Content-Disposition", "attachment; filename*=UTF-8''" + url.PathEscape(filename)) - 再设
c.Response().Header().Set("Content-Type", "application/octet-stream") - 用
os.Open()打开文件,传给c.Stream()的函数参数;注意关闭文件句柄(defer f.Close()) - 返回值需是
error,且不能忽略 —— 若流写入中途出错(如客户端断连),Stream()会返回非 nil error
func downloadHandler(c echo.Context) error {
filename := "订单汇总_202405.xlsx"
f, err := os.Open("./data/" + filename)
if err != nil {
return c.String(http.StatusNotFound, "file not found")
}
defer f.Close()
<pre class="brush:php;toolbar:false;">c.Response().Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape(filename))
c.Response().Header().Set("Content-Type", "application/octet-stream")
return c.Stream(http.StatusOK, "application/octet-stream", f)
}
大文件下载要注意 ResponseWriter 超时和内存
用 File() 或 Stream() 下载几百 MB 以上文件时,若没做处理,容易触发 Echo 默认的 30 秒超时,或因一次性读入内存导致 OOM。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
性能影响关键点:
- Echo 默认
Server.WriteTimeout是 30 秒,大文件需延长,例如e.Server.WriteTimeout = 300 * time.Second -
Stream()是边读边写,只要文件句柄是*os.File就不会全加载进内存;但若你用bytes.NewReader(data)包装整个文件内容,就失去流式优势 - 不要在 handler 里用
ioutil.ReadFile()或os.ReadFile()读大文件再传给String()或JSON()—— 这是典型内存陷阱
前端如何触发下载而不跳转/刷新页面
后端设对了 header,前端还得用正确方式请求,否则仍可能被当普通导航处理。
容易踩的坑:
- 直接
window.location.href = "/download":Chrome/Firefox 会尝试复用当前 tab,导致页面空白或报错(尤其跨域时) - 用
@#@#@#@#@#@#@#@#@#@0:仅对同源 URL 有效,且download属性在某些版本 Safari 不支持 - 推荐方案:用
fetch()拿到 blob,再用URL.createObjectURL()创建临时链接并模拟点击
fetch('/api/download')
.then(res => res.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'report.pdf';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
注意:blob 方式无法自定义保存路径,这是浏览器安全限制;文件名最终是否生效,还取决于后端 Content-Disposition 中的 filename* 是否被正确解析。
最易被忽略的是:后端设置了 filename*,但前端 fetch 时没加 { credentials: 'include' }(如果接口需要 Cookie 鉴权),导致 401;或者 Nginx 反代时清掉了 Content-Disposition 头 —— 这类问题得查响应原始 header,不能只信浏览器开发者工具的「Headers」标签页显示。









