Gestion des erreurs en ASP.NET MVC

Standard

Article publié dans le magazine Programmez ! – Avril 2014 – N° 173

Lors de la conception d’une application web, gérer le comportement du système en cas d’erreur est important et ne devrait pas être laissé au hasard. D’une part, visualiser une page système peu accueillante, non stylisée, et loin des couleurs du site n’est pas toujours agréable pour l’utilisateur qui vient en plus de recevoir une erreur. D’autre part, si certains paramétrages ne sont pas effectués, l’utilisateur pourrait avoir accès aux détails techniques de l’exception. Un visiteur mal intentionné pourrait se servir de ces détails pour accumuler des connaissances sur la structure technique du site et trouver plus facilement une faille de sécurité à exploiter. Ainsi, il appartient à l’équipe de réalisation du site de déterminer en amont le niveau de détails d’erreur à délivrer à l’utilisateur, les codes d’erreur HTTP à renvoyer en fonction des situations et les pages d’erreur qui seront affichées. En somme, il s’agit de décider d’un comportement applicatif en cas d’erreur qui soit uniformément suivi au cours des développements.

ASP.NET MVC offre de nombreuses solutions pour gérer une exception de manière centralisée et rediriger vers une ressource en particulier. Gérer les erreurs peut donc se faire de plusieurs façons et peut s’adapter à divers scénarios. Ces différentes manières seront exposées au cours de cet article, du niveau le plus global à celui le plus fin.

Définir les url à appeler dans le fichier de configuration

La balise customErrors de la section system.web du fichier web.config situé à la racine du projet permet de définir des URLS de prise en charge lors d’exceptions :

<customErrors defaultRedirect="~/Error/" mode="On">
      <error redirect="~/Error/Index/404" statusCode="404"/>
      <error redirect="~/Error/Index/403" statusCode="403"/>
</customErrors>

Il s’agit ici d’URLS correspondant à la table de routage suivante, présente par défaut lors de la création d’un projet :

routes.MapRoute(name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });

Par défaut, le mode de la balise customErrors ne possède pas la valeur On mais la valeur RemoteOnly, ce qui signifie que tous les appels au site en localhost peuvent accéder aux détails techniques de l’exception mais que les appels distants seront bien redirigés vers les URLS spécifiées. Dans la configuration ci-dessus, les exceptions de type HttpException, de code 404 et 403, possèdent un traitement spécial et des URLS de redirection spécifique leur sont assignées par l’attribut redirect. Si le contrôleur et l’action spécifiés sont toujours les mêmes, Error et Index, un paramètre indiquant le code d’erreur est ajouté. Des paramètres supplémentaires auraient bien évidemment pu être spécifiés. Il aurait été bien sûr possible de rediriger vers des fichiers physiques, des webforms par exemple, d’extension *.aspx ou vers toute autre ressource.

Un des attributs de la balise customErrors, redirectMode, permet de changer la façon dont la requête en erreur est traitée, il attend une valeur de l’énumération de type CustomErrorsRedirectMode. Par défaut, lorsque cet attribut n’est pas défini, il possède la valeur ResponseRedirect, c’est-à-dire que le serveur renvoie au client un code HTTP 302 indiquant par convention que la ressource demandée se trouve à une autre URL, spécifiée dans l’en-tête de réponse Location. La ressource spécifiée dans cet en-tête sera alors l’URL de la page d’erreur. Dans l’exemple ci-dessus, si l’application ASP.NET MVC était déployée sur un sous-site de nom « ErrorHandlingSample » et que l’exception rencontrée était une HttpException de code 404, alors l’en-tête de réponse Location possèderait la valeur « /ErrorHandlingSample/Error/Index/404?aspxerrorpath=/ErrorHandlingSample/ » . A partir de cet en-tête, le navigateur est alors invité à effectuer une seconde requête vers cette URL.

Pour éviter une redirection, l’attribut redirectMode  pourrait être affecté à une autre valeur : ResponseRewrite, ce qui équivaut à faire un appel à la méthode Server.Transfer disponible sur la classe HttpServerUtility, depuis la version 5 de IIS. Dès qu’une exception non gérée est rencontrée, la requête cesse d’être traitée par le gestionnaire HTTP courant pour être transférée, elle ainsi que toutes ses variables, vers une page aspx ou vers toute autre ressource statique. Ce mode n’est alors pas compatible avec le moteur de routes d’ASP.NET MVC, le chemin spécifié doit mener vers un fichier physique, la requête ne passera pas par le moteur de routage une nouvelle fois. De fait, si la valeur ResponseRewrite permet d’économiser une redirection client et donc une seconde requête, elle ne permet pas de passer de nouveau dans un contrôleur et une action spécifiques MVC comme avec le mode ResponseRedirect.

