409 votes

Traitement des exceptions de l'API Web d'ASP.NET Core

J'utilise ASP.NET Core pour mon nouveau projet d'API REST après avoir utilisé l'API Web ASP.NET classique pendant de nombreuses années. Je ne vois pas de bonne façon de gérer les exceptions dans l'API Web ASP.NET Core. J'ai essayé d'implémenter un filtre/attribut de gestion des exceptions :

public class ErrorHandlingFilter : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        HandleExceptionAsync(context);
        context.ExceptionHandled = true;
    }

    private static void HandleExceptionAsync(ExceptionContext context)
    {
        var exception = context.Exception;

        if (exception is MyNotFoundException)
            SetExceptionResult(context, exception, HttpStatusCode.NotFound);
        else if (exception is MyUnauthorizedException)
            SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
        else if (exception is MyException)
            SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
        else
            SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
    }

    private static void SetExceptionResult(
        ExceptionContext context, 
        Exception exception, 
        HttpStatusCode code)
    {
        context.Result = new JsonResult(new ApiResponse(exception))
        {
            StatusCode = (int)code
        };
    }
}

Et voici mon inscription au filtre Startup :

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});

Le problème que j'avais est que lorsqu'une exception se produit dans mon AuthorizationFilter ce n'est pas géré par ErrorHandlingFilter . Je m'attendais à ce qu'il soit pris en charge, tout comme cela fonctionnait avec l'ancienne API Web ASP.NET.

Alors comment puis-je attraper toutes les exceptions de l'application ainsi que toutes les exceptions des filtres d'action ?

6 votes

Avez-vous essayé UseExceptionHandler intergiciel ?

755voto

Andrei Mikhalevich Points 6372

Les deux approches utilisent le traitement intégré des exceptions intergiciel . Ajoutez ce code avant d'appeler UseMvc , UseRouting ou UseEndpoints . Il traitera les exceptions de tous les middlewares enregistrés après lui.


Solution rapide et facile.

Il suffit d'ajouter ce middleware avant le routage ASP.NET dans vos enregistrements de middleware.

app.UseExceptionHandler(c => c.Run(async context =>
{
    var exception = context.Features
        .Get<IExceptionHandlerPathFeature>()
        .Error;
    var response = new { error = exception.Message };
    await context.Response.WriteAsJsonAsync(response);
}));

Remarque : ceci est uniquement pour ASP.NET Core 5.0+.


Activez l'injection de dépendances pour la journalisation et/ou d'autres objectifs.

Étape 1. Dans votre startup, enregistrez votre route de traitement des exceptions :

// It should be one of your very first registrations
app.UseExceptionHandler("/error"); // Add this
app.UseEndpoints(endpoints => endpoints.MapControllers());

Étape 2. Créez un contrôleur qui traitera toutes les exceptions et produira une réponse d'erreur :

