.NET 7 IFormFile大文件上传异常求助:Postman报错、客户端无响应
.NET 7大文件上传问题排查与修复
问题现象
- 上传30MB文件时失败;
- 使用Postman每隔2秒重复发送7MB文件请求,抛出
System.IO.InvalidDataException: Multipart body length limit 16384 exceeded异常; - C#客户端上传文件时请求无响应,服务器端无任何日志或反馈。
需求目标
实现单次请求内的文件上传功能,支持KB到GB级文件,需包含以下参数:
- Id: string
- Document: IFormFile
- Attachments: IList
当前实现代码
Program.cs
builder.Services.Configure<FormOptions>(options => { options.MultipartBodyLengthLimit = 50 * 1024 * 1024; options.BufferBodyLengthLimit = 50 * 1024 * 1024; });
上传控制器
[Route("api/[controller]")] [ApiController] public class UploadController : ControllerBase { [HttpPost] [DisableFormValueModelBinding] [RequestSizeLimit(50 * 1024 * 1024)] public async Task<IActionResult> Post() { var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType), int.MaxValue); var reader = new MultipartReader(boundary, HttpContext.Request.Body); var section = await reader.ReadNextSectionAsync(); while (section != null) { section = await reader.ReadNextSectionAsync(); } return Ok(); } }
DisableFormValueModelBinding特性
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter { public void OnResourceExecuting(ResourceExecutingContext context) { var factories = context.ValueProviderFactories; factories.RemoveType<FormValueProviderFactory>(); factories.RemoveType<FormFileValueProviderFactory>(); factories.RemoveType<JQueryFormValueProviderFactory>(); } public void OnResourceExecuted(ResourceExecutedContext context) { // Empty since factories don't need to be added again (P.W. 2021-07-07 - SonarCloud issue fix) } }
MultipartRequestHelper工具类
public static class MultipartRequestHelper { // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq" // The spec says 70 characters is a reasonable limit. public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit) { var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value; if (string.IsNullOrWhiteSpace(boundary)) throw new InvalidDataException("Missing content-type boundary."); if (boundary.Length > lengthLimit) throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded."); return boundary; } public static bool IsMultipartContentType(string contentType) { return !string.IsNullOrEmpty(contentType) && contentType.Contains("multipart/", StringComparison.OrdinalIgnoreCase); } public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition) { // Use if you stored data objects in the body // Content-Disposition: form-data; name="key"; return contentDisposition != null && contentDisposition.DispositionType.Equals("form-data") && string.IsNullOrEmpty(contentDisposition.FileName.Value) && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value); } public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition) { // Use if you stored file data in the body // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg" return contentDisposition != null && contentDisposition.DispositionType.Equals("form-data") && (!string.IsNullOrEmpty(contentDisposition.FileName.Value) || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value)); } }
C#客户端代码
Stream _fileStream = File.OpenRead(... file ...); var httpClient = new HttpClient(); httpClient.Timeout = TimeSpan.FromMinutes(30); var multipartContent = new MultipartFormDataContent { { new StringContent("srTest"), "Id" }, { new StreamContent(_fileStream), "Document", "Document.doc" } }; Console.WriteLine($"FileSize: {_fileStream.Length}"); var postResponse = await httpClient.PostAsync("http://localhost:58639/api/Upload", multipartContent); Console.WriteLine(postResponse.StatusCode); Console.WriteLine(postResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult());
问题分析与修复方案
1. 30MB文件上传失败
当前配置的MultipartBodyLengthLimit和RequestSizeLimit都是50MB,但30MB仍失败,核心原因:
- 未正确处理请求流:控制器仅循环读取section但未消费流内容,导致请求未完成;
- 缺少Kestrel层面的全局请求大小限制。
修复:
- 在Program.cs中配置Kestrel的最大请求体大小:
builder.WebHost.ConfigureKestrel(options => { options.Limits.MaxRequestBodySize = 10L * 1024 * 1024 * 1024; // 10GB });
- 在控制器中添加section内容处理逻辑,确保流被完全消费。
2. 重复请求抛出Multipart body length limit异常
异常中的16KB是默认限制值,说明FormOptions配置未生效——因为DisableFormValueModelBinding移除了FormFileValueProviderFactory,导致配置未关联到请求处理。
修复:
在控制器方法中手动设置请求的MultipartBodyLengthLimit:
[HttpPost] [DisableFormValueModelBinding] [RequestSizeLimit(10L * 1024 * 1024 * 1024)] public async Task<IActionResult> Post() { // 手动绑定大文件限制 HttpContext.Features.Get<IFormFeature>()?.FormOptions.MultipartBodyLengthLimit = 10L * 1024 * 1024 * 1024; var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType), int.MaxValue); var reader = new MultipartReader(boundary, HttpContext.Request.Body); var section = await reader.ReadNextSectionAsync(); while (section != null) { var contentDisposition = ContentDispositionHeaderValue.Parse(section.ContentDisposition); if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition)) { // 读取文件到磁盘 using var fileStream = new FileStream(Path.Combine("uploads", contentDisposition.FileName.Value), FileMode.Create); await section.Body.CopyToAsync(fileStream); } else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition)) { // 读取Id参数 var id = await new StreamReader(section.Body).ReadToEndAsync(); } section = await reader.ReadNextSectionAsync(); } return Ok(); }
3. C#客户端请求无响应
- 客户端未正确释放文件流:使用
using包裹流确保资源释放; - 服务器未完成请求处理:原控制器未消费流,导致请求挂起,需添加流处理逻辑;
- 缺少异常捕获:服务器端添加日志记录未捕获的异常。
客户端修复代码:
using var _fileStream = File.OpenRead(... file ...); var httpClient = new HttpClient(); httpClient.Timeout = TimeSpan.FromMinutes(30); var multipartContent = new MultipartFormDataContent { { new StringContent("srTest"), "Id" }, { new StreamContent(_fileStream), "Document", "Document.doc" } }; Console.WriteLine($"FileSize: {_fileStream.Length}"); try { var postResponse = await httpClient.PostAsync("http://localhost:58639/api/Upload", multipartContent); postResponse.EnsureSuccessStatusCode(); Console.WriteLine(postResponse.StatusCode); Console.WriteLine(await postResponse.Content.ReadAsStringAsync()); } catch (Exception ex) { Console.WriteLine($"请求失败: {ex.Message}"); }
最终配置要点
- 全局配置Kestrel的最大请求体大小;
- 控制器中手动绑定
MultipartBodyLengthLimit; - 必须消费每个multipart section的流内容;
- 客户端使用
using确保文件流释放; - 添加服务器端日志,便于异常排查。
内容的提问来源于stack exchange,提问作者Vesen
相关产品推荐
相关产品推荐

