IIS 上の ASP.NET MVC 5 でのアップロードのサイズ制限

MVC 5 で大きなアップロードを拒否できるレイヤーの図。ギャラリーダイアログ、IIS の要求フィルタリング、MVC のアクション選択、RteController.Upload

IIS 上の ASP.NET MVC 5 のアクションに送られた大きすぎる画像は、4 か所で止められる可能性があります。そのうち 2 か所は HTML で応答し、そのうち 1 か所はアクションが選ばれる前に発生するため、コントローラーの try/catch では捕捉できません。このページでは、各レイヤー、バイト単位とキロバイト単位の web.config の値、そしてすべての拒否をエディター向けの JSON の 413 にする Global.asax.cs の 2 つのイベントを紹介します。

各レイヤー

  1. ギャラリーダイアログ。ビューで maxUploadFileSize: 4000000 を指定すると、ダイアログはそれより大きなファイルを送信前に拒否し、「big.png is 4.3 MB. The maximum allowed size is 3.8 MB.」と表示します。ダイアログは 2 進数のメガバイトで計算し、リクエストは送信されませんでした。これは利便性のための機能です。ほかの場所やほかのクライアントからのアップロードは、依然としてサーバーに届きます。
  2. IIS の要求フィルタリング。maxAllowedContentLength はバイト単位です。既定値は 30,000,000 で、ASP.NET が起動する前に Content-Length と照合されます。
  3. ASP.NET。<httpRuntime maxRequestLength> はキロバイト単位です。既定値は 4096 KB、つまり 4,194,304 バイトです。本文を最初に読み取ったときに発生します。
  4. RteFiles.SaveUpload:4,000,000 バイトで、JSON の 413 を返します。
XML · web.config
<!-- maxRequestLength is in KILOBYTES (default 4096 = 4,194,304 bytes). 5120 KB = 5,242,880
     bytes: comfortably above the 4,000,000-byte image limit plus multipart overhead, so a
     file that is slightly too big still reaches the handler and gets its JSON 413. -->
<httpRuntime targetFramework="4.8" maxRequestLength="5120" />
XML · web.config
<requestFiltering>
  <!-- maxAllowedContentLength is in BYTES (default 30,000,000). Same 5,242,880 bytes as
       maxRequestLength above. IIS checks this one first, before ASP.NET runs. -->
  <requestLimits maxAllowedContentLength="5242880" />
</requestFiltering>

どちらも 5,242,880 バイトで、一方は 5120 KB、もう一方はバイトで記述しています。4,000,000 バイトから約 5.2 MB までのファイルはアクションに届き、それに応じたメッセージを受け取ります。

意外な点:[HttpPost] が本文を読み取る

ASP.NET の制限が先に適用されるよう maxAllowedContentLength を 50,000,000 に引き上げ、アクション内のファイルへのアクセスを try/catch で囲みました。それでも、6 MB のアップロードは HTML の 500「Maximum request length exceeded.」を返しました。スタックトレースを見ると理由がわかります。

スタックトレース(抜粋)、6,000,008 バイトのアップロード
[HttpException (0x80004005): Maximum request length exceeded.]
   System.Web.HttpRequest.GetEntireRawContent()
   System.Web.HttpRequest.FillInFormCollection()
   System.Web.HttpRequest.get_Form()
   System.Web.Mvc.HttpRequestExtensions.GetHttpMethodOverride(HttpRequestBase request)
   System.Web.Mvc.AcceptVerbsAttribute.IsValidForRequest(...)
   System.Web.Mvc.ActionMethodSelectorBase.RunSelectionFilters(...)

[HttpPost] は、MVC がまだアクションを選んでいる段階で、X-HTTP-Method-Override というフォームフィールドを探します。Request.Form を読み取ると本文全体が読み取られ、そこで maxRequestLength が例外をスローします。この時点では、アクションもアクションフィルターもまだ実行されていません。それでも Application_Error はこれを捕捉できます。

C# · Global.asax.cs
// Optional, both below: oversized uploads answer in JSON on /rte/ like every other error.
//
// IIS rejects bodies over maxAllowedContentLength before MVC runs (HTTP 413.1, HTML page),
// but EndRequest still runs.
protected void Application_EndRequest(object sender, EventArgs e)
{
    RteFiles.RewriteTooLarge(new HttpContextWrapper(Context));
}