[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorsController : ControllerBase
{
    [Route("error")]
    public MyErrorResponse Error()
    {
        var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
        var exception = context.Error; // Your exception
        var code = 500; // Internal Server Error by default

        if      (exception is MyNotFoundException) code = 404; // Not Found
        else if (exception is MyUnauthException)   code = 401; // Unauthorized
        else if (exception is MyException)         code = 400; // Bad Request

        Response.StatusCode = code; // You can use HttpStatusCode enum instead

        return new MyErrorResponse(exception); // Your error model
    }
}

Quelques notes et observations importantes :

  • Vous pouvez injecter vos dépendances dans le constructeur du contrôleur.
  • [ApiExplorerSettings(IgnoreApi = true)] est nécessaire. Sinon, cela peut casser votre Swashbuckle swagger.
  • Encore une fois, app.UseExceptionHandler("/error"); doit être l'un des meilleurs enregistrements de votre startup. Configure(...) méthode. Il est probablement plus sûr de le placer au début de la méthode.
  • Le chemin dans app.UseExceptionHandler("/error") et dans le contrôleur [Route("error")] doivent être les mêmes, afin de permettre au contrôleur de gérer les exceptions redirigées depuis le middleware de gestion des exceptions.

Voici le lien à la documentation officielle de Microsoft.


Idées de modèles de réponse.

Mettez en œuvre votre propre modèle de réponse et vos propres exceptions. Cet exemple n'est qu'un bon point de départ. Chaque service devra gérer les exceptions à sa manière. Avec l'approche décrite, vous disposez d'une flexibilité et d'un contrôle total sur le traitement des exceptions et le renvoi de la bonne réponse par votre service.

Un exemple de modèle de réponse aux erreurs (pour vous donner quelques idées) :

public class MyErrorResponse
{
    public string Type { get; set; }
    public string Message { get; set; }
    public string StackTrace { get; set; }

    public MyErrorResponse(Exception ex)
    {
        Type = ex.GetType().Name;
        Message = ex.Message;
        StackTrace = ex.ToString();
    }
}

Pour des services plus simples, vous pouvez mettre en œuvre une exception de code d'état http qui ressemblerait à ceci :

public class HttpStatusException : Exception
{
    public HttpStatusCode Status { get; private set; }

    public HttpStatusException(HttpStatusCode status, string msg) : base(msg)
    {
        Status = status;
    }
}

On peut le lancer de n'importe où de cette façon :

throw new HttpStatusCodeException(HttpStatusCode.NotFound, "User not found");

Votre code de manipulation pourrait alors être simplifié comme suit :

if (exception is HttpStatusException httpException)
{
    code = (int) httpException.Status;
}

HttpContext.Features.Get<IExceptionHandlerFeature>() WAT ?

Les développeurs d'ASP.NET Core ont adopté le concept d'intergiciels où différents aspects de la fonctionnalité tels que Auth, MVC, Swagger, etc. sont séparés et exécutés séquentiellement dans le pipeline de traitement des demandes. Chaque intergiciel a accès au contexte de la demande et peut écrire dans la réponse si nécessaire. Retirer la gestion des exceptions de MVC est logique s'il est important de traiter les erreurs provenant d'intergiciels non-MVC de la même manière que les exceptions MVC, ce qui est très courant dans les applications réelles. Ainsi, comme le middleware de gestion des exceptions intégré ne fait pas partie de MVC, MVC lui-même n'en sait rien et vice versa, le middleware de gestion des exceptions ne sait pas vraiment d'où vient l'exception, à part bien sûr qu'il sait qu'elle s'est produite quelque part dans le tuyau d'exécution de la requête. Mais les deux peuvent avoir besoin d'être "connectés" l'un à l'autre. Ainsi, lorsqu'une exception n'est détectée nulle part, l'intergiciel de gestion des exceptions la détecte et ré-exécute le pipeline pour une route enregistrée dans celui-ci. C'est ainsi que l'on peut "passer" le traitement des exceptions à MVC avec une méthode cohérente. négociation de contenu ou un autre intergiciel si vous le souhaitez. L'exception elle-même est extraite du contexte commun du middleware. Cela a l'air bizarre, mais cela fait l'affaire :).

5 votes

Je me suis battu contre le bureau pour essayer de faire fonctionner un middleware personnalisé aujourd'hui, et il fonctionne essentiellement de la même manière (je l'utilise pour gérer l'unité de travail/transaction pour une demande). Le problème auquel je suis confronté est que les exceptions soulevées dans 'next' ne sont pas capturées dans le middleware. Comme vous pouvez l'imaginer, c'est problématique. Qu'est-ce que je fais de mal/manque ? Avez-vous des conseils ou des suggestions ?

0 votes

@brappleye3 première pensée - assurez-vous que vous await tous vos appels asynchrones, par exemple await context.SaveChangesAsync(); . C'est difficile de deviner sans voir le code. J'y jetterai un coup d'oeil si vous posez votre question avec des détails.

3 votes

