ASP.NET MVC Custom Error Handling Application_Error Global.asax?

108

Mam podstawowy kod do określenia błędów w mojej aplikacji MVC. Obecnie w moim projekcie mam kontrolera nazywane Errormetodami działania HTTPError404(), HTTPError500()oraz General(). Wszystkie akceptują parametr łańcuchowy error. Używanie lub modyfikowanie poniższego kodu. Jaki jest najlepszy / właściwy sposób przekazania danych administratorowi błędów do przetwarzania? Chciałbym mieć rozwiązanie tak solidne, jak to tylko możliwe.

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
źródło

Odpowiedzi:

104

Zamiast tworzyć w tym celu nową trasę, możesz po prostu przekierować do kontrolera / akcji i przekazać informacje za pomocą zapytań. Na przykład:

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

Wtedy twój kontroler otrzyma wszystko, co chcesz:

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

Twoje podejście wiąże się z pewnymi kompromisami. Bądź bardzo ostrożny z zapętleniem w tego rodzaju obsłudze błędów. Inną rzeczą jest to, że ponieważ przechodzisz przez potok asp.net, aby obsłużyć 404, utworzysz obiekt sesji dla wszystkich tych trafień. Może to stanowić problem (wydajność) w przypadku intensywnie używanych systemów.

andrecarlucci
źródło
Kiedy mówisz „uważaj na pętle”, co dokładnie masz na myśli? Czy istnieje lepszy sposób na obsługę tego typu przekierowań błędów (zakładając, że był to system intensywnie używany)?
aherrick
4
Przez zapętlenie mam na myśli, że jeśli masz błąd na stronie błędu, będziesz przekierowywany na stronę błędu raz po raz ... (na przykład, chcesz zarejestrować swój błąd w bazie danych i jest wyłączony).
andrecarlucci
125
Przekierowywanie na błędy jest sprzeczne z architekturą sieci. Identyfikator URI powinien pozostać taki sam, gdy serwer odpowie na poprawny kod stanu HTTP, aby klient znał dokładny kontekst niepowodzenia. Lepszym rozwiązaniem jest implementacja HandleErrorAttribute.OnException lub Controller.OnException. A jeśli to się nie powiedzie, wykonaj Server.Transfer ("~ / Error") w Global.asax.
Asbjørn Ulsberg
1
@Chris, to dopuszczalne, ale nie najlepsza praktyka. Zwłaszcza, że ​​często jest przekierowywany do pliku zasobów, który jest obsługiwany z kodem stanu HTTP 200, co sprawia, że ​​klient wierzy, że wszystko poszło dobrze.
Asbjørn Ulsberg
1
Musiałem dodać <httpErrors errorMode = "Detailed" /> do pliku web.config, aby to działało na serwerze.
Jeroen K
28

Aby odpowiedzieć na wstępne pytanie „jak prawidłowo przekazać dane trasowane do kontrolera błędów?”:

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

Następnie w swojej klasie ErrorController zaimplementuj taką funkcję:

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

Spowoduje to umieszczenie wyjątku w widoku. Stronę widoku należy zadeklarować w następujący sposób:

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

A kod do wyświetlenia błędu:

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

Oto funkcja, która zbiera wszystkie komunikaty o wyjątkach z drzewa wyjątków:

    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
źródło
9

Znalazłem rozwiązanie problemu AJAX, które odnotował 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ý
źródło
Podczas wdrażania tego dostałem następujący błąd: „System.Web.HttpRequest” nie zawiera definicji dla „IsAjaxRequest”. Ten artykuł ma rozwiązanie: stackoverflow.com/questions/14629304/ ...
Julian Dormon
8

Wcześniej zmagałem się z pomysłem scentralizowania globalnej procedury obsługi błędów w aplikacji MVC. Mam post na forach ASP.NET .

W zasadzie obsługuje wszystkie błędy aplikacji w global.asax bez potrzeby używania kontrolera błędów, dekorowania [HandlerError]atrybutem lub majstrowania przy customErrorswęźle w web.config.

Jack Hsu
źródło
6

Być może lepszym sposobem obsługi błędów w MVC jest zastosowanie atrybutu HandleError do kontrolera lub akcji i zaktualizowanie pliku Shared / Error.aspx, aby robić to, co chcesz. Obiekt Model na tej stronie zawiera właściwość Exception, a także ControllerName i ActionName.

Brian
źródło
1
Jak więc poradzisz sobie z 404błędem? skoro nie ma kontrolera / akcji do tego wyznaczonej?
Dementic
Zaakceptowana odpowiedź zawiera 404. To podejście jest przydatne tylko w przypadku błędów 500.
Brian
Może powinieneś dodać to do swojej odpowiedzi. Perhaps a better way of handling errorsbrzmi prawie jak Wszystkie błędy, a nie tylko 500.
Dementic
4

Błąd aplikacji związany z żądaniami Ajax. Jeśli błąd został obsłużony w akcji, która została wywołana przez Ajax - wyświetli widok błędu wewnątrz powstałego kontenera.

Victor Gelmutdinov
źródło
4

To może nie być najlepszy sposób dla MVC ( https://stackoverflow.com/a/9461386/5869805 )

Poniżej przedstawiono sposób renderowania widoku w Application_Error i zapisywania go w odpowiedzi http. Nie musisz używać przekierowania. Zapobiegnie to drugiemu żądaniu do serwera, więc łącze w pasku adresu przeglądarki pozostanie niezmienione. To może być dobre lub złe, zależy od tego, czego chcesz.

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

Widok

@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
źródło
To najlepszy sposób IMO. Dokładnie to, czego szukałem.
Steve Harris
@SteveHarris cieszę się, że pomogło! :)
burkay
3

Brian, To podejście działa świetnie w przypadku żądań innych niż Ajax, ale jak stwierdził Lion_cl, jeśli wystąpi błąd podczas wywołania Ajax, widok Share / Error.aspx (lub niestandardowy widok strony błędu) zostanie zwrócony do dzwoniącego Ajax. -użytkownik NIE zostanie przekierowany na stronę błędu.

niezaprzeczalnie
źródło
0

Użyj następującego kodu do przekierowania na stronie trasy. Użyj wyjątku Komunikat zamiast wyjątku. Ciąg zapytania o wyjątek Coz powoduje błąd, jeśli rozszerza długość zapytania.

routeData.Values.Add("error", exception.Message);
// clear error on server
Server.ClearError();
Response.RedirectToRoute(routeData.Values);
Swapnil Malap
źródło
-1

Mam problem z tym podejściem do obsługi błędów: W przypadku pliku web.config:

<customErrors mode="On"/>

Procedura obsługi błędów przeszukuje widok Error.shtml, a przepływ sterowania przechodzi do Application_Error global.asax tylko po wystąpieniu wyjątku

System.InvalidOperationException: widok „Błąd” lub jego wzorzec nie został znaleziony lub żaden silnik widoku nie obsługuje wyszukiwanych lokalizacji. Przeszukano następujące lokalizacje: ~ / 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 at System.Web.Mvc.ViewResult.FindView (kontekst ControllerContext) ........ ............

Więc

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

httpException ma zawsze wartość null, a następnie customErrors mode = "On" :( Jest to mylące Wtedy <customErrors mode="Off"/>lub <customErrors mode="RemoteOnly"/>użytkownicy widzą customErrors html, Wtedy customErrors mode = "On" ten kod też jest nieprawidłowy


Kolejny problem z tym kodem

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

Wróć do strony z kodem 302 zamiast prawdziwego kodu błędu (402,403 itd.)

Александр Шмыков
źródło