// A body over <httpRuntime maxRequestLength> (reachable when maxAllowedContentLength is
// the larger of the two, or for a chunked upload) throws "Maximum request length
// exceeded" when MVC first reads the form: during action selection ([HttpPost] looks
// for an X-HTTP-Method-Override form field), before any action or filter can catch it.
protected void Application_Error(object sender, EventArgs e)
{
    HttpException ex = Server.GetLastError() as HttpException;
    if (ex != null && ex.WebEventCode == WebEventCodes.RuntimeErrorPostTooLarge
        && Request.AppRelativeCurrentExecutionFilePath.StartsWith("~/rte/", StringComparison.OrdinalIgnoreCase))
    {
        Server.ClearError();
        RteFiles.WriteError(new HttpContextWrapper(Context), 413, "The upload is larger than the server allows.");
    }
}

これを追加すると、同じ 6 MB のアップロードは 413 {"error":"The upload is larger than the server allows."} を返しました。

IIS 独自の 413.1

サンプルに含まれる値のままでは、6 MB のアップロードは ASP.NET に届きません。IIS の要求フィルタリングが HTML ページで 413.1「Request Entity Too Large」を返します(IIS 7 と 8 ではこれに 404.13 が使われており、サンプルはどちらにも対応しています)。上記と同じリージョンにある Application_EndRequest は、このリクエストでも実行されます。RteFiles.RewriteTooLarge はサブステータスをチェックするため、アクション自体の 413 のメッセージはそのまま残ります。

C# · RteFiles.cs
// Call from Application_EndRequest (Global.asax.cs). A body over maxAllowedContentLength is
// rejected by IIS request filtering before any handler runs - HTTP 413.1 with an HTML
// page (404.13 on IIS 7/8) - but EndRequest still runs, so the /rte/ routes can answer
// in JSON instead. The substatus check leaves the handlers' own 413s alone.
public static void RewriteTooLarge(HttpContextBase context)
{
    HttpResponseBase response = context.Response;
    bool rejectedByIis = (response.StatusCode == 413 && response.SubStatusCode == 1)
        || (response.StatusCode == 404 && response.SubStatusCode == 13);
    if (rejectedByIis
        && context.Request.AppRelativeCurrentExecutionFilePath.StartsWith("~/rte/", StringComparison.OrdinalIgnoreCase))
    {
        WriteError(context, 413, "The upload is larger than the server allows.");
    }
}

自分のマシンでは、これで十分です。IIS の既定の errorMode="DetailedLocalOnly" では、リモートの訪問者にはカスタムエラーページが表示されます。同じ web.config を持つ Web Forms のサンプルで、リモートの訪問者の代わりとして errorMode="Custom" を設定したところ、IIS は結局 JSON を独自の「request entity is too large」のページに置き換えました。次の location ブロックは、/rte/ に限って JSON をそのまま通します。

XML · web.config
<!-- /rte/ always answers in JSON. With errorMode="Custom" (what remote visitors get by
     default) IIS would otherwise swap its own HTML page in for the 413 that
     Global.asax.cs writes when request filtering rejects an oversized upload. -->
<location path="rte">
  <system.webServer>
    <httpErrors existingResponse="PassThrough" />
  </system.webServer>
</location>

テストする

このガイドのサンプルプロジェクトをポート 5402 で実行した状態で、以下を実行します。

Shell
head -c 4500000 /dev/zero > four.jpg
printf '\xff\xd8\xff' | cat - four.jpg > big.jpg
curl -F "file=@big.jpg" http://localhost:5402/rte/upload
{"error":"The image is larger than the 4 MB (4,000,000 bytes) limit."}

head -c 6000000 /dev/urandom > huge.jpg
curl -F "file=@huge.jpg" http://localhost:5402/rte/upload
{"error":"The upload is larger than the server allows."}

どちらも JSON 付きの HTTP 413 です。1 つ目は RteFiles から、2 つ目は IIS の 413.1 の後で Application_EndRequest から返されたものです。アクションの選択時のケースを確認するには、maxAllowedContentLength を 5,242,880 より大きくしてから、もう一度 huge.jpg を送信します。Application_Error がそれに応答します。

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

JavaScript · Next.js · PHP · ASP.NET Core · Web Forms

次に読む