Comment puis-je enregistrer TOUTES les exceptions globalement pour une application WebAPI C # MVC4?

175

Contexte

Je développe une couche de service API pour un client et on m'a demandé de détecter et de consigner toutes les erreurs au niveau mondial.

Ainsi, alors que quelque chose comme un point de terminaison inconnu (ou une action) est facilement géré en utilisant ELMAH ou en ajoutant quelque chose comme ceci au Global.asax:

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

. . Les erreurs .unhandled qui ne sont pas liées au routage ne sont pas enregistrées. Par exemple:

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

J'ai également essayé de définir l' [HandleError]attribut globalement en enregistrant ce filtre:

filters.Add(new HandleErrorAttribute());

Mais cela n'enregistre pas non plus toutes les erreurs.

Problème / Question

Comment intercepter les erreurs comme celle générée en appelant /testci - dessus afin de pouvoir les consigner? Il semble que cette réponse devrait être évidente, mais j'ai essayé tout ce à quoi je pouvais penser jusqu'à présent.

Idéalement, je souhaite ajouter des éléments à la journalisation des erreurs, tels que l'adresse IP de l'utilisateur demandeur, la date, l'heure, etc. Je souhaite également pouvoir envoyer automatiquement un e-mail au personnel de support en cas d'erreur. Tout cela, je peux le faire si seulement je peux intercepter ces erreurs lorsqu'elles se produisent!

RÉSOLU!

Grâce à Darin Dimitrov, dont j'ai accepté la réponse, j'ai compris cela. WebAPI ne gère pas les erreurs de la même manière qu'un contrôleur MVC standard.

Voici ce qui a fonctionné:

1) Ajoutez un filtre personnalisé à votre espace de noms:

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) Enregistrez maintenant le filtre globalement dans la 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());
     }
}

OU vous pouvez ignorer l'enregistrement et simplement décorer un seul contrôleur avec l' [ExceptionHandling]attribut.

Matt Cashatt
la source
J'ai le même problème. Les exceptions non gérées sont interceptées dans l'attribut de filtre d'exception, mais lorsque je lance une nouvelle exception, elle n'est pas interceptée dans l'attribut de filtre d'exception, une idée à ce sujet?
daveBM
1
Les appels de contrôleur API inconnus tels que les erreurs myhost / api / undefinedapicontroller ne sont toujours pas détectés. Le code de filtre Application_error et Exception n'est pas exécuté. Comment les attraper aussi?
Andrus
1
La gestion globale des erreurs a été ajoutée à WebAPI v2.1. Voir ma réponse ici: stackoverflow.com/questions/17449400/…
DarrellNorton
1
Cela n'attrapera pas les erreurs dans certaines circonstances, comme «ressource introuvable» ou des erreurs dans un constructeur de contrôleur. Reportez-vous ici: aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/Elmah/…
Jordan Morris
Salut Matt. Vous avez écrit la réponse dans le cadre de la question, mais ce n'est pas une bonne pratique en matière de SO. Ici, les réponses doivent être séparées de la question. Pourriez-vous s'il vous plaît écrire cela comme une réponse distincte (vous pouvez utiliser le bouton bleu «Répondez à votre propre question» en bas).
sashoalm

Réponses:

56

Si votre API Web est hébergée dans une application ASP.NET, l' Application_Errorévénement sera appelé pour toutes les exceptions non gérées dans votre code, y compris celle de l'action de test que vous avez affichée. Il vous suffit donc de gérer cette exception dans l'événement Application_Error. Dans l'exemple de code que vous avez montré, vous ne gérez que des exceptions de type, HttpExceptionce qui n'est évidemment pas le cas avec le Convert.ToInt32("a")code. Assurez-vous donc de vous connecter et de gérer toutes les exceptions là-dedans:

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 gestion des exceptions dans l'API Web peut être effectuée à différents niveaux. Voici une detailed articleexplication des différentes possibilités:

  • attribut de filtre d'exception personnalisé qui pourrait être enregistré comme filtre d'exception global

    [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"
            });
        }
    }
  • invocateur d'action personnalisé

    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;
        }
    }