Je fais typiquement un mélange d'intergiciel et de IExceptionFilter . Le filtre gère directement les erreurs du contrôleur, et j'utilise le middleware pour une gestion plus "bas niveau". A titre d'information, si quelqu'un a besoin d'exécuter du code par type d'exception dans le gestionnaire global, pour le rendre plus "lisible", n'hésitez pas à jeter un coup d'oeil à une petite bibliothèque que j'ai créée juste pour ça : medium.com/@nogravity00/

35voto

Ashley Lee Points 2575

Votre meilleure chance est d'utiliser un intergiciel pour réaliser l'enregistrement que vous recherchez. Vous voulez placer la journalisation des exceptions dans un intergiciel et gérer les pages d'erreur affichées à l'utilisateur dans un autre intergiciel. Cela permet de séparer la logique et de suivre la conception que Microsoft a établie avec les deux composants middleware. Voici un bon lien vers la documentation de Microsoft : Gestion des erreurs dans ASP.Net Core

Pour votre exemple spécifique, vous voudrez peut-être utiliser l'une des extensions de la section intergiciel StatusCodePage ou faites le vôtre comme ce .

Vous pouvez trouver ici un exemple de journalisation des exceptions : ExceptionHandlerMiddleware.cs

public void Configure(IApplicationBuilder app)
{
    // app.UseErrorPage(ErrorPageOptions.ShowAll);
    // app.UseStatusCodePages();
    // app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
    // app.UseStatusCodePages("text/plain", "Response, status code: {0}");
    // app.UseStatusCodePagesWithRedirects("~/errors/{0}");
    // app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
    // app.UseStatusCodePages(builder => builder.UseWelcomePage());
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");  // I use this version

    // Exception handling logging below
    app.UseExceptionHandler();
}

Si vous n'aimez pas cette implémentation spécifique, vous pouvez aussi utiliser Middleware ELM et voici quelques exemples : Elm Exception Middleware

public void Configure(IApplicationBuilder app)
{
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");
    // Exception handling logging below
    app.UseElmCapture();
    app.UseElmPage();
}

Si cela ne répond pas à vos besoins, vous pouvez toujours créer votre propre composant Middleware en examinant les implémentations de l'ExceptionHandlerMiddleware et de l'ElmMiddleware afin de saisir les concepts pour construire le vôtre.

Il est important d'ajouter l'intergiciel de gestion des exceptions sous l'intergiciel StatusCodePages mais au-dessus de tous vos autres composants intergiciels. Ainsi, votre middleware Exception capturera l'exception, l'enregistrera, puis permettra à la requête de passer au middleware StatusCodePage qui affichera la page d'erreur conviviale à l'utilisateur.

1 votes

Notez que Elm ne conserve pas les journaux, et il est recommandé d'utiliser Serilog ou NLog pour fournir la sérialisation. Voir Les journaux ELM disparaissent. Peut-on le faire persister dans un fichier ou une BD ?

2 votes

Le lien est maintenant rompu.

0 votes

@AshleyLee, je doute que UseStatusCodePages est utile dans les implémentations de services d'API Web. Pas de vues ou de HTML du tout, seulement des réponses JSON...

21voto

Ihar Yakimush Points 292

Pour configurer le comportement de traitement des exceptions par type d'exception, vous pouvez utiliser les intergiciels des paquets NuGet :

