Gestion des erreurs personnalisées ASP.NET MVC Application_Error Global.asax?

108

J'ai un code de base pour déterminer les erreurs dans mon application MVC. À l' heure actuelle dans mon projet , j'ai appelé un contrôleur Erroravec des méthodes d'action HTTPError404(), HTTPError500()et General(). Ils acceptent tous un paramètre de chaîne error. Utilisation ou modification du code ci-dessous. Quelle est la meilleure / la manière appropriée de transmettre les données au contrôleur d'erreur pour traitement? J'aimerais avoir une solution la plus robuste possible.

protected void Application_Error(object sender, EventArgs e)
{
    Exception exception = Server.GetLastError();
    Response.Clear();

    HttpException httpException = exception as HttpException;
    if (httpException != null)
    {
        RouteData routeData = new RouteData();
        routeData.Values.Add("controller", "Error");
        switch (httpException.GetHttpCode())
        {
            case 404:
                // page not found
                routeData.Values.Add("action", "HttpError404");
                break;
            case 500:
                // server error
                routeData.Values.Add("action", "HttpError500");
                break;
            default:
                routeData.Values.Add("action", "General");
                break;
        }
        routeData.Values.Add("error", exception);
        // clear error on server
        Server.ClearError();

        // at this point how to properly pass route data to error controller?
    }
}
aherrick
la source

Réponses:

104

Au lieu de créer une nouvelle route pour cela, vous pouvez simplement rediriger vers votre contrôleur / action et transmettre les informations via une chaîne de requête. Par exemple:

protected void Application_Error(object sender, EventArgs e) {
  Exception exception = Server.GetLastError();
  Response.Clear();

  HttpException httpException = exception as HttpException;

  if (httpException != null) {
    string action;

    switch (httpException.GetHttpCode()) {
      case 404:
        // page not found
        action = "HttpError404";
        break;
      case 500:
        // server error
        action = "HttpError500";
        break;
      default:
        action = "General";
        break;
      }

      // clear error on server
      Server.ClearError();

      Response.Redirect(String.Format("~/Error/{0}/?message={1}", action, exception.Message));
    }

Ensuite, votre contrôleur recevra ce que vous voulez:

// GET: /Error/HttpError404
public ActionResult HttpError404(string message) {
   return View("SomeView", message);
}

Il y a des compromis avec votre approche. Soyez très prudent avec les boucles dans ce type de gestion des erreurs. Une autre chose est que puisque vous passez par le pipeline asp.net pour gérer un 404, vous allez créer un objet de session pour tous ces hits. Cela peut être un problème (performances) pour les systèmes très utilisés.

andrecarlucci
la source
Quand vous dites "faites attention au bouclage", que voulez-vous dire exactement? Existe-t-il une meilleure façon de gérer ce type de redirection d'erreur (en supposant qu'il s'agissait d'un système fortement utilisé)?
aherrick le
4
Par boucle, je veux dire que lorsque vous avez une erreur dans votre page d'erreur, vous serez redirigé vers votre page d'erreur encore et encore ... (par exemple, vous voulez enregistrer votre erreur dans une base de données et elle est en panne).
andrecarlucci
125
La redirection sur les erreurs va à l'encontre de l'architecture du web. L'URI doit rester le même lorsque le serveur répond au code d'état HTTP correct afin que le client connaisse le contexte exact de l'échec. L'implémentation de HandleErrorAttribute.OnException ou Controller.OnException est une meilleure solution. Et si cela échoue, faites un Server.Transfer ("~ / Error") dans Global.asax.
Asbjørn Ulsberg
1
@Chris, c'est acceptable, mais ce n'est pas la meilleure pratique. D'autant plus qu'il est souvent redirigé vers un fichier de ressources qui est servi avec un code d'état HTTP 200, ce qui laisse le client croire que tout s'est bien passé.
Asbjørn Ulsberg
1
J'ai dû ajouter <httpErrors errorMode = "Detailed" /> au web.config pour que cela fonctionne sur le serveur.
Jeroen K
28

Pour répondre à la question initiale "comment transmettre correctement les données de routage au contrôleur d'erreur?":

IController errorController = new ErrorController();
errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));

Ensuite, dans votre classe ErrorController, implémentez une fonction comme celle-ci:

[AcceptVerbs(HttpVerbs.Get)]
public ViewResult Error(Exception exception)
{
    return View("Error", exception);
}

Cela pousse l'exception dans la vue. La page d'affichage doit être déclarée comme suit:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<System.Exception>" %>

Et le code pour afficher l'erreur:

<% if(Model != null) { %>  <p><b>Detailed error:</b><br />  <span class="error"><%= Helpers.General.GetErrorMessage((Exception)Model, false) %></span></p> <% } %>

