You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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}");
}

最终配置要点

  1. 全局配置Kestrel的最大请求体大小;
  2. 控制器中手动绑定MultipartBodyLengthLimit;
  3. 必须消费每个multipart section的流内容;
  4. 客户端使用using确保文件流释放;
  5. 添加服务器端日志,便于异常排查。

内容的提问来源于stack exchange,提问作者Vesen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 20:57:43