Exemple de code :

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddExceptionHandlingPolicies(options =>
    {
        options.For<InitializationException>().Rethrow();

        options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();

        options.For<SomeBadRequestException>()
        .Response(e => 400)
            .Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
            .WithBody((req,sw, exception) =>
                {
                    byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
                    return sw.WriteAsync(array, 0, array.Length);
                })
        .NextPolicy();

        // Ensure that all exception types are handled by adding handler for generic exception at the end.
        options.For<Exception>()
        .Log(lo =>
            {
                lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
                lo.Category = (context, exception) => "MyCategory";
            })
        .Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
            .ClearCacheHeaders()
            .WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
        .Handled();
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseExceptionHandlingPolicies();
    app.UseMvc();
}

20voto

CountZero Points 789

Tout d'abord, merci à Andrei car j'ai basé ma solution sur son exemple.

J'ai inclus le mien car il s'agit d'un échantillon plus complet qui pourrait faire gagner du temps aux lecteurs.

La limite de l'approche d'Andrei est qu'elle ne gère pas la journalisation, la capture de variables de requête potentiellement utiles et la négociation du contenu (elle renverra toujours JSON, peu importe ce que le client a demandé - XML / texte brut, etc.)

Mon approche consiste à utiliser un ObjectResult qui nous permet d'utiliser la fonctionnalité intégrée à MVC.

Ce code empêche également la mise en cache de la réponse.

La réponse d'erreur a été décorée de manière à pouvoir être sérialisée par le sérialiseur XML.

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate next;
    private readonly IActionResultExecutor<ObjectResult> executor;
    private readonly ILogger logger;
    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
    {
        this.next = next;
        this.executor = executor;
        logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));

            if (context.Response.HasStarted)
            {
                throw;
            }

            var routeData = context.GetRouteData() ?? new RouteData();

            ClearCacheHeaders(context.Response);

            var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

            var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
            {
                StatusCode = (int) HttpStatusCode.InternalServerError,
            };

            await executor.ExecuteAsync(actionContext, result);
        }
    }

    private static string GetRequestData(HttpContext context)
    {
        var sb = new StringBuilder();

        if (context.Request.HasFormContentType && context.Request.Form.Any())
        {
            sb.Append("Form variables:");
            foreach (var x in context.Request.Form)
            {
                sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
            }
        }

        sb.AppendLine("Method: " + context.Request.Method);

        return sb.ToString();
    }

    private static void ClearCacheHeaders(HttpResponse response)
    {
        response.Headers[HeaderNames.CacheControl] = "no-cache";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
        response.Headers.Remove(HeaderNames.ETag);
    }

    [DataContract(Name= "ErrorResponse")]
    public class ErrorResponse
    {
        [DataMember(Name = "Message")]
        public string Message { get; set; }

        public ErrorResponse(string message)
        {
            Message = message;
        }
    }
}

0 votes

Voir github.com/dotnet/aspnetcore/blob/master/src/Middleware/ si vous voulez vérifier le code source actuel et ajouter des choses à partir de cette approche.

10voto

Edward Brey Points 8771

Tout d'abord, configurez ASP.NET Core 2 Startup pour réexécuter vers une page d'erreur pour toute erreur du serveur web et toute exception non gérée.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment()) {
        // Debug config here...
    } else {
        app.UseStatusCodePagesWithReExecute("/Error");
        app.UseExceptionHandler("/Error");
    }
    // More config...
}

Ensuite, définissez un type d'exception qui vous permettra de lancer des erreurs avec des codes d'état HTTP.

public class HttpException : Exception
{
    public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
    public HttpStatusCode StatusCode { get; private set; }
}

Enfin, dans votre contrôleur pour la page d'erreur, personnalisez la réponse en fonction de la raison de l'erreur et si la réponse sera vue directement par un utilisateur final. Ce code suppose que toutes les URL d'API commencent par /api/ .

[AllowAnonymous]
public IActionResult Error()
{
    // Gets the status code from the exception or web server.
    var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
        httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;

    // For API errors, responds with just the status code (no page).
    if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
        return StatusCode((int)statusCode);

    // Creates a view model for a user-friendly error page.
    string text = null;
    switch (statusCode) {
        case HttpStatusCode.NotFound: text = "Page not found."; break;
        // Add more as desired.
    }
    return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}

ASP.NET Core enregistrera les détails de l'erreur pour que vous puissiez les déboguer. Un code d'état peut donc être tout ce que vous voulez fournir à un demandeur (potentiellement non fiable). Si vous souhaitez afficher plus d'informations, vous pouvez améliorer la fonction HttpException pour le fournir. Pour les erreurs d'API, vous pouvez insérer des informations d'erreur codées en JSON dans le corps du message en remplaçant return StatusCode... avec return Json... .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X