Passer au contenu du pied de page
Iron Academy Logo
Apprendre le C#
Apprendre le C#

Autres catégories

Gestion globale des erreurs dans les APIs minimales C#

[[academy-video-youtube({"vid": "B5NsgtdwOlg", "start_time": "0", "title": "Global Error Handling in C# Minimal APIs", "creator": "Tim Corey", "length": "13m 30s"})]]

Une API web qui lance une exception non capturée retournera par défaut le type de page d'erreur qui aide un développeur à déboguer localement et aide un inconnu à cartographier votre pile d'appels. Les numéros de ligne, les noms de type et le chemin vers le fichier source sont tous retransmis à celui qui a fait la demande. Capturer chaque erreur au point où elle pourrait se produire est la bonne approche, mais cela ne fonctionne que jusqu'au prochain bloc try/catch oublié. Un gestionnaire global est le filet de sécurité qui attrape ce que le point de terminaison a manqué.

Dans sa vidéo "Gestion globale des erreurs dans les API minimales en C#", Tim Corey construit une petite API minimale avec un point de terminaison délibérément cassé, démontre la page d'erreur du développeur qui est renvoyée sans protection, puis connecte app.UseExceptionHandler pour intercepter toute exception non interceptée et répondre avec un message générique 500. Il renforce également pourquoi la gestion au niveau du point de terminaison reste le chemin préféré : le gestionnaire global est le recours, pas la stratégie. Quiconque souhaite expédier une API minimale et s'assurer qu'aucune trace de pile ne quitte le serveur trouvera ci-dessous la configuration du middleware et la logique de conception qui l'entoure.

Construire une API minimale avec un point de terminaison cassé

[1:08 - 3:01] Tim commence avec un projet de Web API ASP.NET Core .NET 8 nommé ErrorDemoApp. Les options de modèle de projet restent proches des valeurs par défaut : HTTPS activé, OpenAPI activé, pas d'authentification, déclarations de haut niveau laissées activées, et la case des contrôleurs décochée car il s'agit d'une API minimale. Le Program.cs généré garde Swagger, mais l'exemple de prévision météo et son enregistrement sont supprimés, de sorte que le fichier ne montre que l'essentiel.

À la place de l'exemple, il ajoute un seul point de terminaison sur /demo dont le but est de lancer une erreur :

app.MapGet("/demo", () =>
{
    throw new Exception("This is a demo exception");
});
app.MapGet("/demo", () =>
{
    throw new Exception("This is a demo exception");
});

Exécuter le projet avec Ctrl+F5 (démarrage sans débogage) empêche le débogueur de Visual Studio d'intercepter le throw, donc l'échec refait surface comme il le ferait pour un appelant HTTP réel. Swagger s'ouvre, le point de terminaison /demo est le seul disponible et l'exécution retourne une réponse 500. Le corps de la réponse contient le type d'exception, le message et une référence à Program.cs ligne 18.

Pourquoi la page d'erreur par défaut divulgue des détails d'implémentation

[3:01 - 5:00] Accéder à /demo directement dans le navigateur (sans le wrapper ?message= de Swagger) montre la page d'exception développeur plutôt que la réponse JSON. La page rend le nom de l'exception, le message, le chemin et le numéro de ligne où le throw est survenu, les détails bruts de l'exception et les trames de pile au-dessus du throw. Pour un développeur travaillant localement, c'est de l'or. Pour quiconque d'autre, c'est une carte gratuite de la base de code.

Le point de Tim atterrit sans y mettre les formes : cette page existe pour aider les développeurs, et elle ne devrait jamais atteindre les utilisateurs finaux. Le fait qu'elle le fasse parfois est la raison pour laquelle un gestionnaire global est important. Même les équipes qui enveloppent diligemment chaque point de terminaison dans une gestion des exceptions finissent par en manquer un, et le coût de manquer un est l'envoi de toute la trace de pile à celui qui a demandé.

Attraper les erreurs d'abord au point de terminaison

[5:00 - 6:30] Avant d'installer le gestionnaire global, Tim enveloppe le point de terminaison de démonstration dans un try/catch pour illustrer le chemin préféré. Le gestionnaire retourne Results.BadRequest(ex.Message) pour toute exception lancée :

app.MapGet("/demo", () =>
{
    try
    {
        throw new Exception("This is a demo exception");
    }
    catch (Exception ex)
    {
        return Results.BadRequest(ex.Message);
    }
});
app.MapGet("/demo", () =>
{
    try
    {
        throw new Exception("This is a demo exception");
    }
    catch (Exception ex)
    {
        return Results.BadRequest(ex.Message);
    }
});

Le résultat est un 400 ne contenant que la chaîne de message. Pas de trace de pile, pas de chemin de fichier, pas de numéro de ligne. Si le message lui-même doit être exposé dépend de l'application ; pour une API publique, même le message peut révéler plus que ce que l'équipe souhaite, auquel cas le gestionnaire substitue une chaîne générique. La capture locale donne au point de terminaison un contrôle total sur ce que le client voit, y compris le choix de retourner un code d'état plus spécifique que 500 lorsque le mode d'échec est effectivement connu.

Ce que cette structure ne peut pas faire, c'est capturer ce que le point de terminaison a oublié d'encadrer. Tout nouveau chemin de code, toute relance d'une couche plus profonde, tout Task qui lance sur un autre thread, contourne tous le try/catch du point de terminaison. C'est l'écart que le middleware comble.

Branchement du Middleware UseExceptionHandler

