Limites de taille d’envoi avec ASP.NET MVC 5 sous IIS
Une image trop volumineuse envoyée à une action ASP.NET MVC 5 sous IIS peut être arrêtée à quatre endroits. Deux d’entre eux répondent en HTML, et l’un d’eux se déclenche avant même que votre action soit choisie : aucun try/catch dans le contrôleur ne peut donc l’atteindre. Cette page liste les couches, les valeurs de web.config en octets et en kilo-octets, et les deux événements de Global.asax.cs qui transforment chaque refus en un 413 JSON pour l’éditeur.
Les couches
- La boîte de dialogue de la galerie.
maxUploadFileSize: 4000000dans la vue lui fait refuser un fichier plus grand avant l’envoi : « big.png is 4.3 MB. The maximum allowed size is 3.8 MB. ». La boîte de dialogue compte en mégaoctets binaires, et aucune requête n’a été envoyée. C’est une commodité. Les envois provenant d’autres endroits, ou d’autres clients, atteignent quand même le serveur. - Le filtrage des requêtes d’IIS,
maxAllowedContentLengthen octets. La valeur par défaut est 30,000,000. Elle est comparée à Content-Length avant le démarrage d’ASP.NET. - ASP.NET,
<httpRuntime maxRequestLength>en kilo-octets. La valeur par défaut est 4096 Ko, soit 4,194,304 octets. Elle se déclenche à la première lecture du corps. - RteFiles.SaveUpload : 4,000,000 octets, avec une réponse 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>Les deux valent 5,242,880 octets, écrits une fois en 5120 Ko et une fois en octets. Tout ce qui se situe entre 4,000,000 octets et environ 5.2 Mo atteint l’action et reçoit son message spécifique.
La surprise : [HttpPost] lit le corps
Nous avons relevé maxAllowedContentLength à 50,000,000, pour que la limite d’ASP.NET s’applique en premier, et entouré l’accès au fichier dans l’action d’un try/catch. Un envoi de 6 Mo a quand même renvoyé une erreur HTML 500 « Maximum request length exceeded. ». La trace de pile montre pourquoi :
[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] cherche un champ de formulaire X-HTTP-Method-Override pendant que MVC choisit encore l’action. Lire Request.Form lit tout le corps, et c’est là que maxRequestLength lève l’exception. Aucune action ni aucun filtre d’action ne s’est encore exécuté. Application_Error la voit quand même :
// 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.");
}
}Avec ce code en place, le même envoi de 6 Mo a répondu 413 {"error":"The upload is larger than the server allows."}.
Le 413.1 propre à IIS
Avec les valeurs fournies, un envoi de 6 Mo n’atteint jamais ASP.NET. Le filtrage des requêtes d’IIS répond 413.1 « Request Entity Too Large » avec une page HTML. (IIS 7 et 8 utilisaient 404.13 dans ce cas, et l’exemple accepte l’un comme l’autre.) Application_EndRequest, dans la même région ci-dessus, s’exécute quand même pour cette requête. RteFiles.RewriteTooLarge vérifie le sous-statut, si bien que les messages 413 propres à l’action ne sont pas touchés :
// 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.");
}
}Sur votre propre machine, cela suffit. Le mode par défaut d’IIS, errorMode="DetailedLocalOnly", montre des pages d’erreur personnalisées aux visiteurs distants. Sur l’exemple Web Forms, qui a le même web.config, nous avons défini errorMode="Custom" pour simuler un visiteur distant. IIS a alors remplacé malgré tout le JSON par sa propre page « request entity is too large ». Ce bloc location laisse passer le JSON sur /rte/ uniquement :
<!-- /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>Tester
Avec le projet d’exemple de ce guide en cours d’exécution sur le port 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."}Les deux réponses sont des HTTP 413 en JSON. La première vient de RteFiles et la seconde d'Application_EndRequest, après le 413.1 d’IIS. Pour observer le cas de la sélection de l’action, relevez maxAllowedContentLength au-dessus de 5,242,880 et envoyez de nouveau huge.jpg. C’est Application_Error qui répond.
Le même guide pour d’autres plateformes
JavaScript · Next.js · PHP · ASP.NET Core · Web Forms