Voici la fonction qui rassemble tous les messages d'exception de l'arborescence d'exceptions:

    public static string GetErrorMessage(Exception ex, bool includeStackTrace)
    {
        StringBuilder msg = new StringBuilder();
        BuildErrorMessage(ex, ref msg);
        if (includeStackTrace)
        {
            msg.Append("\n");
            msg.Append(ex.StackTrace);
        }
        return msg.ToString();
    }

    private static void BuildErrorMessage(Exception ex, ref StringBuilder msg)
    {
        if (ex != null)
        {
            msg.Append(ex.Message);
            msg.Append("\n");
            if (ex.InnerException != null)
            {
                BuildErrorMessage(ex.InnerException, ref msg);
            }
        }
    }
Tim Cooper
la source
9

J'ai trouvé une solution pour le problème ajax noté par Lion_cl.

global.asax:

protected void Application_Error()
    {           
        if (HttpContext.Current.Request.IsAjaxRequest())
        {
            HttpContext ctx = HttpContext.Current;
            ctx.Response.Clear();
            RequestContext rc = ((MvcHandler)ctx.CurrentHandler).RequestContext;
            rc.RouteData.Values["action"] = "AjaxGlobalError";

            // TODO: distinguish between 404 and other errors if needed
            rc.RouteData.Values["newActionName"] = "WrongRequest";

            rc.RouteData.Values["controller"] = "ErrorPages";
            IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
            IController controller = factory.CreateController(rc, "ErrorPages");
            controller.Execute(rc);
            ctx.Server.ClearError();
        }
    }

ErrorPagesController

public ActionResult AjaxGlobalError(string newActionName)
    {
        return new AjaxRedirectResult(Url.Action(newActionName), this.ControllerContext);
    }

AjaxRedirectResult

public class AjaxRedirectResult : RedirectResult
{
    public AjaxRedirectResult(string url, ControllerContext controllerContext)
        : base(url)
    {
        ExecuteResult(controllerContext);
    }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context.RequestContext.HttpContext.Request.IsAjaxRequest())
        {
            JavaScriptResult result = new JavaScriptResult()
            {
                Script = "try{history.pushState(null,null,window.location.href);}catch(err){}window.location.replace('" + UrlHelper.GenerateContentUrl(this.Url, context.HttpContext) + "');"
            };

            result.ExecuteResult(context);
        }
        else
        {
            base.ExecuteResult(context);
        }
    }
}

AjaxRequestExtension

public static class AjaxRequestExtension
{
    public static bool IsAjaxRequest(this HttpRequest request)
    {
        return (request.Headers["X-Requested-With"] != null && request.Headers["X-Requested-With"] == "XMLHttpRequest");
    }
}
Jozef Krchňavý
la source
Lors de la mise en œuvre de cela, j'ai eu l'erreur suivante: «System.Web.HttpRequest» ne contient pas de définition pour «IsAjaxRequest». Cet article a une solution: stackoverflow.com/questions/14629304/…
Julian Dormon
8

J'ai eu du mal avec l'idée de centraliser une routine globale de gestion des erreurs dans une application MVC auparavant. J'ai un message sur les forums ASP.NET .

Il gère essentiellement toutes vos erreurs d'application dans le global.asax sans avoir besoin d'un contrôleur d'erreur, de décorer avec l' [HandlerError]attribut ou de jouer avec le customErrorsnœud dans le web.config.

Jack Hsu
la source
6

Une meilleure façon de gérer les erreurs dans MVC consiste peut-être à appliquer l'attribut HandleError à votre contrôleur ou à votre action et à mettre à jour le fichier Shared / Error.aspx pour faire ce que vous voulez. L'objet Model sur cette page comprend une propriété Exception ainsi que ControllerName et ActionName.

Brian
la source
1
Comment allez-vous gérer une 404erreur alors? puisqu'il n'y a pas de contrôleur / d'action désigné pour cela?
Dementic
La réponse acceptée comprend les 404. Cette approche n'est utile que pour 500 erreurs.
Brian
Vous devriez peut-être modifier cela dans votre réponse. Perhaps a better way of handling errorssonne à peu près comme Toutes les erreurs et pas seulement 500.
Dementic
4

Application_Error ayant un problème avec les requêtes Ajax. Si une erreur est gérée dans l'action qui a été appelée par Ajax - il affichera votre vue d'erreur dans le conteneur résultant.

Victor Gelmutdinov
la source
4

Ce n'est peut-être pas le meilleur moyen pour MVC ( https://stackoverflow.com/a/9461386/5869805 )

Voici comment rendre une vue dans Application_Error et l'écrire dans la réponse http. Vous n'avez pas besoin d'utiliser la redirection. Cela empêchera une deuxième demande au serveur, de sorte que le lien dans la barre d'adresse du navigateur restera le même. Cela peut être bon ou mauvais, cela dépend de ce que vous voulez.

Global.asax.cs

