Come posso registrare TUTTE le eccezioni a livello globale per un'app WebAPI C # MVC4?


175

sfondo

Sto sviluppando un livello di servizio API per un client e mi è stato richiesto di rilevare e registrare tutti gli errori a livello globale.

Quindi, mentre qualcosa come un endpoint (o un'azione) sconosciuto può essere facilmente gestito utilizzando ELMAH o aggiungendo qualcosa di simile a Global.asax:

protected void Application_Error()
{
     Exception unhandledException = Server.GetLastError();
     //do more stuff
}

. . Gli errori .unhandled che non sono correlati al routing non vengono registrati. Per esempio:

public class ReportController : ApiController
{
    public int test()
    {
        var foo = Convert.ToInt32("a");//Will throw error but isn't logged!!
        return foo;
    }
}

Ho anche provato a impostare l' [HandleError]attributo a livello globale registrando questo filtro:

filters.Add(new HandleErrorAttribute());

Ma anche questo non registra tutti gli errori.

Problema / Domanda

Come posso intercettare errori come quello generato chiamando /testsopra in modo da poterli registrare? Sembra che questa risposta dovrebbe essere ovvia, ma finora ho provato tutto ciò a cui riesco a pensare.

Idealmente, voglio aggiungere alcune cose alla registrazione degli errori, come l'indirizzo IP dell'utente richiedente, la data, l'ora e così via. Voglio anche essere in grado di inviare automaticamente un'e-mail allo staff di supporto quando si verifica un errore. Tutto ciò che posso fare se solo riesco a intercettare questi errori quando si verificano!

RISOLTO!

Grazie a Darin Dimitrov, la cui risposta ho accettato, l'ho capito. WebAPI non gestisce gli errori come un normale controller MVC.

Ecco cosa ha funzionato:

1) Aggiungi un filtro personalizzato al tuo spazio dei nomi:

public class ExceptionHandlingAttribute : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext context)
    {
        if (context.Exception is BusinessException)
        {
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent(context.Exception.Message),
                ReasonPhrase = "Exception"
            });

        }

        //Log Critical errors
        Debug.WriteLine(context.Exception);

        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
        {
            Content = new StringContent("An error occurred, please try again or contact the administrator."),
            ReasonPhrase = "Critical Exception"
        });
    }
}

2) Ora registra il filtro a livello globale nella classe WebApiConfig :

public static class WebApiConfig
{
     public static void Register(HttpConfiguration config)
     {
         config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{action}/{id}", new { id = RouteParameter.Optional });
         config.Filters.Add(new ExceptionHandlingAttribute());
     }
}

O puoi saltare la registrazione e decorare un singolo controller con l' [ExceptionHandling]attributo.


Ho lo stesso problema. Le eccezioni non gestite vengono catturate correttamente nell'attributo del filtro delle eccezioni, ma quando lancio una nuova eccezione non vengono catturate nell'attributo del filtro delle eccezioni, qualche idea al riguardo?
daveBM,

1
Le chiamate sconosciute del controller api come errori myhost / api / undefinedapicontroller non vengono ancora rilevate. Il codice del filtro Application_error e Exception non viene eseguito. Come catturarli anche?
Andrus,

1
La gestione degli errori globali è stata aggiunta a WebAPI v2.1. Vedere la mia risposta qui: stackoverflow.com/questions/17449400/...
DarrellNorton

1
Ciò non rileverà errori in alcune circostanze, come "risorsa non trovata" o errori in un costruttore di controller. Fare riferimento qui: aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/Elmah/…
Jordan Morris

Ciao Matt. Hai scritto la risposta come parte della domanda ma questa non è una buona pratica in SO. Qui le risposte dovrebbero essere separate dalla domanda. Potresti scriverlo come una risposta separata (puoi usare il pulsante blu "Rispondi alla tua domanda" in basso).
sashoalm,

Risposte:


56