Darin Dimitrov
la source
J'aurais aimé que ce soit aussi simple que cela, mais l'erreur n'est toujours pas détectée. J'ai mis à jour la question pour éviter toute confusion. Merci.
Matt Cashatt
@MatthewPatrickCashatt, si cette exception n'est pas interceptée dans l' Application_Errorévénement, cela signifie qu'un autre code la consomme auparavant. Par exemple, vous pourriez avoir des HandleErrorAttributes personnalisés, des modules personnalisés, ... Il existe des millions d'autres endroits où des exceptions pourraient être interceptées et gérées. Mais le meilleur endroit pour le faire est l'événement Application_Error, car c'est là que toutes les exceptions non gérées vont se terminer.
Darin Dimitrov
Merci encore, mais quoi /testqu'il arrive , l' exemple n'est pas touché. J'ai mis un point d'arrêt sur la première ligne ( Exception unhandledException = . . .) mais je ne peux pas atteindre ce point d'arrêt dans le /testscénario. Si je mets une fausse URL, cependant, le point d'arrêt est atteint.
Matt Cashatt
1
@MatthewPatrickCashatt, vous avez tout à fait raison. L' Application_Errorévénement n'est pas le bon endroit pour gérer les exceptions de l'API Web car il ne sera pas déclenché dans tous les cas. J'ai trouvé un article très détaillé expliquant les différentes possibilités pour y parvenir: weblogs.asp.net/fredriknormen/archive/2012/06/11/…
Darin Dimitrov
1
@Darin Dimitrov Les appels de contrôleurs api inconnus comme les erreurs myhost / api / undefinedapi ne sont toujours pas détectés. Le code de filtre Application_error et Exception n'est pas exécuté. Comment les attraper aussi?
Andrus
79

En complément des réponses précédentes.

Hier, l'API Web ASP.NET 2.1 a été officiellement publiée .
Il offre une autre opportunité de gérer les exceptions à l'échelle mondiale.
Les détails sont donnés dans l' exemple .

En bref, vous ajoutez des journaux d'exceptions globaux et / ou un gestionnaire d'exceptions global (un seul).
Vous les ajoutez à la configuration:

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

Et leur réalisation:

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);
  }
}
Vladimir
la source
2
Cela a parfaitement fonctionné. Je connecte et gère simultanément (car j'obtiens le logID et je le transmets pour que l'utilisateur puisse ajouter un commentaire), donc je règle Result sur un nouveau ResponseMessageResult. Cela me dérange depuis un moment, merci.
Brett
8

Pourquoi relancer etc? Cela fonctionne et cela rendra le statut de retour du service 500, etc.

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);
    }
}
Anders
la source
2

avez-vous pensé à faire quelque chose comme un filtre d'action d'erreur de gestion comme

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

vous pouvez également créer une version personnalisée de [HandleError]avec laquelle vous pouvez écrire des informations d'erreur et tous les autres détails à consigner

FROID DIT
la source
Merci, mais j'ai déjà cet ensemble dans le monde entier. Cela pose le même problème que ci-dessus, toutes les erreurs ne sont pas enregistrées.
Matt Cashatt
1

Enveloppez le tout dans un try / catch et enregistrez l'exception non gérée, puis transmettez-la. Sauf s'il existe une meilleure façon intégrée de le faire.

Voici une référence Catch All (géré ou non) Exceptions

(modifier: oh API)

Tim
la source
Juste au cas où, il aurait également besoin de rejeter l'exception.
DigCamara
@DigCamara Désolé, c'est ce que je voulais dire par transmettre. jeter; devrait gérer cela. J'ai dit à l'origine "décider de quitter ou de recharger", puis j'ai réalisé qu'il avait dit que c'était une API. Dans ce cas, mieux vaut laisser l'application décider de ce qu'elle veut faire en la transmettant.
Tim
1
C'est une mauvaise réponse car cela entraînera des charges de code dupliqué dans chaque action.
Jansky