Limites de tamanho de upload no ASP.NET Web Forms no IIS

Diagrama das camadas que podem recusar um upload grande no Web Forms: a caixa de diálogo da galeria, a filtragem de requisições do IIS, o httpRuntime do ASP.NET e o handler de upload

No IIS, um upload do ASP.NET Web Forms passa por dois limites de tamanho antes de o seu código vê-lo. Eles usam unidades diferentes, e cada um gera uma página de erro diferente, nenhuma delas em JSON. Esta página lista todas as camadas que podem recusar uma imagem grande, as configurações que o exemplo usa e as duas peças extras (um hook no Global.asax e uma linha em httpErrors) que fazem toda rejeição voltar como JSON. A segunda peça só importa para visitantes remotos.

As camadas, em ordem

  1. A caixa de diálogo da galeria. Com maxUploadFileSize: 4000000, ela recusa um arquivo maior antes de enviar qualquer coisa. Um arquivo de 4,500,008 bytes recebeu "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 chegou ao servidor. Isso é uma conveniência, e o servidor não pode depender dela.
  2. A filtragem de requisições do IIS: maxAllowedContentLength, em bytes. O padrão é 30,000,000. O IIS compara esse valor com o Content-Length antes de o ASP.NET rodar.
  3. O ASP.NET: <httpRuntime maxRequestLength>, em kilobytes. O padrão é 4096, que equivale a 4,194,304 bytes (new HttpRuntimeSection().MaxRequestLength imprimiu 4096 na nossa máquina). Ele é aplicado quando o corpo é lido pela primeira vez.
  4. A verificação do próprio handler: 4,000,000 bytes por arquivo, respondida 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 estão definidos com os mesmos 5,242,880 bytes, uma vez como 5120 KB e outra em bytes. O padrão de 4096 KB também comportaria uma imagem de 4,000,000 bytes mais as poucas centenas de bytes de overhead do multipart. A folga extra faz com que um arquivo um pouco grande demais ainda chegue ao handler e receba a mensagem específica dele.

O que cada camada devolve

UploadRecusado porResposta sem as peças extras
4,500,008 byteso handler413 {"error":"The image is larger than the 4 MB (4,000,000 bytes) limit."}
6,000,008 bytesfiltragem de requisições do IIS413.1 "Request Entity Too Large", uma página HTML do IIS
6,000,008 bytes, com maxAllowedContentLength aumentado para 50,000,000ASP.NETHTML 500 "Maximum request length exceeded."

Guias mais antigos descrevem a rejeição do IIS como 404.13. O IIS 7 e 8 usavam esse código, mas o IIS Express 10 retornou 413.1 aqui. O exemplo trata os dois.

Como transformar a página do IIS em JSON

A filtragem de requisições rejeita a requisição antes de qualquer handler rodar, mas o evento gerenciado Application_EndRequest ainda dispara para ela:

C# · Global.asax
// Optional: makes "request too large" from IIS come back as JSON on /rte/ routes.
void Application_EndRequest(object sender, EventArgs e)
{
    RteFiles.RewriteTooLarge(Context);
}
C# · App_Code/RteFiles.cs
// Call from Application_EndRequest (Global.asax). 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(HttpContext context)
{
    HttpResponse 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.");
    }
}

A verificação do substatus importa. Nossa primeira versão capturava todo 413 em /rte/ e sobrescrevia a mensagem 413 do próprio handler, que é mais precisa.

Isso funcionou localmente e ainda falhava para visitantes remotos. O IIS Express e o IIS usam por padrão errorMode="DetailedLocalOnly". Definimos errorMode="Custom", que é o que um visitante remoto recebe. O JSON gravado no EndRequest foi então substituído pela mensagem do IIS "The page was not displayed because the request entity is too large." Adicionar este bloco resolveu o problema, e o teste de contrato continuou passando:

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
     writes when request filtering rejects an oversized upload. -->
<location path="rte">
  <system.webServer>
    <httpErrors existingResponse="PassThrough" />
  </system.webServer>
</location>

O limite do ASP.NET é tratado no próprio handler de upload. Request.Files é lido dentro de um try, e uma HttpException com WebEventCodes.RuntimeErrorPostTooLarge vira um JSON 413. Com o limite do IIS aumentado para que o limite do ASP.NET fosse aplicado primeiro, um upload de 6 MB recebeu esse JSON em vez do HTML 500. Com as configurações que vêm no exemplo, só um upload sem cabeçalho Content-Length (chunked) deve seguir esse caminho. Não testamos esse caso.

Teste

Com o projeto de exemplo deste guia rodando na porta 5401 (run.ps1):

Shell
head -c 4500000 /dev/zero > four.jpg     # then make it start like a JPEG:
printf '\xff\xd8\xff' | cat - four.jpg > big.jpg
curl -F "file=@big.jpg" http://localhost:5401/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:5401/rte/upload
{"error":"The upload is larger than the server allows."}

As duas são HTTP 413 com JSON. A primeira veio do handler e a segunda do hook no Global.asax. Exclua o Global.asax e envie huge.jpg de novo para ver a página 413.1 do IIS.

O mesmo guia para outras plataformas

JavaScript · Next.js · PHP · ASP.NET Core · MVC 5

Leia a seguir