Benutzerdefinierte ASP.NET MVC-Fehlerbehandlung Application_Error Global.asax?

107

Ich habe einen grundlegenden Code, um Fehler in meiner MVC-Anwendung zu ermitteln. Zur Zeit in meinem Projekt habe ich einen Controller namens Errormit Aktionsmethoden HTTPError404(), HTTPError500()und General(). Sie alle akzeptieren einen String-Parameter error. Verwenden oder Ändern des folgenden Codes. Was ist der beste / richtige Weg, um die Daten zur Verarbeitung an den Fehlercontroller zu übergeben? Ich hätte gerne eine möglichst robuste Lösung.

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
quelle

Antworten:

103

Anstatt eine neue Route dafür zu erstellen, können Sie einfach zu Ihrem Controller / Ihrer Aktion umleiten und die Informationen per Querystring weitergeben. Zum Beispiel:

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));
    }

Dann erhält Ihr Controller, was Sie wollen:

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

Es gibt einige Kompromisse bei Ihrem Ansatz. Seien Sie sehr, sehr vorsichtig mit Schleifen bei dieser Art der Fehlerbehandlung. Eine andere Sache ist, dass Sie, da Sie die asp.net-Pipeline durchlaufen, um einen 404 zu verarbeiten, ein Sitzungsobjekt für all diese Treffer erstellen. Dies kann ein Problem (Leistung) für stark genutzte Systeme sein.

andrecarlucci
quelle
Was genau meinen Sie, wenn Sie "Vorsicht beim Schleifen" sagen? Gibt es eine bessere Möglichkeit, mit dieser Art der Fehlerumleitung umzugehen (vorausgesetzt, es handelt sich um ein stark genutztes System)?
aherrick
4
Mit Schleife meine ich, wenn Sie einen Fehler auf Ihrer Fehlerseite haben, werden Sie immer wieder auf Ihre Fehlerseite umgeleitet ... (zum Beispiel möchten Sie Ihren Fehler in einer Datenbank protokollieren und er ist ausgefallen).
andrecarlucci
125
Das Umleiten bei Fehlern widerspricht der Architektur des Webs. Der URI sollte unverändert bleiben, wenn der Server auf den richtigen HTTP-Statuscode antwortet, damit der Client den genauen Kontext des Fehlers kennt. Die Implementierung von HandleErrorAttribute.OnException oder Controller.OnException ist eine bessere Lösung. Und wenn diese fehlschlagen, führen Sie einen Server.Transfer ("~ / Error") in Global.asax durch.
Asbjørn Ulsberg
1
@ Chris, es ist akzeptabel, aber keine bewährte Methode. Zumal es häufig in eine Ressourcendatei umgeleitet wird, die mit einem HTTP 200-Statuscode versehen ist, sodass der Client glauben kann, dass alles in Ordnung war.
Asbjørn Ulsberg
1
Ich musste <httpErrors errorMode = "Detailed" /> zur web.config hinzufügen, damit dies auf dem Server funktioniert.
Jeroen K
28

So beantworten Sie die erste Frage: "Wie werden Routedaten ordnungsgemäß an den Fehlercontroller übergeben?":

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

Implementieren Sie dann in Ihrer ErrorController-Klasse eine Funktion wie die folgende:

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

Dadurch wird die Ausnahme in die Ansicht verschoben. Die Ansichtsseite sollte wie folgt deklariert werden:

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

Und der Code zur Anzeige des Fehlers:

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

Hier ist die Funktion, die alle Ausnahmemeldungen aus dem Ausnahmebaum sammelt:

    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
quelle
9

Ich habe eine Lösung für das von Lion_cl festgestellte Ajax-Problem gefunden.

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ý
quelle
Bei der Implementierung wurde der folgende Fehler angezeigt: 'System.Web.HttpRequest' enthält keine Definition für 'IsAjaxRequest'. Dieser Artikel hat eine Lösung: stackoverflow.com/questions/14629304/…
Julian Dormon
8

Ich hatte zuvor Probleme mit der Idee, eine globale Fehlerbehandlungsroutine in einer MVC-App zu zentralisieren. Ich habe einen Beitrag in den ASP.NET-Foren .