Se l'API Web è ospitata all'interno di un'applicazione ASP.NET, l' Application_Errorevento verrà chiamato per tutte le eccezioni non gestite nel codice, incluso quello nell'azione di test che hai mostrato. Quindi tutto ciò che devi fare è gestire questa eccezione all'interno dell'evento Application_Error. Nel codice di esempio che hai mostrato stai gestendo solo un'eccezione di tipo HttpExceptionche ovviamente non è il caso del Convert.ToInt32("a")codice. Quindi assicurati di accedere e gestire tutte le eccezioni presenti:

protected void Application_Error()
{
    Exception unhandledException = Server.GetLastError();
    HttpException httpException = unhandledException as HttpException;
    if (httpException == null)
    {
        Exception innerException = unhandledException.InnerException;
        httpException = innerException as HttpException;
    }

    if (httpException != null)
    {
        int httpCode = httpException.GetHttpCode();
        switch (httpCode)
        {
            case (int)HttpStatusCode.Unauthorized:
                Response.Redirect("/Http/Error401");
                break;

            // TODO: don't forget that here you have many other status codes to test 
            // and handle in addition to 401.
        }
        else
        {
            // It was not an HttpException. This will be executed for your test action.
            // Here you should log and handle this case. Use the unhandledException instance here
        }
    }
}

La gestione delle eccezioni nell'API Web potrebbe essere eseguita a vari livelli. Ecco una detailed articlespiegazione delle diverse possibilità:

  • attributo filtro eccezioni personalizzato che potrebbe essere registrato come filtro globale eccezioni

    [AttributeUsage(AttributeTargets.All)]
    public class ExceptionHandlingAttribute : ExceptionFilterAttribute
    {
        public override void OnException(HttpActionExecutedContext context)
        {
            if (context.Exception is BusinessException)
            {
                throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
                {
                    Content = new StringContent(context.Exception.Message),
                    ReasonPhrase = "Exception"
                });
            }
    
            //Log Critical errors
            Debug.WriteLine(context.Exception);
    
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent("An error occurred, please try again or contact the administrator."),
                ReasonPhrase = "Critical Exception"
            });
        }
    }
  • invocatore di azioni personalizzate

    public class MyApiControllerActionInvoker : ApiControllerActionInvoker
    {
        public override Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, System.Threading.CancellationToken cancellationToken)
        {
            var result = base.InvokeActionAsync(actionContext, cancellationToken);
    
            if (result.Exception != null && result.Exception.GetBaseException() != null)
            {
                var baseException = result.Exception.GetBaseException();
    
                if (baseException is BusinessException)
                {
                    return Task.Run<HttpResponseMessage>(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)
                    {
                        Content = new StringContent(baseException.Message),
                        ReasonPhrase = "Error"
    
                    });
                }
                else
                {
                    //Log critical error
                    Debug.WriteLine(baseException);
    
                    return Task.Run<HttpResponseMessage>(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)
                    {
                        Content = new StringContent(baseException.Message),
                        ReasonPhrase = "Critical Error"
                    });
                }
            }
    
            return result;
        }
    }

Vorrei che fosse così semplice, ma l'errore non viene ancora colto. Ho aggiornato la domanda per evitare confusione. Grazie.
Matt Cashatt,

@MatthewPatrickCashatt, se questa eccezione non viene rilevata Application_Errornell'evento, ciò significa che qualche altro codice lo sta consumando prima. Ad esempio potresti avere alcuni handleErrorAttributes personalizzati, moduli personalizzati, ... Ci sono milioni di altri luoghi in cui le eccezioni possono essere catturate e gestite. Ma il posto migliore per farlo è l'evento Application_Error, perché è lì che finiranno tutte le eccezioni non gestite.
Darin Dimitrov,

Grazie ancora, ma non importa quale sia, l' /testesempio non viene colpito. Ho inserito un breakpoint sulla prima riga ( Exception unhandledException = . . .) ma non riesco a raggiungere quel breakpoint nello /testscenario. Se inserisco un indirizzo falso, tuttavia, il punto di interruzione viene colpito.
Matt Cashatt

