Upload size limits in ASP.NET MVC 5 on IIS

Diagram of the layers that can refuse a large upload in MVC 5: the gallery dialog, IIS request filtering, MVC action selection and RteController.Upload

An oversized image sent to an ASP.NET MVC 5 action on IIS can be stopped in four places. Two of them answer with HTML, and one of them fires before your action is even chosen, so no try/catch in the controller can reach it. This page lists the layers, the web.config values in bytes and in kilobytes, and the two Global.asax.cs events that make every rejection a JSON 413 for the editor.

The layers

  1. The gallery dialog. maxUploadFileSize: 4000000 in the view makes it refuse a larger file before sending: "big.png is 4.3 MB. The maximum allowed size is 3.8 MB." The dialog counts binary megabytes, and no request was sent. That is a convenience. Uploads from other places, or from other clients, still reach the server.
  2. IIS request filtering, maxAllowedContentLength in bytes. The default is 30,000,000. It is checked against Content-Length before ASP.NET starts.
  3. ASP.NET, <httpRuntime maxRequestLength> in kilobytes. The default is 4096 KB, which is 4,194,304 bytes. It fires on the first read of the body.
  4. RteFiles.SaveUpload: 4,000,000 bytes, answered with a 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>

Both are 5,242,880 bytes, written once as 5120 KB and once as bytes. Anything between 4,000,000 bytes and about 5.2 MB reaches the action and gets its specific message.

The surprise: [HttpPost] reads the body

We raised maxAllowedContentLength to 50,000,000, so that ASP.NET's limit would apply first, and wrapped the action's file access in a try/catch. A 6 MB upload still returned HTML 500 "Maximum request length exceeded." The stack trace shows why:

Stack trace (shortened), 6,000,008-byte upload
[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] looks for an X-HTTP-Method-Override form field while MVC is still choosing the action. Reading Request.Form reads the whole body, and that is where maxRequestLength throws. No action and no action filter has run yet. Application_Error still sees it:

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

With that in place, the same 6 MB upload answered 413 {"error":"The upload is larger than the server allows."}.

IIS's own 413.1

With the shipped values, a 6 MB upload never reaches ASP.NET. IIS request filtering answers 413.1 "Request Entity Too Large" with an HTML page. (IIS 7 and 8 used 404.13 for this, and the sample accepts either.) Application_EndRequest, in the same region above, still runs for that request. RteFiles.RewriteTooLarge checks the substatus, so the action's own 413 messages are left alone:

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

On your own machine that is enough. IIS's default errorMode="DetailedLocalOnly" shows remote visitors custom error pages. On the Web Forms sample, which has the same web.config, we set errorMode="Custom" to stand in for a remote visitor. IIS then replaced the JSON with its own "request entity is too large" page anyway. This location block lets the JSON through on /rte/ only:

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>

Test it

With the sample project for this guide running on port 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."}

Both are HTTP 413 with JSON. The first came from RteFiles and the second from Application_EndRequest after IIS's 413.1. To see the action-selection case, raise maxAllowedContentLength above 5,242,880 and send huge.jpg again. Application_Error answers it.

The same guide for other platforms

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

Read next