ASP.NET Core大文件下载应使用FileStreamResult配合可寻址流实现流式响应,避免OOM;断点续传需手动处理Range头并返回206状态码,PhysicalFile在Controller中不支持断点续传。

ASP.NET Core WebAPI怎么返回文件流
直接用 FileStreamResult 或 FileContentResult 是最常见做法,但它们会把整个文件加载进内存或一次性读取完再发出去,大文件容易 OOM。真正适合下载的,是让框架按需读取、边读边写——也就是流式响应。
核心是返回 FileStreamResult 并确保底层 Stream 是可寻址(CanSeek == true)且未被提前关闭的。如果用 new FileStream(...),记得加 FileOptions.Asynchronous 和 FileShare.Read,否则并发下载可能卡死或报错 IOException: The process cannot access the file。
- 不要用
MemoryStream包整个大文件,它吃内存 - 不要在
using块里创建FileStream后直接传给FileStreamResult,会导致流提前释放 - 推荐写法:
return new FileStreamResult(fileStream, "application/octet-stream") { FileDownloadName = "xxx.zip" };
如何支持断点续传(Range 请求)
断点续传不是靠客户端“记住位置”,而是服务端必须响应 HTTP 206 Partial Content,并正确处理 Range 请求头。ASP.NET Core 默认的 FileStreamResult 不支持这个,它只会返回 200 OK 和完整文件。
要支持,得自己解析 Range 头、计算偏移、设置 Content-Range 响应头,并返回对应字节段。关键点:文件必须是 CanSeek == true 的流;响应状态码不能硬写 200;还要显式设置 Accept-Ranges: bytes。
- 检查请求头:
Request.Headers.TryGetValue("Range", out var rangeHeader) - 解析范围:
RangeHeaderValue.Parse(rangeHeader)(需引用Microsoft.Net.Http.Headers) - 用
fileStream.Seek(startByte, SeekOrigin.Begin)定位,再用fileStream.Read(buffer, 0, count)读取指定长度 - 响应时设
Response.StatusCode = StatusCodes.Status206PartialContent,并写Content-Range头如bytes 1024-2047/1048576
为什么 PhysicalFile 不适合断点续传场景
PhysicalFile 看起来方便,但它内部用的是 StaticFileMiddleware 的逻辑,只在静态文件中间件管道中才启用 Range 支持。在普通 WebAPI Controller 里调用 PhysicalFile(path, mimeType),它会退化为全量 200 响应,不会识别 Range 头。
换句话说:想用 PhysicalFile 实现断点续传,就得把下载路径放到 wwwroot 下,并走静态文件中间件(比如配置 app.UseStaticFiles()),而不是走 Controller Action。否则就是假支持。
- Controller 里用
PhysicalFile→ 永远200,无断点 - 文件放在
wwwroot/downloads/xxx.bin,直接浏览器访问该 URL → 可断点(前提是没禁用StaticFileOptions.ServeUnknownFileTypes = true) - 需要权限控制?那就别用
PhysicalFile,老老实实手写 Range 处理逻辑
大文件下载时容易忽略的超时和缓冲问题
默认 Kestrel 超时是 2 分钟,IIS 还有额外限制。一个 2GB 文件按 1MB/s 下载要 30+ 分钟,不调就会中途断连。另外,默认响应缓冲区(HttpResponse.Body)可能触发同步写入,拖慢吞吐。
解决方式不是“加大缓冲区”,而是关掉缓冲、用异步流写入,并延长超时。Kestrel 配置里设 KeepAliveTimeout 和 RequestHeadersTimeout;代码里用 Response.BodyWriter 直接写入(.NET 6+),或确保 FileStream 的 ReadAsync + Response.Body.WriteAsync 成对使用。
- 别依赖
HttpContext.Response.StartAsync()手动启响应,现代 ASP.NET Core 已自动处理 - 避免在循环里反复
await Response.Body.WriteAsync(...)小块数据,合并成 8KB–64KB 更稳 - 若用 Nginx 反向代理,记得配
proxy_buffering off和足够大的proxy_read_timeout
断点续传真正的难点不在 Range 解析,而在于多客户端同时 seek 同一文件时的 I/O 竞争,以及文件被意外删除或重命名导致的 FileNotFoundException。这些没法靠框架自动兜底,得在打开文件前加存在性校验,必要时用 FileStream 的 FileShare.ReadWrite 和重试逻辑来扛。


















