ASP.NET Core でのアップロードのサイズ制限:Kestrel、IIS、MVC

図:大きなアップロードを拒否できる 5 つのレイヤー。IIS の maxAllowedContentLength と Kestrel の MaxRequestBodySize から、ルートの制限、フォームパーサー、アプリ独自のチェックまで

ASP.NET Core では、大きな写真が拒否される可能性のある場所が 6 つあり、自分のコードはそのうち最後の 1 つだけです。ほかのレイヤーは、それぞれ独自の方法で応答します。本文のない 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 進数のメガバイトで計算します。サーバーはこれに頼ることはできません。
  2. IIS(その背後でアプリをインプロセスで実行している場合)。Microsoft のドキュメントによると、要求フィルタリングは、maxAllowedContentLength(バイト単位、既定値は 30,000,000)を超える本文を、ASP.NET Core が実行される前に IIS 独自の 404.13 ページで拒否します。このページのために IIS でのテストは行っていません。
  3. Kestrel の MaxRequestBodySize。既定値は 30,000,000 バイトです。31,000,000 バイトのアップロードは 413 になりました。
  4. ルート独自の制限。[RequestSizeLimit] は、1 つのアクションについて Kestrel の制限を置き換えます。最小 API では、同じ属性がエンドポイントのメタデータとして機能します。
  5. マルチパートパーサー。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;

リクエストの制限は、ファイルの制限に、マルチパートの境界と folder フィールドのための 64 KB を加えた値です。この差は意図的なものです。これにより、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 を返し、ブラウザーがまだ送信している最中に接続を閉じます。4 つすべての ASP.NET Core のサンプルで、エディター自体のチェックを経由せずに、Chromium から XHR で 10 MB を送信しました。どの場合も、XHR はステータス 0 の onerror で終わり、413 になることは一度もありませんでした。そのとき rte-files.js は「The server could not be reached.」と報告します。レスポンスを待つ curl では 413 が確認できました。つまり、大きなファイルについてユーザーに役立つメッセージを表示できるのは、エディターの maxUploadFileSize です。これはサーバーの制限と同じ値にしておいてください。

MVC は 413 ではなく 400 を返す

最小 API では、ルートの制限を超える本文はパラメーターのバインド中に失敗し、エンドポイントは本文のない 413 を返します。[ApiController] では、代わりにモデルバインド中に失敗し、自動モデル検証が ProblemDetails の 400 を返します。

MVC のサンプルに 10,000,000 バイトをアップロードしたときの HTTP 400 の本文(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 バイトのファイルでも同じレスポンスになりました。マルチパートの枠組みを含めると、リクエストが 4,065,536 バイトをわずかに超えたためです。サンプル独自のチェックによる {"error": ...} の 413 を返すには、リクエスト全体がルートの制限内に収まる必要があり、4,050,000 バイトのファイルならそれを満たします。

リクエストの制限を無効にした状態で MultipartBodyLengthLimit を超えた場合も、最小 API のエンドポイントは 400 を返しました(既定の 134,217,728 に対して 135,000,000 バイトのアップロード)。

制限を引き上げる

RteFiles.cs の MaxFileBytes と、ページの maxUploadFileSize を一緒に変更します。MaxRequestBytes は MaxFileBytes に追随するので、ルートの制限とマルチパートの制限もそれに合わせて変わります。30,000,000 バイトを超える場合は、前段にあるすべてのレイヤーでも許可する必要があります。Kestrel のグローバルな制限はほかのルートには引き続き適用されます。また Microsoft のドキュメントによると、[RequestSizeLimit] は maxAllowedContentLength を超えられないため、IIS では web.config でこの値を引き上げる必要があります。

テストする

このガイドのサンプルプロジェクト(ポート 5302)と 最小 API のサンプル(ポート 5301)を、それぞれ dotnet run で並べて実行します。

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

1 つ目のレスポンスは、どちらのポートでもサンプルのチェックによる 413 です。2 つ目は Kestrel による本文のない 413 です。huge.jpg をポート 5302 に送信すると、上で紹介した MVC の 400 を確認できます。

ほかのプラットフォーム向けの同じガイド

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

次に読む