Gestion globale des erreurs dans API Web ASP.NET 2

par David Matson, Rick Anderson

Cette rubrique fournit une vue d’ensemble de la gestion globale des erreurs dans API Web ASP.NET 2 pour ASP.NET 4.x. Aujourd’hui, il n’existe aucun moyen simple dans l’API web de journaliser ou de gérer les erreurs à l’échelle mondiale. Certaines exceptions non gérées peuvent être traitées via des filtres d’exception, mais il existe un certain nombre de cas que les filtres d’exception ne peuvent pas gérer. Par exemple :

  1. Les exceptions lancées à partir des constructeurs de contrôleur.
  2. Les exceptions lancées à partir des gestionnaires de messages.
  3. Les exceptions lancées pendant le routage.
  4. Les exceptions lancées pendant la sérialisation du contenu de réponse.

Nous voulons fournir un moyen simple et cohérent de journaliser et de gérer (si possible) ces exceptions.

Il existe deux cas majeurs pour la gestion des exceptions: le cas où nous sommes en mesure d’envoyer une réponse d’erreur et le cas où tout ce que nous pouvons faire est de consigner l’exception. Un exemple dans ce dernier cas est lorsqu’une exception est levée au milieu du contenu de la réponse de diffusion en continu ; dans ce cas, il est trop tard pour envoyer un nouveau message de réponse, car le code status, les en-têtes et le contenu partiel ont déjà été envoyés sur le réseau. Nous avons donc simplement abandonné la connexion. Même si l’exception ne peut pas être gérée pour produire un nouveau message de réponse, nous prenons toujours en charge la journalisation de l’exception. Dans les cas où nous pouvons détecter une erreur, nous pouvons retourner une réponse d’erreur appropriée, comme indiqué dans les éléments suivants :

public IHttpActionResult GetProduct(int id)
{
    var product = products.FirstOrDefault((p) => p.Id == id);
    if (product == null)
    {
        return NotFound();
    }
    return Ok(product);
}

Options existantes

En plus des filtres d’exceptions, les gestionnaires de messages peuvent être utilisés aujourd’hui pour observer toutes les réponses de niveau 500, mais il est difficile d’agir sur ces réponses, car ils manquent de contexte sur l’erreur d’origine. Les gestionnaires de messages ont également certaines des mêmes limitations que les filtres d’exception concernant les cas qu’ils peuvent gérer. Bien que l’API web dispose d’une infrastructure de suivi qui capture les conditions d’erreur, l’infrastructure de suivi est à des fins diagnostics et n’est pas conçue ou adaptée à l’exécution dans les environnements de production. La gestion et la journalisation globales des exceptions doivent être des services qui peuvent s’exécuter pendant la production et être connectés aux solutions de supervision existantes (par exemple, ELMAH).

Vue d'ensemble de la solution

Nous fournissons deux nouveaux services remplaçables par l’utilisateur, IExceptionLogger et IExceptionHandler, pour journaliser et gérer les exceptions non gérées. Les services sont très similaires, avec deux différences main :

  1. Nous prenons en charge l’inscription de plusieurs enregistreurs d’événements d’exceptions, mais un seul gestionnaire d’exceptions.
  2. Les enregistreurs d’événements d’exception sont toujours appelés, même si nous sommes sur le point d’abandonner la connexion. Les gestionnaires d’exceptions sont appelés uniquement lorsque nous sommes toujours en mesure de choisir le message de réponse à envoyer.

Les deux services fournissent l’accès à un contexte d’exception contenant des informations pertinentes à partir du point où l’exception a été détectée, en particulier httpRequestMessage, HttpRequestContext, l’exception levée et la source de l’exception (détails ci-dessous).