protected void Application_Error()
{
    var exception = Server.GetLastError();
    // TODO do whatever you want with exception, such as logging, set errorMessage, etc.
    var errorMessage = "SOME FRIENDLY MESSAGE";

    // TODO: UPDATE BELOW FOUR PARAMETERS ACCORDING TO YOUR ERROR HANDLING ACTION
    var errorArea = "AREA";
    var errorController = "CONTROLLER";
    var errorAction = "ACTION";
    var pathToViewFile = $"~/Areas/{errorArea}/Views/{errorController}/{errorAction}.cshtml"; // THIS SHOULD BE THE PATH IN FILESYSTEM RELATIVE TO WHERE YOUR CSPROJ FILE IS!

    var requestControllerName = Convert.ToString(HttpContext.Current.Request.RequestContext?.RouteData?.Values["controller"]);
    var requestActionName = Convert.ToString(HttpContext.Current.Request.RequestContext?.RouteData?.Values["action"]);

    var controller = new BaseController(); // REPLACE THIS WITH YOUR BASE CONTROLLER CLASS
    var routeData = new RouteData { DataTokens = { { "area", errorArea } }, Values = { { "controller", errorController }, {"action", errorAction} } };
    var controllerContext = new ControllerContext(new HttpContextWrapper(HttpContext.Current), routeData, controller);
    controller.ControllerContext = controllerContext;

    var sw = new StringWriter();
    var razorView = new RazorView(controller.ControllerContext, pathToViewFile, "", false, null);
    var model = new ViewDataDictionary(new HandleErrorInfo(exception, requestControllerName, requestActionName));
    var viewContext = new ViewContext(controller.ControllerContext, razorView, model, new TempDataDictionary(), sw);
    viewContext.ViewBag.ErrorMessage = errorMessage;
    //TODO: add to ViewBag what you need
    razorView.Render(viewContext, sw);
    HttpContext.Current.Response.Write(sw);
    Server.ClearError();
    HttpContext.Current.Response.End(); // No more processing needed (ex: by default controller/action routing), flush the response out and raise EndRequest event.
}

Vue

@model HandleErrorInfo
@{
    ViewBag.Title = "Error";
    // TODO: SET YOUR LAYOUT
}
<div class="">
    ViewBag.ErrorMessage
</div>
@if(Model != null && HttpContext.Current.IsDebuggingEnabled)
{
    <div class="" style="background:khaki">
        <p>
            <b>Exception:</b> @Model.Exception.Message <br/>
            <b>Controller:</b> @Model.ControllerName <br/>
            <b>Action:</b> @Model.ActionName <br/>
        </p>
        <div>
            <pre>
                @Model.Exception.StackTrace
            </pre>
        </div>
    </div>
}
Burkay
la source
C'est la meilleure façon IMO. Exactement ce que je cherchais.
Steve Harris
@SteveHarr est heureux que cela ait aidé! :)
burkay
3

Brian, Cette approche fonctionne très bien pour les requêtes non Ajax, mais comme Lion_cl l'a déclaré, si vous rencontrez une erreur lors d'un appel Ajax, votre vue Share / Error.aspx (ou votre vue de page d'erreur personnalisée) sera renvoyée à l'appelant Ajax- -l'utilisateur ne sera PAS redirigé vers la page d'erreur.

undeniablyrob
la source
0

Utilisez le code suivant pour la redirection sur la page d'itinéraire. Utilisez exception.Message instide of exception. La chaîne de requête d'exception Coz donne une erreur si elle étend la longueur de la chaîne de requête.

routeData.Values.Add("error", exception.Message);
// clear error on server
Server.ClearError();
Response.RedirectToRoute(routeData.Values);
Swapnil Malap
la source
-1

J'ai un problème avec cette approche de gestion des erreurs: Dans le cas de web.config:

<customErrors mode="On"/>

Le gestionnaire d'erreurs recherche la vue Error.shtml et l'étape de flux de contrôle dans Application_Error global.asax uniquement après une exception

System.InvalidOperationException: La vue "Erreur" ou son maître est introuvable ou aucun moteur de vue ne prend en charge les emplacements recherchés. Les emplacements suivants ont été recherchés: ~ / Views / home / Error.aspx ~ / Views / home / Error.ascx ~ / Views / Shared / Error.aspx ~ / Views / Shared / Error.ascx ~ / Views / home / Error. cshtml ~ / Views / home / Error.vbhtml ~ / Views / Shared / Error.cshtml ~ / Views / Shared / Error.vbhtml à System.Web.Mvc.ViewResult.FindView (contexte ControllerContext) ........ ............

Alors

 Exception exception = Server.GetLastError();
  Response.Clear();
  HttpException httpException = exception as HttpException;

httpException est toujours null alors customErrors mode = "On" :( C'est trompeur Alors <customErrors mode="Off"/>ou <customErrors mode="RemoteOnly"/>les utilisateurs voient customErrors html, Then customErrors mode = "On" ce code est également faux


Un autre problème de ce code qui

Response.Redirect(String.Format("~/Error/{0}/?message={1}", action, exception.Message));

Page de retour avec le code 302 à la place du code d'erreur réel (402, 403, etc.)

Александр Шмыков
la source