Ainsi, cette gestion d’erreur via le web.config peut être utile à un niveau global et s’adapte facilement à des besoins simples :

  •  Aucun code n’est à écrire, un simple référencement d’URL en fonction des codes d’erreur HTTP est à effectuer dans le web.config ;
  • Cette méthode permet une redirection par défaut pour toutes les erreurs et une spécification d’URLS en fonction de certains codes HTTP ;
  • Le mode RemoteOnly permet aux développeurs ou aux administrateurs du serveur de voir les erreurs détaillées tandis que les utilisateurs ont automatiquement accès aux pages d’erreur spécifiées ;

Cette méthode présente cependant certains inconvénients :

  • Elle n’est pas compatible avec le paradigme ASP.NET MVC à moins d’opérer une redirection et donc une deuxième requête ;
  • Le code de retour HTTP sera 200 par défaut, le code HTTP de l’erreur originelle n’est pas préservé ou il faut l’affecter manuellement, dans la webform par exemple, pour que le navigateur reçoive un code d’erreur HTTP adapté ;
  • Lorsque le mode redirectMode est affecté à ResponseRedirect, il n’est plus possible d’accéder aux détails de l’exception et d’exposer la raison de l’erreur à l’utilisateur de manière plus détaillée ;
  • Lorsque l’attribut redirectMode est affecté à ResponseRedirect, un querystring est ajouté, « ?aspxerrorpath= »,pour indiquer la page à l’origine de l’erreur et il n’est pas possible de le retirer ;

Ainsi, pour une gestion davantage personnalisée des erreurs, le développeur pourra se tourner vers une manière plus manuelle de gérer ses erreurs en s’abonnant à l’événement global d’erreur au niveau du fichier global.asax de l’application web.

→  Si aucun mécanisme de gestion d’erreurs alternatif n’a été mis en place, il est fortement recommandé de ne pas désactiver les customErrors. En effet, sans mécanisme de repli, l’utilisateur risque d’avoir très facilement accès à tous les détails techniques des erreurs de l’application.

Gérer les erreurs au niveau du global.asax

Lorsqu’une exception non gérée se produit au niveau de l’application web, il est possible de la gérer dans le fichier Global.asax.cs, en y ajoutant cet event handler :


private void Application_Error(object sender, EventArgs e){}

 

Cette méthode sera alors appelée automatiquement en cas d’exception non gérée par l’application. Elle permet d’intercepter un maximum d’erreurs. Il peut s’agir d’exceptions qui se seraient produites dans les actions des contrôleurs mais aussi d’exceptions ayant lieu avant même qu’une méthode ne soit appelée sur un contrôleur, par exemple lorsqu’une route n’est pas trouvée dans la table de routage et ne peut être résolue, ou bien lorsque le model binder a rencontré un problème et n’a pu déserialiser les bons paramètres.

La méthode Server.GetLastError permet alors de récupérer les détails de l’exception. Après traitement, et pour s’assurer qu’aucun module ne prendra ensuite en charge cette erreur, il est préférable d’effectuer un appel à Server.ClearError.

Ainsi, dans cette méthode, l’exception pourrait être analysée, et le contrôleur responsable de la gestion des erreurs pourrait être appelé en conséquence, avec les bons paramètres :


private void Application_Error(object sender, EventArgs e)

       {
//don't break native feature from web.config custom errors
 var configurationError = WebConfigurationManager.GetWebApplicationSection("system.web/customErrors") as CustomErrorsSection;
if(configurationError.Mode == CustomErrorsMode.Off || (configurationError.Mode == CustomErrorsMode.RemoteOnly && Request.IsLocal))
{
return;
}
//avoid iis trapping errors before us
Response.TrySkipIisCustomErrors = true;
           var exc = Server.GetLastError(); //obtenir l'exception d'origine

           int httpCode = 500; //code par défaut, le plus général possible

           if (exc isHttpException || exc.GetBaseException() isHttpException)

           {

   var httpException = exc asHttpException ?? exc.GetBaseException() as    HttpException;

               httpCode = httpException.GetHttpCode();

           }

           Request.RequestContext.RouteData.Values.Clear();

           Request.RequestContext.RouteData.Values.Add("controller", "Error");

           Request.RequestContext.RouteData.Values.Add("action", "Index");

           Request.RequestContext.RouteData.Values.Add("id", httpCode);

           var controller = newErrorController() asIController;

           controller.Execute(Request.RequestContext);

           Server.ClearError();

       }