Principes de conception

  1. Aucune modification cassant Étant donné que cette fonctionnalité est ajoutée dans une version mineure, une contrainte importante impactant la solution est qu’il n’y a aucune modification cassante, que ce soit pour les contrats de type ou pour le comportement. Cette contrainte a exclu un certain nettoyage que nous aimerions effectuer en termes de blocs catch existants transformant les exceptions en 500 réponses. Ce nettoyage supplémentaire est un élément que nous pouvons envisager pour une version majeure ultérieure.
  2. Maintien de la cohérence avec les constructions d’API web Le pipeline de filtre de l’API web est un excellent moyen de gérer les problèmes transversaux avec la flexibilité d’appliquer la logique à une action spécifique, spécifique au contrôleur ou à une étendue globale. Les filtres, y compris les filtres d’exception, ont toujours des contextes d’action et de contrôleur, même s’ils sont inscrits dans l’étendue globale. Ce contrat est judicieux pour les filtres, mais cela signifie que les filtres d’exception, même ceux dont l’étendue est globale, ne conviennent pas à certains cas de gestion des exceptions, tels que les exceptions provenant de gestionnaires de messages, où il n’existe aucun contexte de contrôleur ou d’action. Si nous voulons utiliser la portée flexible offerte par les filtres pour la gestion des exceptions, nous avons toujours besoin de filtres d’exception. Mais si nous devons gérer l’exception en dehors d’un contexte de contrôleur, nous avons également besoin d’une construction distincte pour la gestion complète des erreurs globales (quelque chose sans le contexte du contrôleur et les contraintes de contexte d’action).

Quand l’utiliser

  • Les enregistreurs d’événements d’exceptions sont la solution pour voir toutes les exceptions non gérées interceptées par l’API web.
  • Les gestionnaires d’exceptions sont la solution pour personnaliser toutes les réponses possibles aux exceptions non gérées interceptées par l’API web.
  • Les filtres d’exception sont la solution la plus simple pour traiter le sous-ensemble d’exceptions non gérées liées à une action ou à un contrôleur spécifique.

Détails sur le service

L’enregistreur d’événements d’exceptions et les interfaces de service de gestionnaire sont des méthodes asynchrones simples qui prennent les contextes respectifs :

public interface IExceptionLogger
{
   Task LogAsync(ExceptionLoggerContext context, 
                 CancellationToken cancellationToken);
}

public interface IExceptionHandler
{
   Task HandleAsync(ExceptionHandlerContext context, 
                    CancellationToken cancellationToken);
}

Nous fournissons également des classes de base pour ces deux interfaces. La substitution des méthodes principales (sync ou async) est tout ce qui est nécessaire pour journaliser ou gérer aux heures recommandées. Pour la journalisation, la ExceptionLogger classe de base garantit que la méthode de journalisation principale n’est appelée qu’une seule fois pour chaque exception (même si elle se propage plus tard plus loin dans la pile des appels et est interceptée à nouveau). La ExceptionHandler classe de base appelle la méthode de gestion de base uniquement pour les exceptions en haut de la pile des appels, en ignorant les blocs catch imbriqués hérités. (Les versions simplifiées de ces classes de base figurent dans l’annexe ci-dessous.) IExceptionHandler Et reçoivent des IExceptionLogger informations sur l’exception via un ExceptionContext.

public class ExceptionContext
{
   public Exception Exception { get; set; }

   public HttpRequestMessage Request { get; set; }

   public HttpRequestContext RequestContext { get; set; }

   public HttpControllerContext ControllerContext { get; set; }

   public HttpActionContext ActionContext { get; set; }

   public HttpResponseMessage Response { get; set; }

   public string CatchBlock { get; set; }

   public bool IsTopLevelCatchBlock { get; set; }
}

