ASP.NET Core 中的上传大小限制:Kestrel、IIS 和 MVC

示意图:可能拒绝大文件上传的五层,从 IIS 的 maxAllowedContentLength 和 Kestrel 的 MaxRequestBodySize,到路由限制、表单解析器和应用自己的检查

在 ASP.NET Core 中,一张大照片可能在六个地方被拒绝,而只有最后一个是您自己的代码。其他几层各有各的响应方式:不带响应体的 413、MVC 返回的 ProblemDetails 400、IIS 错误页面,或者浏览器报告为网络错误的连接中断。本页按请求经过的顺序列出这些层,并附上我们测试时每一层的实际响应。

各层的顺序

  1. 编辑器。在选项中设置 maxUploadFileSize: 4000000 后,图片库会在发送之前拒绝更大的文件。一个 4.5 MB 的文件得到了 "big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB." 的提示。对话框按二进制兆字节计算。服务器不能依赖这一层。
  2. IIS(如果应用以进程内方式运行在 IIS 之后)。根据 Microsoft 文档,请求筛选会在 ASP.NET Core 运行之前,以 IIS 自己的 404.13 页面拒绝超过 maxAllowedContentLength(以字节为单位,默认 30,000,000)的请求体。本页没有测试 IIS。
  3. Kestrel 的 MaxRequestBodySize,默认 30,000,000 字节。一次 31,000,000 字节的上传得到了 413。
  4. 路由自己的限制。[RequestSizeLimit] 为单个操作方法替换 Kestrel 的限制。在 Minimal API 中,同一个特性作为端点元数据生效。
  5. multipart 解析器。FormOptions.MultipartBodyLengthLimit 默认为 134,217,728 字节。可以用 [RequestFormLimits] 为单个操作方法设置,或用 .WithFormOptions 为单个端点设置。
  6. 您自己的检查:示例中为 4,000,000 字节,超出时返回 JSON 格式的 413。

代码

C# · RteFiles.cs
public const long MaxFileBytes = 4_000_000;
// The request is a little bigger than the file: multipart boundaries, headers, the "folder" field.
public const long MaxRequestBytes = MaxFileBytes + 64 * 1024;

请求限制等于文件限制加上 64 KB,用于容纳 multipart 边界和 folder 字段。这个差额是有意留出的:略超过 4,000,000 字节的文件仍然能到达代码,并得到一个可读的响应。

C# · Controllers/RteController.cs
// POST /rte/upload   file, folder
[HttpPost("upload")]
[RequestSizeLimit(RteFiles.MaxRequestBytes)]                              // the whole request body
[RequestFormLimits(MultipartBodyLengthLimit = RteFiles.MaxRequestBytes)]  // the multipart parser
public async Task<IActionResult> Upload(IFormFile? file, [FromForm] string? folder) =>
    Send(await RteFiles.Save(HttpContext, file, folder));
C# · RteEndpoints.cs
rte.MapPost("/upload", async (HttpContext http, IFormFile? file, [FromForm] string? folder) =>
    Json(await RteFiles.Save(http, file, folder)))
    .DisableAntiforgery()
    // Kestrel accepts 30,000,000-byte bodies by default; this route needs ~4 MB.
    .WithMetadata(new RequestSizeLimitAttribute(RteFiles.MaxRequestBytes))
    .WithFormOptions(multipartBodyLengthLimit: RteFiles.MaxRequestBytes);

浏览器看到什么

当请求超过路由限制时,Kestrel 会在浏览器仍在发送数据时返回 413 并关闭连接。我们在全部四个 ASP.NET Core 示例中,从 Chromium 用 XHR 发送了 10 MB 数据(绕过编辑器自身的检查)。每一次 XHR 都以状态 0 进入 onerror,从未得到 413。此时 rte-files.js 会报告 "The server could not be reached."。而会等待响应的 curl 确实看到了 413。因此,对于大文件,能给用户有用提示的是编辑器的 maxUploadFileSize。请让它与服务器的限制保持一致。

MVC 返回 400 而不是 413

在 Minimal API 中,超过路由限制的请求体会在参数绑定时失败,端点返回不带响应体的 413。而在 [ApiController] 中,它会在模型绑定时失败,并由自动模型验证返回 ProblemDetails 400:

HTTP 400 响应体,向 MVC 示例上传 10,000,000 字节(省略了 traceId)
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,
 "errors":{"":["Failed to read the request form. Request body too large. The max request body size is 4065536 bytes."]}}

一个 4,100,000 字节的文件也得到了同样的响应,因为加上 multipart 封装后,请求刚好超过 4,065,536 字节。要得到示例自身检查返回的 {"error": ...} 413,整个请求必须在路由限制以内,4,050,000 字节的文件就满足这一点。

关闭请求限制后,超过 MultipartBodyLengthLimit 时,Minimal API 端点同样返回 400(以默认值 134,217,728 测试,上传 135,000,000 字节)。

提高限制

同时修改 RteFiles.cs 中的 MaxFileBytes 和页面上的 maxUploadFileSize。MaxRequestBytes 跟随 MaxFileBytes 变化,因此路由限制和 multipart 限制也会随之调整。超过 30,000,000 字节时,前面的每一层也都必须允许:Kestrel 的全局限制仍然适用于其他路由,而根据 Microsoft 文档,IIS 需要在 web.config 中调高 maxAllowedContentLength,因为 [RequestSizeLimit] 不能超过它。

测试

用 dotnet run 运行本指南的示例项目(端口 5302),并同时运行 Minimal API 示例(端口 5301):

Shell
head -c 4050000 /dev/zero > big.jpg
curl -F "file=@big.jpg" http://localhost:5302/rte/upload
{"error":"The image is larger than 4 MB."}

head -c 10000000 /dev/zero > huge.jpg
curl -s -w "\nHTTP %{http_code}\n" -F "file=@huge.jpg" http://localhost:5301/rte/upload

HTTP 413

第一个响应是示例检查返回的 413,两个端口都一样。第二个是 Kestrel 返回的不带响应体的 413。把 huge.jpg 发送到端口 5302,就能看到上文所述 MVC 返回的 400。

其他平台的同一指南

JavaScript · Next.js · PHP · MVC 5 · Web Forms

延伸阅读