Ci-dessus, l’action Index du contrôleur Error est exécutée et c’est elle qui a la responsabilité de délivrer une vue d’erreur à l’utilisateur. Le contrôleur Error contiendrait alors seulement :


public ActionResult Index(int? id)

       {

           var errorCode = id.GetValueOrDefault(500);

           ErrorModel model = GetModelAndSetStatusCodeFromErrorCode(errorCode);

           Response.StatusCode = errorCode;

           return View(model);

       }

       privateErrorModel GetModelAndSetStatusCodeFromErrorCode(int errorCodeIn)

       {

           var errorCode = 500;

           var message = "Une erreur interne est survenue.";

           switch (errorCodeIn)

           {

               case 404:

                   {

                       message = "La ressource est introuvable";

                       errorCode = 404;

                       break;

                   }

               case 401:

                  {

                       message = "Vous n'êtes pas autorisé à accéder à cette ressource, demandez les droits à l'administrateur";

                       errorCode = 401;

                       break;

                   }

               case 403:

                   {

                       message = "L'accès à cette ressource est interdit.";

                       errorCode = 403;

                       break;

                   }

           }

           Response.StatusCode = errorCode;

           returnnewErrorModel(errorCode, message);

       }

 

Dans la méthode qui renvoie un modèle d’erreur, le code HTTP de la réponse est affecté. Une seule requête est donc effectuée, l’URL n’a pas changé, et le navigateur reçoit bien un code d’erreur adapté :

img1

La gestion de l’exception aurait pu être encore plus fine, en faisant par exemple correspondre des codes d’erreur HTTP à des exceptions métier ou en passant d’autres paramètres à l’action Index, ou encore, en appelant des actions différentes qui renverraient des vues spécialisées pour chaque cas.

→ Attention, le mécanisme de gestion d’erreur d’ASP.NET ne prend effet que lorsque la requête a été interceptée par le processus d’ASP.NET qui va tenter d’y répondre. Si l’utilisateur demande un simple fichier html, il sera directement pris en charge par IIS et son module StaticFileModule, qui renverra une page d’erreur système. Pour éviter toute page d’erreur système et délivrer une page d’erreur personnalisée, quelle que soit la requête cliente, il est possible de rajouter la ligne suivante dans le web.config, dans la section webserver, pour que tous les modules de code managé soient exécutés même si la requête arbore une extension qui ne concerne a priori pas de code managé :


   <modules runAllManagedModulesForAllRequests="true"></modules>

 

Cependant, ajouter cette ligne implique que pour toute requête, y compris des requêtes très simples visant une ressource basique accessible par un chemin physique sur le serveur, tous les modules managés seront exécutés afin de vérifier qu’aucun d’entre eux ne peut prendre la requête en charge. Le processus ASP.NET aura alors la main sur la requête. Mais cela est susceptible d’entraîner une perte de performances si de nombreuses requêtes qui ne nécessitent pas l’exécution de code managé sont exécutées. Le développeur peut alors se tourner vers d’autres solutions, en développant un handler HTTP personnalisé qui traiterait certaines extensions par exemple.

 

Gérer les erreurs avec des filtres d’erreur au niveau contrôleur

Bien que la marge de manœuvre offerte par la méthode ci-dessus soit assez large, lors d’une exception dans une action, le développeur peut vouloir récolter des informations uniquement disponibles dans le contexte ASP.NET MVC. Par exemple, il peut vouloir savoir si l’action était de type enfant (appelée dans le cadre d’une autre action, dite principale), afin de déterminer s’il faut renvoyer une vue d’erreur comprenant le layout ou une vue d’erreur partielle. Or, la méthode permettant de savoir s’il s’agit d’une action enfant est accessible depuis le contexte du contrôleur uniquement :


ControllerContext.IsChildAction();

De nombreuses autres informations importantes ne sont accessibles que depuis le contexte du contrôleur. Ainsi, pour accéder au contexte du contrôleur dans lequel l’exception s’est produite, un filtre d’erreur peut être appliqué à certaines actions en y apposant l’attribut, ou bien, à toutes les actions de l’application en enregistrant le filtre dans les GlobalFilters,dans la méthode Application_Start du global.asax. Par défaut, les modèles de projet ASP.NET MVC embarquent un filtre d’erreur nommé HandleErrorAttribute, ajouté aux filtres globaux de l’application, dans la classe FilterConfig :

 

public static void RegisterGlobalFilters(GlobalFilterCollection filters)

{

filters.Add(newHandleErrorAttribute());

}