[6:30 - 10:00] Juste en dessous de la ligne app.UseHttpsRedirection(), le gestionnaire est enregistré avec app.UseExceptionHandler. La surcharge qui prend une action du constructeur expose le pipeline sous-jacent, ce qui permet au gestionnaire de définir explicitement la forme de la réponse :

app.UseExceptionHandler(appError =>
{
    appError.Run(async context =>
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/json";

        var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
        if (contextFeature is not null)
        {
            Console.WriteLine($"Error: {contextFeature.Error}");
        }

        await context.Response.WriteAsJsonAsync(new
        {
            StatusCode = context.Response.StatusCode,
            Message = "Internal Server Error"
        });
    });
});
app.UseExceptionHandler(appError =>
{
    appError.Run(async context =>
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/json";

        var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
        if (contextFeature is not null)
        {
            Console.WriteLine($"Error: {contextFeature.Error}");
        }

        await context.Response.WriteAsJsonAsync(new
        {
            StatusCode = context.Response.StatusCode,
            Message = "Internal Server Error"
        });
    });
});

Quelques choix dans ce bloc sont importants. Forcer le code d'état à 500 signifie que le client ne peut rien déduire du numéro de réponse ; quel que soit le type d'exception interne, la surface paraît identique. Forcer le type de contenu à application/json correspond au reste des réponses de l'API, ce qui maintient les clients sur un seul parseur. Le IExceptionHandlerFeature expose l'exception originale de sorte qu'un vrai gestionnaire puisse la consigner ; Tim utilise Console.WriteLine ici comme substitut pour n'importe quel logger que le projet porterait réellement.

L'appel final WriteAsJsonAsync renvoie un objet anonyme avec le code statut et le message générique. Le corps ne dit rien sur ce qui a échoué au-delà du fait que quelque chose a échoué, ce qui est le but. Les diagnostics internes appartiennent au log, pas à la réponse.

Test des chemins traités et non traités

[10:00 - 13:14] Avec le try/catch toujours en place, le point de terminaison exécute le chemin local : Swagger montre un 400 portant "Ceci est une exception de démonstration". Le middleware ne voit jamais l'exception car le bloc catch la résout en premier. C'est le design que Tim veut par défaut : les gestionnaires locaux font leur travail, et le gestionnaire global est en veille.

Supprimer le try/catch et exécuter à nouveau exerce la relance. La même requête renvoie maintenant un 500 avec le corps JSON { "statusCode": 500, "message": "Internal Server Error" }. Rien dans la réponse ne révèle où l'exception a été lancée ou de quel type il s'agissait. La fenêtre de console de Visual Studio, cependant, affiche le texte de l'exception originale enregistré via le placeholder Console.WriteLine, incluant le chemin du fichier et le numéro de ligne. Le diagnostic complet reste là où les développeurs peuvent le lire ; la réponse reste là où elle ne peut pas fuiter.

Ce schéma se poursuit jusqu'à une API minimale avec un middlewares de validation, une authentification personnalisée, ou tout autre composant de pipeline. Le gestionnaire d'exceptions se situe tôt dans le pipeline et intercepte tout ce qui remonte d'une étape ultérieure.

Conclusion : Défense en profondeur

[13:14 - 13:30] La gestion locale et la gestion globale ne sont pas des alternatives ; ce sont des couches. Le gestionnaire local donne au point de terminaison la chance de répondre de manière significative lorsque le mode de défaillance est connu. Le gestionnaire global garantit que tout ce que la couche locale a manqué produit une réponse qui est cohérente, générique et sûre. Les API basées sur les contrôleurs utilisent la même idée avec des ajustements mineurs de syntaxe, mais la forme de l'API minimale est celle qu'il vaut la peine de se familiariser en premier car la surface est suffisamment petite pour voir toute l'image dans un fichier.

Conclusion

[13:14 - 13:30] La mise en place d'un gestionnaire d'erreur global dans une API minimale se fait en trois étapes : enregistrer UseExceptionHandler tôt dans le pipeline, définir le statut de la réponse et le type de contenu à l'intérieur du gestionnaire, et écrire un corps volontairement générique pour que aucun détail d'implémentation ne fuite. Associez cela à des blocs try/catch locaux autour des chemins de code les plus susceptibles d'échouer, et vous avez un modèle de défense en profondeur où le gestionnaire global est le filet de sécurité plutôt que la stratégie.

Conseil d'exemple : lorsque le gestionnaire appelle un vrai logger, passez l'objet exception entier (pas seulement le message) afin que le pipeline de journalisation structuré capture le type, la pile et les exceptions internes éventuelles. Un logger comme Serilog conservera tout cela en tant que propriétés interrogeables, ce qui signifie que l'alerte qui s'active pour un 500 en production porte suffisamment de contexte pour reproduire localement sans que personne ne réexécute la requête.

Regardez la vidéo complète sur sa chaîne YouTube et obtenez plus d'informations sur la construction d'APIs minimales prêtes pour la production dans la série de formation de 10 minutes.

Hero Worlddot related to Gestion globale des erreurs dans les APIs minimales C#
Hero Affiliate related to Gestion globale des erreurs dans les APIs minimales C#

Gagnez plus en partageant ce que vous aimez

Vous créez du contenu pour les développeurs travaillant avec .NET, C#, Java, Python ou Node.js ? Transformez votre expertise en revenu supplémentaire !

Équipe de soutien Iron

Nous sommes en ligne 24 heures sur 24, 5 jours sur 7.
Chat
Email
Appelez-moi