Gestione degli errori personalizzata ASP.NET MVC Application_Error Global.asax?


108

Ho del codice di base per determinare gli errori nella mia applicazione MVC. Attualmente nel mio progetto ho un controller chiamato Errorcon i metodi di azione HTTPError404(), HTTPError500()e General(). Tutti accettano un parametro stringa error. Utilizzando o modificando il codice sottostante. Qual è il modo migliore / corretto per passare i dati al controller degli errori per l'elaborazione? Vorrei avere una soluzione il più robusta possibile.

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

Risposte:


104

Invece di creare un nuovo percorso per quello, potresti semplicemente reindirizzare al tuo controller / azione e passare le informazioni tramite querystring. Per esempio:

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

Quindi il tuo controller riceverà quello che vuoi:

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

Ci sono alcuni compromessi con il tuo approccio. Fai molta attenzione con il loop in questo tipo di gestione degli errori. Un'altra cosa è che poiché stai attraversando la pipeline asp.net per gestire un 404, creerai un oggetto sessione per tutti quegli hit. Questo può essere un problema (prestazioni) per i sistemi molto utilizzati.


Quando dici "fai attenzione al looping" cosa intendi esattamente? C'è un modo migliore per gestire questo tipo di reindirizzamento degli errori (supponendo che sia stato un sistema molto utilizzato)?
aherrick

4
Con il looping intendo che quando hai un errore nella tua pagina di errore, verrai reindirizzato alla tua pagina di errore ancora e ancora ... (ad esempio, vuoi registrare il tuo errore in un database ed è inattivo).
andrecarlucci

125
Il reindirizzamento sugli errori va contro l'architettura del web. L'URI dovrebbe rimanere lo stesso quando il server risponde al codice di stato HTTP corretto in modo che il client conosca il contesto esatto dell'errore. L'implementazione di HandleErrorAttribute.OnException o Controller.OnException è una soluzione migliore. E se falliscono, esegui un Server.Transfer ("~ / Error") in Global.asax.
Asbjørn Ulsberg,

1
@ Chris, è accettabile, ma non è la migliore pratica. Soprattutto perché viene spesso reindirizzato a un file di risorse servito con un codice di stato HTTP 200, che lascia al client la convinzione che tutto sia andato bene.
Asbjørn Ulsberg

1
Ho dovuto aggiungere <httpErrors errorMode = "Detailed" /> a web.config per fare in modo che funzionasse sul server.
Jeroen K

28

Per rispondere alla domanda iniziale "come passare correttamente i dati di instradamento al controller di errore?":

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

Quindi nella tua classe ErrorController, implementa una funzione come questa:

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

Questo spinge l'eccezione nella vista. La pagina di visualizzazione dovrebbe essere dichiarata come segue:

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

E il codice per visualizzare l'errore:

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

Ecco la funzione che raccoglie tutti i messaggi di eccezione dall'albero delle eccezioni:

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

9

Ho trovato una soluzione per il problema ajax segnalato da 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");
    }
}

Durante l'implementazione ho ricevuto il seguente errore: "System.Web.HttpRequest" non contiene una definizione per "IsAjaxRequest". Questo articolo ha una soluzione: stackoverflow.com/questions/14629304/...
Julian Dormon

8

Ho già lottato con l'idea di centralizzare una routine di gestione degli errori globale in un'app MVC. Ho un post sui forum ASP.NET .

Fondamentalmente gestisce tutti gli errori dell'applicazione nel global.asax senza la necessità di un controller degli errori, decorando con l' [HandlerError]attributo o armeggiando con il customErrorsnodo in web.config.


6

Forse un modo migliore per gestire gli errori in MVC è applicare l'attributo HandleError al controller o all'azione e aggiornare il file Shared / Error.aspx per fare ciò che si desidera. L'oggetto Model in quella pagina include una proprietà Exception, ControllerName e ActionName.


1
Come gestirai un 404errore allora? poiché non esiste un controller / azione designato per questo?
Demenza

La risposta accettata include 404. Questo approccio è utile solo per 500 errori.
Brian il

Forse dovresti modificarlo nella tua risposta. Perhaps a better way of handling errorssuona più o meno come Tutti gli errori e non solo 500.
Demenziale

4

Application_Error che ha problemi con le richieste Ajax. Se viene gestito un errore in Action richiamato da Ajax, verrà visualizzata la visualizzazione degli errori all'interno del contenitore risultante.


4

Questo potrebbe non essere il modo migliore per MVC ( https://stackoverflow.com/a/9461386/5869805 )

Di seguito è riportato come eseguire il rendering di una vista in Application_Error e scriverla nella risposta http. Non è necessario utilizzare il reindirizzamento. Ciò impedirà una seconda richiesta al server, quindi il collegamento nella barra degli indirizzi del browser rimarrà lo stesso. Questo può essere buono o cattivo, dipende da quello che vuoi.

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

Visualizza

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

Questo è il modo migliore IMO. Esattamente quello che stavo cercando.
Steve Harris

@SteveHarris contento che abbia aiutato! :)
burkay

3

Brian, questo approccio funziona alla grande per le richieste non Ajax, ma come affermato da Lion_cl, se si verifica un errore durante una chiamata Ajax, la visualizzazione Share / Error.aspx (o la visualizzazione della pagina di errore personalizzata) verrà restituita al chiamante Ajax- -l'utente NON verrà reindirizzato alla pagina di errore.


0

Usa il seguente codice per il reindirizzamento sulla pagina del percorso. Usa eccezione, messaggio al posto dell'eccezione. La stringa di query dell'eccezione Coz restituisce un errore se estende la lunghezza della stringa di query.

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

-1

Ho un problema con questo approccio di gestione degli errori: In caso di web.config:

<customErrors mode="On"/>

Il gestore degli errori sta cercando la vista Error.shtml e il flusso di controllo entra in Application_Error global.asax solo dopo l'eccezione

System.InvalidOperationException: la vista "Errore" o il suo master non è stato trovato o nessun motore di visualizzazione supporta le posizioni cercate. Sono state cercate le seguenti posizioni: ~ / 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 in System.Web.Mvc.ViewResult.FindView (contesto ControllerContext) ........ ............

Così

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

httpException è sempre nulla, quindi customErrors mode = "On" :( È fuorviante Allora <customErrors mode="Off"/>o <customErrors mode="RemoteOnly"/>gli utenti vedono customErrors html, Quindi customErrors mode = "On" anche questo codice è sbagliato


Un altro problema di questo codice è quello

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

Pagina di ritorno con codice 302 al posto del codice di errore reale (402,403 ecc.)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.