Limites de tamanho de upload no ASP.NET MVC 5 no IIS
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
- A caixa de diálogo da galeria.
maxUploadFileSize: 4000000na 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. - A filtragem de requisições do IIS,
maxAllowedContentLengthem bytes. O padrão é 30,000,000. Esse valor é comparado com o Content-Length antes de o ASP.NET começar. - 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. - RteFiles.SaveUpload: 4,000,000 bytes, respondido com um JSON 413.
<!-- 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" /><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ê:
[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ê:
// 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:
// 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/:
<!-- /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:
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