Limites de tamanho de upload no ASP.NET Core: Kestrel, IIS e MVC

Diagrama: cinco camadas que podem recusar um upload grande, do maxAllowedContentLength do IIS e do MaxRequestBodySize do Kestrel ao limite da rota, ao parser do formulário e à verificação do próprio app

Uma foto grande pode ser barrada em seis lugares no ASP.NET Core, e só o último é o seu código. Os outros respondem cada um à sua maneira: um 413 sem corpo, um ProblemDetails 400 do MVC, uma página de erro do IIS ou uma conexão encerrada que o navegador relata como erro de rede. Esta página lista as camadas na ordem em que uma requisição passa por elas, com o que cada uma devolveu quando testamos.

As camadas, em ordem

  1. O editor. Com maxUploadFileSize: 4000000 nas opções, a galeria recusa um arquivo maior antes de enviá-lo. Um arquivo de 4.5 MB gerou "big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB." A caixa de diálogo conta em megabytes binários. O servidor não pode depender disso.
  2. IIS, se o aplicativo roda in-process atrás dele. Segundo a documentação da Microsoft, a filtragem de requisições rejeita corpos acima de maxAllowedContentLength (em bytes, 30,000,000 por padrão) com a página 404.13 do próprio IIS, antes de o ASP.NET Core rodar. Não testamos o IIS para esta página.
  3. MaxRequestBodySize do Kestrel, 30,000,000 bytes por padrão. Um upload de 31,000,000 bytes recebeu 413.
  4. O limite da própria rota. [RequestSizeLimit] substitui o limite do Kestrel para uma action. Em uma Minimal API, o mesmo atributo funciona como metadado do endpoint.
  5. O parser de multipart. FormOptions.MultipartBodyLengthLimit é 134,217,728 bytes por padrão. Defina-o por action com [RequestFormLimits] ou por endpoint com .WithFormOptions.
  6. A sua verificação: 4,000,000 bytes no exemplo, respondida com um 413 em JSON.

O código

C# · RteFiles.cs
public const long MaxFileBytes = 4_000_000;
// The request is a little bigger than the file: multipart boundaries, headers, the "folder" field.
public const long MaxRequestBytes = MaxFileBytes + 64 * 1024;

O limite da requisição é o limite do arquivo mais 64 KB para os delimitadores do multipart e o campo folder. Essa folga é proposital: um arquivo um pouco acima de 4,000,000 bytes ainda chega ao código e recebe uma resposta legível.

C# · Controllers/RteController.cs
// POST /rte/upload   file, folder
[HttpPost("upload")]
[RequestSizeLimit(RteFiles.MaxRequestBytes)]                              // the whole request body
[RequestFormLimits(MultipartBodyLengthLimit = RteFiles.MaxRequestBytes)]  // the multipart parser
public async Task<IActionResult> Upload(IFormFile? file, [FromForm] string? folder) =>
    Send(await RteFiles.Save(HttpContext, file, folder));
C# · RteEndpoints.cs
rte.MapPost("/upload", async (HttpContext http, IFormFile? file, [FromForm] string? folder) =>
    Json(await RteFiles.Save(http, file, folder)))
    .DisableAntiforgery()
    // Kestrel accepts 30,000,000-byte bodies by default; this route needs ~4 MB.
    .WithMetadata(new RequestSizeLimitAttribute(RteFiles.MaxRequestBytes))
    .WithFormOptions(multipartBodyLengthLimit: RteFiles.MaxRequestBytes);

O que o navegador vê

Quando uma requisição passa do limite da rota, o Kestrel responde 413 e fecha a conexão enquanto o navegador ainda está enviando. Enviamos 10 MB a partir do Chromium com uma XHR, contornando a verificação do próprio editor, nos quatro exemplos ASP.NET Core. Em todas as vezes, a XHR terminou em onerror com status 0, nunca com um 413. O rte-files.js então informa "The server could not be reached." O curl, que espera pela resposta, viu o 413. Portanto, é o maxUploadFileSize do editor que dá aos usuários uma mensagem útil para arquivos grandes. Mantenha-o igual ao limite do servidor.

O MVC responde 400, não 413

Em uma Minimal API, um corpo acima do limite da rota falha durante o binding de parâmetros, e o endpoint responde 413 com o corpo vazio. Em um [ApiController], a falha acontece durante o model binding, e a validação automática do modelo responde com um ProblemDetails 400:

Corpo do HTTP 400, upload de 10,000,000 bytes para o exemplo MVC (traceId omitido)
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,
 "errors":{"":["Failed to read the request form. Request body too large. The max request body size is 4065536 bytes."]}}

Um arquivo de 4,100,000 bytes gerou a mesma resposta, porque, com o enquadramento do multipart, a requisição ficou um pouco acima de 4,065,536 bytes. O 413 com {"error": ...} da verificação do próprio exemplo exige que a requisição inteira caiba no limite da rota, o que acontece com um arquivo de 4,050,000 bytes.

Acima de MultipartBodyLengthLimit, com o limite da requisição desativado, um endpoint de Minimal API também respondeu 400 (upload de 135,000,000 bytes contra o padrão de 134,217,728).

Aumentando o limite

Altere MaxFileBytes em RteFiles.cs e maxUploadFileSize na página ao mesmo tempo. MaxRequestBytes acompanha MaxFileBytes, então o limite da rota e o limite do multipart mudam junto. Acima de 30,000,000 bytes, todas as camadas à frente também precisam permitir: o limite global do Kestrel continua valendo para as outras rotas, e a documentação da Microsoft diz que o IIS precisa que maxAllowedContentLength seja aumentado em web.config, porque [RequestSizeLimit] não pode ultrapassá-lo.

Teste

Execute o projeto de exemplo deste guia com dotnet run (porta 5302) e, ao lado dele, o exemplo de Minimal API (porta 5301):

Shell
head -c 4050000 /dev/zero > big.jpg
curl -F "file=@big.jpg" http://localhost:5302/rte/upload
{"error":"The image is larger than 4 MB."}

head -c 10000000 /dev/zero > huge.jpg
curl -s -w "\nHTTP %{http_code}\n" -F "file=@huge.jpg" http://localhost:5301/rte/upload

HTTP 413

A primeira resposta é um 413 da verificação do exemplo, em qualquer uma das portas. A segunda é o 413 do Kestrel, sem corpo. Envie huge.jpg para a porta 5302 para ver o 400 do MVC mostrado acima.

O mesmo guia para outras plataformas

JavaScript · Next.js · PHP · MVC 5 · Web Forms

Leia a seguir