filestreamresult最稳妥,适合返回本地磁盘文件;httpclient下载远程资源必须分块流式处理,避免oom或超时。

用 FileStreamResult 返回本地文件最稳妥,HttpClient 下载远程资源必须分块流式处理,否则大文件直接 OOM 或超时。
ASP.NET Core 后端返回磁盘文件:用 FileStreamResult,别用 FileContentResult
小文件(几 MB 内)用 FileContentResult 没问题;但只要文件可能超过 5MB,就必须切到 FileStreamResult——它不把整个文件读进内存,而是边读边传,服务器压力低、支持基础断点续传。
- 路径必须真实存在且 Web 进程有读取权限,Linux 下常因
www-data或nginx用户无权访问报UnauthorizedAccessException -
Content-Disposition头必须设为attachment; filename="xxx.pdf",否则浏览器可能内嵌打开而非下载 - 别裸写
new FileStream(path, ...),要用leaveOpen: false或using确保流关闭,否则句柄泄漏 -
ContentType要准确,比如 PDF 是application/pdf,错设成text/plain会导致 Chrome 直接展示乱码二进制
示例:
public IActionResult Download(string fileName)
{
var filePath = Path.Combine(_env.WebRootPath, "uploads", fileName);
if (!System.IO.File.Exists(filePath)) return NotFound();
var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 8192, leaveOpen: false);
return new FileStreamResult(stream, "application/pdf") { FileDownloadName = fileName };
}
用 HttpClient 下载远程文件:禁用 ReadAsByteArrayAsync,必须流式分块
WebClient 已被标记为 obsolete,.NET 6+ 中它不支持取消、精细超时,实际项目里卡死、无法响应用户取消,基本都源于还在用它。
- 必须调
response.EnsureSuccessStatusCode(),否则 404/500 会静默返回空流,结果写了个 0 字节文件 - 绝不能用
ReadAsByteArrayAsync()——100MB 文件就吃掉 100MB 内存,容易触发OutOfMemoryException -
cancellationToken要传给GetAsync、ReadAsStreamAsync和CopyToAsync,漏一个,“取消”就失效 - 缓冲区大小建议
8192(8KB),太小 IO 频繁,太大延迟高且浪费内存
示例(带进度):
var buffer = new byte[8192];
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
await using var fileStream = File.Create(savePath);
long totalBytes = 0;
long? totalSize = response.Content.Headers.ContentLength;
while (true)
{
int bytesRead = await stream.ReadAsync(buffer, cancellationToken);
if (bytesRead == 0) break;
await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);
totalBytes += bytesRead;
progress?.Report(totalSize.HasValue ? (double)totalBytes / totalSize.Value : 0);
}
前端触发下载失败?别用 fetch 或 axios 直接拿响应体
浏览器限制:JavaScript 无法通过 fetch 拿到二进制流后自动触发保存对话框。Vue/React 里调接口没反应,基本都是这个原因。
- 正确做法是生成一个带签名的临时 URL,然后用
<a href="..." download></a>或location.href = url - 如果必须走 API(比如要带 token),就用隐藏
<form></form>提交:form.submit(),服务端返回FileStreamResult即可 - 跨域场景下,
Content-Disposition: attachment仍有效,但前端不能用 Blob + URL.createObjectURL() —— Safari 和部分 Chrome 版本会静默失败
真正麻烦的从来不是“怎么写”,而是路径权限、流生命周期管理、前端下载链路是否闭环——这三个点漏一个,线上就表现为“点不动”“下空文件”“大文件超时”。










