Límites de tamaño de carga en ASP.NET MVC 5 sobre IIS
Una imagen demasiado grande enviada a una acción de ASP.NET MVC 5 en IIS puede detenerse en cuatro lugares. Dos de ellos responden con HTML, y uno se dispara antes incluso de que se elija tu acción, así que ningún try/catch del controlador puede alcanzarlo. Esta página enumera las capas, los valores de web.config en bytes y en kilobytes, y los dos eventos de Global.asax.cs que convierten cada rechazo en un 413 en JSON para el editor.
Las capas
- El cuadro de diálogo de la galería.
maxUploadFileSize: 4000000en la vista hace que rechace un archivo más grande antes de enviarlo: "big.png is 4.3 MB. The maximum allowed size is 3.8 MB." El cuadro de diálogo cuenta en megabytes binarios, y no se envió ninguna solicitud. Es una comodidad. Las cargas desde otros lugares, o desde otros clientes, siguen llegando al servidor. - El filtrado de solicitudes de IIS,
maxAllowedContentLength, en bytes. El valor predeterminado es 30,000,000. Se compara con Content-Length antes de que se inicie ASP.NET. - ASP.NET,
<httpRuntime maxRequestLength>, en kilobytes. El valor predeterminado es 4096 KB, es decir, 4,194,304 bytes. Se dispara en la primera lectura del cuerpo. - RteFiles.SaveUpload: 4,000,000 bytes, con una respuesta 413 en JSON.
<!-- 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>Ambos son 5,242,880 bytes, escritos una vez como 5120 KB y otra como bytes. Todo lo que esté entre 4,000,000 bytes y unos 5.2 MB llega a la acción y recibe su mensaje específico.
La sorpresa: [HttpPost] lee el cuerpo
Aumentamos maxAllowedContentLength a 50,000,000, para que se aplicara primero el límite de ASP.NET, y envolvimos el acceso al archivo de la acción en un try/catch. Una carga de 6 MB seguía devolviendo un HTML 500 "Maximum request length exceeded." El stack trace muestra 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] busca un campo de formulario X-HTTP-Method-Override mientras MVC todavía está eligiendo la acción. Leer Request.Form lee el cuerpo completo, y ahí es donde lanza la excepción maxRequestLength. Todavía no se ha ejecutado ninguna acción ni ningún filtro de acción. Application_Error sí la ve:
// 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.");
}
}Con eso, la misma carga de 6 MB respondió 413 {"error":"The upload is larger than the server allows."}.
El 413.1 propio de IIS
Con los valores incluidos, una carga de 6 MB nunca llega a ASP.NET. El filtrado de solicitudes de IIS responde 413.1 "Request Entity Too Large" con una página HTML. (IIS 7 y 8 usaban 404.13 para esto, y el ejemplo acepta ambos.) Application_EndRequest, en la misma región de arriba, se sigue ejecutando para esa solicitud. RteFiles.RewriteTooLarge comprueba el subestado, así que los mensajes 413 propios de la acción no se tocan:
// 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.");
}
}En tu propia máquina eso basta. El errorMode="DetailedLocalOnly" predeterminado de IIS muestra páginas de error personalizadas a los visitantes remotos. En el ejemplo de Web Forms, que tiene el mismo web.config, establecimos errorMode="Custom" para simular un visitante remoto. IIS sustituyó entonces el JSON por su propia página "request entity is too large" de todos modos. Este bloque location deja pasar el JSON solo en /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>Pruébalo
Con el proyecto de ejemplo de esta guía en ejecución en el puerto 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."}Ambas son HTTP 413 con JSON. La primera vino de RteFiles y la segunda de Application_EndRequest, después del 413.1 de IIS. Para ver el caso de la selección de acción, aumenta maxAllowedContentLength por encima de 5,242,880 y vuelve a enviar huge.jpg. Responde Application_Error.
La misma guía para otras plataformas
JavaScript · Next.js · PHP · ASP.NET Core · Web Forms