1
@MatthewPatrickCashatt, hai perfettamente ragione. L' Application_Errorevento non è il posto corretto per gestire le eccezioni per l'API Web perché non verrà attivato in tutti i casi. Ho trovato un articolo molto dettagliato che spiega le varie possibilità per raggiungere questo obiettivo: weblogs.asp.net/fredriknormen/archive/2012/06/11/…
Darin Dimitrov

1
@Darin Dimitrov Le chiamate sconosciute del controller api come errori myhost / api / undefinedapi non vengono ancora rilevate. Il codice del filtro Application_error e Exception non viene eseguito. Come catturarli anche?
Andrus,

79

In aggiunta alle risposte precedenti.

Ieri, l'API Web ASP.NET 2.1 è stata ufficialmente rilasciata .
Offre un'altra opportunità per gestire le eccezioni a livello globale.
I dettagli sono riportati nel campione .

In breve, si aggiungono logger di eccezioni globali e / o gestore di eccezioni globali (solo uno).
Li aggiungi alla configurazione:

public static void Register(HttpConfiguration config)
{
  config.MapHttpAttributeRoutes();

  // There can be multiple exception loggers.
  // (By default, no exception loggers are registered.)
  config.Services.Add(typeof(IExceptionLogger), new ElmahExceptionLogger());

  // There must be exactly one exception handler.
  // (There is a default one that may be replaced.)
  config.Services.Replace(typeof(IExceptionHandler), new GenericTextExceptionHandler());
}

E la loro realizzazione:

public class ElmahExceptionLogger : ExceptionLogger
{
  public override void Log(ExceptionLoggerContext context)
  {
    ...
  }
}

public class GenericTextExceptionHandler : ExceptionHandler
{
  public override void Handle(ExceptionHandlerContext context)
  {
    context.Result = new InternalServerErrorTextPlainResult(
      "An unhandled exception occurred; check the log for more information.",
      Encoding.UTF8,
      context.Request);
  }
}

2
Questo ha funzionato perfettamente. Accedo e gestisco contemporaneamente (perché ottengo il logID e lo passo indietro in modo che l'utente possa aggiungere commenti), quindi sto impostando il risultato su un nuovo ResponseMessageResult. Questo mi ha infastidito per un po ', grazie.
Brett,

8

Perché ripensare ecc? Funziona e renderà lo stato di restituzione del servizio 500 ecc

public class LogExceptionFilter : ExceptionFilterAttribute
{
    private static readonly ILog log = LogManager.GetLogger(typeof (LogExceptionFilter));

    public override void OnException(HttpActionExecutedContext actionExecutedContext)
    {
        log.Error("Unhandeled Exception", actionExecutedContext.Exception);
        base.OnException(actionExecutedContext);
    }
}

2

hai pensato di fare qualcosa di simile a un filtro di azione dell'errore handle come

[HandleError]
public class BaseController : Controller {...}

è inoltre possibile creare una versione personalizzata [HandleError]con la quale è possibile scrivere informazioni di errore e tutti gli altri dettagli per accedere


Grazie, ma l'ho già impostato a livello globale. Pone lo stesso problema di cui sopra, non tutti gli errori vengono registrati.
Matt Cashatt

1

Avvolgi il tutto in un tentativo / cattura e registra l'eccezione non gestita, quindi passala. A meno che non ci sia un modo integrato migliore per farlo.

Ecco un riferimento Cattura tutte (gestite o non gestite) eccezioni

(modifica: oh API)


Per ogni evenienza, avrebbe bisogno di riproporre anche l'eccezione.
DigCamara

@DigCamara Siamo spiacenti, questo è ciò che intendevo per passarlo. gettare; dovrebbe gestirlo. Inizialmente ho detto "decidi se uscire o ricaricare", poi ho capito che aveva detto che era un'API. In tal caso, è meglio lasciare che l'App decida cosa vuole fare trasmettendolo.
Tim

1
Questa è una cattiva risposta perché comporterà un sacco di codice duplicato in ogni azione.
Jansky,
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.