Grundsätzlich werden alle Ihre Anwendungsfehler in der Datei global.asax behandelt, ohne dass ein Fehlercontroller erforderlich ist, der mit dem [HandlerError]Attribut dekoriert oder mit dem customErrorsKnoten in der Datei web.config herumgespielt hat.

Jack Hsu
quelle
6

Eine bessere Möglichkeit, Fehler in MVC zu behandeln, besteht darin, das HandleError-Attribut auf Ihren Controller oder Ihre Aktion anzuwenden und die Datei Shared / Error.aspx zu aktualisieren, um das zu tun, was Sie möchten. Das Model-Objekt auf dieser Seite enthält eine Exception-Eigenschaft sowie ControllerName und ActionName.

Brian
quelle
1
Wie werden Sie dann mit einem 404Fehler umgehen ? da dafür kein Controller / keine Aktion vorgesehen ist?
Dementic
Die akzeptierte Antwort enthält 404s. Dieser Ansatz ist nur für 500 Fehler nützlich.
Brian
Vielleicht sollten Sie das in Ihrer Antwort bearbeiten. Perhaps a better way of handling errorsklingt ziemlich nach All Errors und nicht nur nach 500.
Dementic
4

Application_Error hat ein Problem mit Ajax-Anforderungen. Wenn ein Fehler in einer von Ajax aufgerufenen Aktion behandelt wird, wird Ihre Fehleransicht im resultierenden Container angezeigt.

Victor Gelmutdinov
quelle
4

Dies ist möglicherweise nicht der beste Weg für MVC ( https://stackoverflow.com/a/9461386/5869805 ).

Im Folgenden wird beschrieben, wie Sie eine Ansicht in Application_Error rendern und in die http-Antwort schreiben. Sie müssen die Umleitung nicht verwenden. Dadurch wird eine zweite Anforderung an den Server verhindert, sodass der Link in der Adressleiste des Browsers unverändert bleibt. Dies kann gut oder schlecht sein, es hängt davon ab, was Sie wollen.

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.
}

Aussicht

@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
quelle
Dies ist der beste Weg IMO. Genau das, wonach ich gesucht habe.
Steve Harris
@SteveHarris froh, dass es geholfen hat! :)
Burkay
3

Brian, Dieser Ansatz eignet sich hervorragend für Nicht-Ajax-Anforderungen. Wenn jedoch während eines Ajax-Aufrufs ein Fehler auftritt, wird Ihre Share / Error.aspx-Ansicht (oder Ihre benutzerdefinierte Fehlerseitenansicht) an den Ajax-Aufrufer zurückgegeben. -Der Benutzer wird NICHT zur Fehlerseite weitergeleitet.

unbestreitbarrob
quelle
0

Verwenden Sie den folgenden Code zum Umleiten auf der Routenseite. Verwenden Sie exception.Message instide of exception. Die Coz-Ausnahme-Abfragezeichenfolge gibt einen Fehler aus, wenn sie die Länge des Querystrings erweitert.

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

Ich habe ein Problem mit diesem Fehlerbehandlungsansatz: Im Fall von web.config:

<customErrors mode="On"/>

Der Fehlerbehandler durchsucht die Ansicht Error.shtml und den Kontrollflussschritt erst nach einer Ausnahme in Application_Error global.asax

System.InvalidOperationException: Die Ansicht 'Error' oder ihr Master wurde nicht gefunden oder keine View Engine unterstützt die gesuchten Speicherorte. Die folgenden Speicherorte wurden durchsucht: ~ / 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 bei System.Web.Mvc.ViewResult.FindView (ControllerContext-Kontext) ........ ............

So

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

httpException ist immer null, dann customErrors mode = "On" :( Es ist irreführend. Dann <customErrors mode="Off"/>oder <customErrors mode="RemoteOnly"/>die Benutzer sehen customErrors html. Then customErrors mode = "On". Dieser Code ist ebenfalls falsch


Ein weiteres Problem dieses Codes ist das

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

Rückgabeseite mit Code 302 statt echtem Fehlercode (402.403 usw.)

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