Il s’agit d’un attribut car il hérite de la classe FilterAttribute et il est dédié à la gestion des exceptions car il hérite de l’interface IExceptionFilter en implémentant la méthode OnException, appelée lorsqu’une exception non gérée se produit. L’implémentation offerte par HandleErrorAttribute ne gère que les exceptions HTTP de code 500, ne concernant pas les actions enfant. Si aucune vue n’est spécifiée lors de la déclaration de l’attribut, par défaut, l’attribut cherche une vue de nom Error, soit dans le dossier concernant le contrôleur courant, soit dans le dossier Shared. Le nom de la vue peut être spécifié comme suit :

[HandleError(View = "MyErrorView" )]

public ActionResult Index()

{}

 

Le développeur peut cependant proposer une implémentation personnalisée en héritant de HandleErrorAttribute et en surchargeant sa méthode virtual, OnException. Il reçoit en paramètre un type intéressant : ExceptionContext, qui hérite de ControllerContext. Il s’agit donc bien de recueillir des informations concernant le contrôleur et l’action à l’origine de l’exception. De plus, ce paramètre permet directement, par certaines de ses propriétés accessibles en écriture, de gérer la réponse et d’indiquer un résultat via sa propriété Result, laquelle attend un type ActionResult. Avant tout traitement, il convient d’effacer la réponse précédemment construite, pour la remplir selon l’erreur :

 filterContext.HttpContext.Response.Clear();

Via le paramètre ExceptionContext et une fois l’exception traitée et le résultat affecté, il est préférable de marquer explicitement l’exception comme gérée. De la sorte, si une gestion globale des erreurs existe au niveau du global.asax, celle-ci ne viendra pas interférer avec le traitement effectué par l’attribut. Le développeur peut également activer la propriété TrySkipIisCustomErrorssur laréponse HTTP pour s’assurer que le mécanisme des customErrors vu précédemment ne prenne pas le pas :

filterContext.ExceptionHandled = true;

filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;

Voici par exemple une implémentation de la méthode OnException. Dans cet exemple, le développeur vérifie s’il s’agit d’une requête Ajax ou d’une action enfant qui a causé l’exception pour savoir s’il doit retourner un ViewResult, c’est-à-dire une page entière avec le layout de base compris, ou une page d’erreur partielle, sans layout, qui viendrait s’insérer dans le DOM de la page web :

public override void OnException(ExceptionContext filterContext)

{

string controllerName = (string)filterContext.RouteData.Values["controller"];

string actionName = (string)filterContext.RouteData.Values["action"];

HandleErrorInfo model = newHandleErrorInfo(filterContext.Exception, controllerName, actionName);

var viewName = "Error";

var viewData = newViewDataDictionary<HandleErrorInfo>(model);

var tempData = filterContext.Controller.TempData;

if (filterContext.IsChildAction || filterContext.HttpContext.Request.IsAjaxRequest())

{

filterContext.Result = newPartialViewResult

{

ViewName = viewName,

ViewData = viewData,

TempData = tempData

};

}

else

{

filterContext.Result = newViewResult

{

ViewName = viewName,

MasterName = Master,

ViewData = viewData,

TempData = tempData

};

}

int httpCode = 500; //code par défaut, le plus général possible

var exception = filterContext.Exception;

if (exception isHttpException || exception.GetBaseException() isHttpException)

{

var httpException = exception asHttpException ?? exception.GetBaseException() asHttpException;

httpCode = httpException.GetHttpCode();

}

filterContext.HttpContext.Response.Clear();

filterContext.ExceptionHandled = true;

filterContext.HttpContext.Response.StatusCode = httpCode;

filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;

}

Tout comme pour la gestion des erreurs dans le global.asax, une seule requête est effectuée et un code d’erreur HTTP adapté peut être renvoyé. Les deux systèmes restent bien sûr totalement compatibles et collaborent efficacement.

Cet article a ainsi introduit plusieurs manières de gérer les erreurs en ASP.NET MVC. Elles ne sont pas exclusives les unes des autres et peuvent au contraire s’avérer complémentaires. Tandis que l’attribut d’erreur personnalisé, apposé globalement à toutes les actions ou visant certaines actions en particulier, permet une gestion plus fine de l’exception et de la réponse à l’utilisateur, l’événement Application_Error du global.asax permet de ne laisser passer aucune exception non gérée, dans le cadre d’ASP.NET, et s’adresse aux développeurs soucieux d’avoir un contrôle total sur les réponses effectuées au client en cas d’erreur. Par ailleurs, cela permet de logguer facilement les erreurs ou de tracer certaines informations précises. Pour ceux qui attendent au contraire un moyen simple et rapide de fournir des pages d’erreurs statiques et sans complexité, le système des customErrors du web.config, notamment en mode RewriteResponse, peut s’avérer idéal.

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s