Lorsque l’infrastructure appelle un enregistreur d’événements d’exceptions ou un gestionnaire d’exceptions, il fournit toujours un Exception et un Request. À l’exception des tests unitaires, il fournit également toujours un RequestContext. Il fournit rarement un et ActionContext (uniquement lors de l’appel à partir du bloc catch pour les filtres d’exceptionControllerContext). Il fournit très rarement un Response(uniquement dans certains cas IIS quand au milieu de la tentative d’écriture de la réponse). Notez que, étant donné que certaines de ces propriétés peuvent êtrenull, il appartient au consommateur de case activée pour null avant d’accéder aux membres de la classe exception.CatchBlock est une chaîne indiquant le bloc catch qui a vu l’exception. Les chaînes de blocs catch sont les suivantes :

  • HttpServer (méthode SendAsync)

  • HttpControllerDispatcher (méthode SendAsync)

  • HttpBatchHandler (méthode SendAsync)

  • IExceptionFilter (traitement par ApiController du pipeline de filtre d’exceptions dans ExecuteAsync)

  • Hôte OWIN :

    • HttpMessageHandlerAdapter.BufferResponseContentAsync (pour la sortie de mise en mémoire tampon)
    • HttpMessageHandlerAdapter.CopyResponseContentAsync (pour la sortie de streaming)
  • Hôte web :

    • HttpControllerHandler.WriteBufferedResponseContentAsync (pour la sortie de mise en mémoire tampon)
    • HttpControllerHandler.WriteStreamedResponseContentAsync (pour la sortie de streaming)
    • HttpControllerHandler.WriteErrorResponseContentAsync (pour les échecs de récupération d’erreur en mode de sortie mis en mémoire tampon)

La liste des chaînes de blocs catch est également disponible via des propriétés readonly statiques. (La chaîne de bloc catch de base se trouve sur exceptionCatchBlocks statique ; le reste apparaît sur une classe statique chacune pour OWIN et l’hôte web).IsTopLevelCatchBlock est utile pour suivre le modèle recommandé de gestion des exceptions uniquement en haut de la pile des appels. Au lieu de transformer les exceptions en 500 réponses partout où se produit un bloc catch imbriqué, un gestionnaire d’exceptions peut laisser les exceptions se propager jusqu’à ce qu’elles soient sur le point d’être vues par l’hôte.

En plus de , ExceptionContextun enregistreur d’événements obtient une information supplémentaire via le complet ExceptionLoggerContext:

public class ExceptionLoggerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public bool CanBeHandled { get; set; }
}

La deuxième propriété, CanBeHandled, permet à un journal d’identifier une exception qui ne peut pas être gérée. Lorsque la connexion est sur le point d’être abandonnée et qu’aucun nouveau message de réponse ne peut être envoyé, les enregistreurs d’événements sont appelés, mais le gestionnaire ne l’est pas , et les enregistreurs d’événements peuvent identifier ce scénario à partir de cette propriété.

En plus de , ExceptionContextun gestionnaire obtient une propriété supplémentaire qu’il peut définir sur le full ExceptionHandlerContext pour gérer l’exception :

public class ExceptionHandlerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public IHttpActionResult Result { get; set; }
}

Un gestionnaire d’exceptions indique qu’il a géré une exception en affectant à la propriété un Result résultat d’action (par exemple, un ExceptionResult, InternalServerErrorResult, StatusCodeResult ou un résultat personnalisé). Si la propriété a la Result valeur null, l’exception n’est pas gérée et l’exception d’origine est levée de nouveau.

Pour les exceptions en haut de la pile des appels, nous avons pris une étape supplémentaire pour nous assurer que la réponse est appropriée pour les appelants d’API. Si l’exception se propage à l’hôte, l’appelant voit l’écran jaune de la mort ou une autre réponse fournie par l’hôte qui est généralement HTML et qui n’est généralement pas une réponse d’erreur d’API appropriée. Dans ce cas, le résultat démarre avec une valeur non null, et ce n’est que si un gestionnaire d’exceptions personnalisé le rétablit null explicitement sur (non géré) que l’exception se propage à l’hôte. null Dans de tels cas, la définition Result de sur peut être utile pour deux scénarios :

  1. API web hébergée OWIN avec gestion d’un intergiciel personnalisé d’exception inscrit avant/en dehors de l’API web.
  2. Débogage local via un navigateur, où l’écran jaune de la mort est en fait une réponse utile pour une exception non gérée.

