Content-Disposition 头未生效主因是响应头被覆盖或设置不当;应使用 FileDownloadName 属性而非手动拼接,它自动处理 UTF-8 编码与 RFC 5987 兼容;中文名无需 UrlEncode;需配准 MIME 类型并设 X-Content-Type-Options: nosniff 防嗅探;Stream 必须可定位且未释放。

Content-Disposition 头没生效?检查响应头是否被覆盖
浏览器下载行为不按预期走,大概率是 Content-Disposition 没真正发出去。ASP.NET Core 中常见陷阱是:在 FileStreamResult 或 FileContentResult 之后又手动调用 Response.Headers.Add(),结果被框架内部逻辑覆盖——因为这些 Result 类型会在 ExecuteResultAsync 里自己写一次 Content-Disposition,你后写的反而丢了。
正确做法是直接构造 FileContentResult 并设置其 FileDownloadName 属性:
return new FileContentResult(fileBytes, "application/pdf")
{
FileDownloadName = "报告_2024.pdf"
};- 这个属性会自动注入标准的
attachment; filename="..."(含 UTF-8 编码处理) - 不要手动拼
Content-Disposition字符串,尤其别用filename*=手动编码,.NET 已内置 RFC 5987 兼容 - 如果用了
HttpResponse.TransmitFile()(旧版 WebForms 或 .NET Framework),必须自己设Response.AddHeader("Content-Disposition", ...),且得在TransmitFile前调用
中文文件名乱码?别碰 filename=,用 FileDownloadName 就行
老代码常把中文名用 HttpUtility.UrlEncode 后塞进 filename="...",结果 Chrome 显示成 %E6%8A%A5%E5%91%8A.pdf。这是误用了过时的兼容写法——现代浏览器(Chrome/Firefox/Edge 80+)只认 filename*=UTF-8''... 格式,而手动拼容易漏单引号、空格或编码错误。
FileDownloadName 在 ASP.NET Core 2.1+ 中已自动处理所有细节:
var fileName = "用户数据-张三.xlsx"; return File(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName);
- 底层会生成
Content-Disposition: attachment; filename="user-data-zhangsan.xlsx"; filename*=UTF-8''%E7%94%A8%E6%88%B7%E6%95%B0%E6%8D%AE-%E5%BC%A0%E4%B8%89.xlsx - IE11 不支持
filename*,但会退回到filename的 ASCII 版本(即空字符串或默认名),所以务必保证fileName参数本身是安全 ASCII 名作为 fallback - 避免在文件名中使用
/ \ ? * : | " < >,Windows 系统下保存会失败,浏览器可能静默截断
想强制下载而不是预览 PDF/图片?Content-Type 和 Content-Disposition 都得对
即使设置了 Content-Disposition: attachment,如果 Content-Type 是 text/plain 或 application/octet-stream,Chrome 仍可能根据文件头 sniff 出 PDF 并内嵌预览。这不是 bug,是浏览器主动优化。
解决方法很直接:
- 用准确的 MIME 类型:
application/pdf、image/png、application/vnd.ms-excel,而不是笼统的application/octet-stream - 确保
Content-Disposition是attachment(不是inline),且FileDownloadName非空 - 如果仍被预览,加一个不干扰的响应头压制 sniff:
Response.Headers["X-Content-Type-Options"] = "nosniff"
注意:nosniff 在 IE 和旧 Edge 上有效,但现代 Chromium 已默认严格按 Content-Type 执行,加它主要是保险。
Stream 未关闭或未 Seek 到开头?下载卡住或文件损坏
返回 FileStreamResult 时传入的 Stream 如果之前被读过(比如为了计算 MD5),位置不在开头,浏览器收到的就是截断内容;更糟的是,如果 Stream 被提前 Dispose,.NET 会抛 ObjectDisposedException,但 HTTP 响应可能已部分发出,前端表现为“下载完成但打不开”。
- 传给
FileStreamResult前,务必调用stream.Position = 0(仅限可 seek 的 stream) - 如果是
MemoryStream,确认没调过stream.Dispose();若用using包裹了 stream,就别传给 Result —— 改用FileContentResult+ToArray() - 大文件慎用
MemoryStream,改用FileStreamResult直接传磁盘路径,避免内存暴涨
最稳妥的流处理:用 FileStream 构造时加 FileAccess.Read 和 FileShare.Read,并确保 Controller 方法签名是 async Task<ActionResult>,让框架能异步传输。










