Limites de tamanho de upload no ASP.NET MVC 5 no IIS

Diagrama das camadas que podem recusar um upload grande no MVC 5: a caixa de diálogo da galeria, a filtragem de requisições do IIS, a seleção de action do MVC e RteController.Upload

Uma imagem grande demais enviada para uma action do ASP.NET MVC 5 no IIS pode ser barrada em quatro lugares. Dois deles respondem com HTML, e um dispara antes mesmo de a sua action ser escolhida, então nenhum try/catch no controller consegue alcançá-lo. Esta página lista as camadas, os valores do web.config em bytes e em kilobytes, e os dois eventos do Global.asax.cs que transformam toda rejeição em um JSON 413 para o editor.

As camadas

  1. A caixa de diálogo da galeria. maxUploadFileSize: 4000000 na view faz com que ela recuse um arquivo maior antes de enviá-lo: "big.png is 4.3 MB. The maximum allowed size is 3.8 MB." A caixa de diálogo conta megabytes binários, e nenhuma requisição foi enviada. Isso é uma conveniência. Uploads feitos de outros lugares, ou de outros clientes, ainda chegam ao servidor.
  2. A filtragem de requisições do IIS, maxAllowedContentLength em bytes. O padrão é 30,000,000. Esse valor é comparado com o Content-Length antes de o ASP.NET começar.
  3. O ASP.NET, <httpRuntime maxRequestLength> em kilobytes. O padrão é 4096 KB, que equivale a 4,194,304 bytes. Ele dispara na primeira leitura do corpo.
  4. RteFiles.SaveUpload: 4,000,000 bytes, respondido com um 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>

Os dois são 5,242,880 bytes, escritos uma vez como 5120 KB e outra em bytes. Qualquer coisa entre 4,000,000 bytes e cerca de 5.2 MB chega à action e recebe a mensagem específica dela.

A surpresa: [HttpPost] lê o corpo

Aumentamos maxAllowedContentLength para 50,000,000, para que o limite do ASP.NET fosse aplicado primeiro, e envolvemos o acesso ao arquivo na action em um try/catch. Um upload de 6 MB ainda retornou HTML 500 "Maximum request length exceeded." O stack trace mostra por quê:

Stack trace (resumido), upload de 6,000,008 bytes
[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] procura um campo de formulário X-HTTP-Method-Override enquanto o MVC ainda está escolhendo a action. Ler Request.Form lê o corpo inteiro, e é aí que maxRequestLength lança a exceção. Nenhuma action e nenhum action filter foi executado ainda. O Application_Error ainda a vê:

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

Com isso, o mesmo upload de 6 MB respondeu 413 {"error":"The upload is larger than the server allows."}.

O 413.1 do próprio IIS

Com os valores que vêm no exemplo, um upload de 6 MB nunca chega ao ASP.NET. A filtragem de requisições do IIS responde 413.1 "Request Entity Too Large" com uma página HTML. (O IIS 7 e 8 usavam 404.13 para isso, e o exemplo aceita qualquer um dos dois.) O Application_EndRequest, na mesma região acima, ainda é executado para essa requisição. RteFiles.RewriteTooLarge verifica o substatus, então as mensagens 413 da própria action não são alteradas:

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

Na sua própria máquina, isso basta. O padrão do IIS, errorMode="DetailedLocalOnly", mostra páginas de erro personalizadas para visitantes remotos. No exemplo Web Forms, que tem o mesmo web.config, definimos errorMode="Custom" para simular um visitante remoto. O IIS então substituiu o JSON pela sua própria página "request entity is too large" mesmo assim. Este bloco location deixa o JSON passar apenas em /rte/:

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>

Teste

Com o projeto de exemplo deste guia rodando na porta 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."}

As duas são HTTP 413 com JSON. A primeira veio do RteFiles e a segunda do Application_EndRequest, depois do 413.1 do IIS. Para ver o caso da seleção da action, aumente maxAllowedContentLength acima de 5,242,880 e envie huge.jpg de novo. O Application_Error responde.

O mesmo guia para outras plataformas

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

Leia a seguir