Pour les enregistreurs d’événements d’exceptions et les gestionnaires d’exceptions, nous ne faisons rien pour récupérer si l’enregistreur d’événements ou le gestionnaire lui-même lève une exception. (En plus de laisser l’exception se propager, laissez les commentaires en bas de cette page si vous avez une meilleure approche.) Le contrat des enregistreurs d’événements et des gestionnaires d’exceptions est qu’ils ne doivent pas laisser les exceptions se propager à leurs appelants ; sinon, l’exception se propage simplement, souvent jusqu’à l’hôte, ce qui entraîne une erreur HTML (comme ASP). Écran jaune de NET) renvoyé au client (qui n’est généralement pas l’option préférée pour les appelants d’API qui attendent JSON ou XML).

Exemples

Enregistreur d’événements d’exceptions de suivi

L’enregistreur d’événements d’exceptions ci-dessous envoie les données d’exception aux sources de trace configurées (y compris la fenêtre Debug output dans Visual Studio).

class TraceExceptionLogger : ExceptionLogger
{
    public override void LogCore(ExceptionLoggerContext context)
    {
        Trace.TraceError(context.ExceptionContext.Exception.ToString());
    }
}

Gestionnaire d’exceptions de message d’erreur personnalisé

Le gestionnaire d’exceptions ci-dessous génère une réponse d’erreur personnalisée aux clients, y compris une adresse e-mail pour contacter le support.

class OopsExceptionHandler : ExceptionHandler
{
    public override void HandleCore(ExceptionHandlerContext context)
    {
        context.Result = new TextPlainErrorResult
        {
            Request = context.ExceptionContext.Request,
            Content = "Oops! Sorry! Something went wrong." +
                      "Please contact support@contoso.com so we can try to fix it."
        };
    }

    private class TextPlainErrorResult : IHttpActionResult
    {
        public HttpRequestMessage Request { get; set; }

        public string Content { get; set; }

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            HttpResponseMessage response = 
                             new HttpResponseMessage(HttpStatusCode.InternalServerError);
            response.Content = new StringContent(Content);
            response.RequestMessage = Request;
            return Task.FromResult(response);
        }
    }
}

Inscription de filtres d’exceptions

Si vous utilisez le modèle de projet « application web ASP.NET MVC 4 » pour créer votre projet, placez votre code de configuration d’API web à l’intérieur de la WebApiConfig classe, dans le dossier App_Start :

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());

        // Other configuration code...
    }
}

Annexe : Détails de la classe de base

public class ExceptionLogger : IExceptionLogger
{
    public virtual Task LogAsync(ExceptionLoggerContext context, 
                                 CancellationToken cancellationToken)
    {
        if (!ShouldLog(context))
        {
            return Task.FromResult(0);
        }

        return LogAsyncCore(context, cancellationToken);
    }

    public virtual Task LogAsyncCore(ExceptionLoggerContext context, 
                                     CancellationToken cancellationToken)
    {
        LogCore(context);
        return Task.FromResult(0);
    }

    public virtual void LogCore(ExceptionLoggerContext context)
    {
    }

    public virtual bool ShouldLog(ExceptionLoggerContext context)
    {
        IDictionary exceptionData = context.ExceptionContext.Exception.Data;

        if (!exceptionData.Contains("MS_LoggedBy"))
        {
            exceptionData.Add("MS_LoggedBy", new List<object>());
        }

        ICollection<object> loggedBy = ((ICollection<object>)exceptionData[LoggedByKey]);

        if (!loggedBy.Contains(this))
        {
            loggedBy.Add(this);
            return true;
        }
        else
        {
            return false;
        }
    }
}

public class ExceptionHandler : IExceptionHandler
{
    public virtual Task HandleAsync(ExceptionHandlerContext context, 
                                    CancellationToken cancellationToken)
    {
        if (!ShouldHandle(context))
        {
            return Task.FromResult(0);
        }

        return HandleAsyncCore(context, cancellationToken);
    }

    public virtual Task HandleAsyncCore(ExceptionHandlerContext context, 
                                       CancellationToken cancellationToken)
    {
        HandleCore(context);
        return Task.FromResult(0);
    }

    public virtual void HandleCore(ExceptionHandlerContext context)
    {
    }

    public virtual bool ShouldHandle(ExceptionHandlerContext context)
    {
        return context.ExceptionContext.IsOutermostCatchBlock;
    }
}