Gestion des exceptions de l'API Web ASP.NET Core

280

J'utilise ASP.NET Core pour mon nouveau projet d'API REST après avoir utilisé l'API Web ASP.NET régulière pendant de nombreuses années. Je ne vois aucun bon moyen 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 enregistrement de filtre de démarrage:

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

Le problème que je rencontrais est que lorsque des exceptions se produisent dans mon, AuthorizationFilteril n'est pas géré par ErrorHandlingFilter. Je m'attendais à ce qu'il soit pris là comme il fonctionnait avec l'ancienne API Web ASP.NET.

Alors, comment puis-je intercepter toutes les exceptions d'application ainsi que toutes les exceptions des filtres d'action?

Andrei
la source
3
Avez-vous essayé le UseExceptionHandlermiddleware?
Pawel
J'ai un exemple ici sur la façon d'utiliser le UseExceptionHandlermiddleware
Ilya Chernomordik

Réponses:

539

Middleware de gestion des exceptions

Après de nombreuses expériences avec différentes approches de gestion des exceptions, j'ai fini par utiliser un middleware. Il a fonctionné le mieux pour mon application API Web ASP.NET Core. Il gère les exceptions d'application ainsi que les exceptions des filtres d'action et j'ai un contrôle total sur la gestion des exceptions et la réponse HTTP. Voici mon exception de gestion des middlewares:

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate next;
    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

        if      (ex is MyNotFoundException)     code = HttpStatusCode.NotFound;
        else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
        else if (ex is MyException)             code = HttpStatusCode.BadRequest;

        var result = JsonConvert.SerializeObject(new { error = ex.Message });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        return context.Response.WriteAsync(result);
    }
}

Enregistrez-le avant MVC en Startupclasse:

app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();

Vous pouvez ajouter une trace de pile, un nom de type d'exception, des codes d'erreur ou tout ce que vous voulez. Très souple. Voici un exemple de réponse d'exception:

{ "error": "Authentication token is not valid." }

Pensez à injecter IOptions<MvcJsonOptions>dans la Invokeméthode pour ensuite l'utiliser lorsque vous sérialisez l'objet de réponse pour utiliser les paramètres de sérialisation d'ASP.NET MVC JsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings)pour une meilleure cohérence de sérialisation sur tous les points de terminaison.

Approche 2

Il existe une autre API non évidente appelée UseExceptionHandlerqui fonctionne "ok" pour des scénarios simples:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var feature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = feature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

Ce n'est pas un moyen très évident mais facile de configurer la gestion des exceptions. Cependant, je préfère toujours l'approche middleware, car j'obtiens plus de contrôle avec la possibilité d'injecter les dépendances nécessaires.

Andrei
la source
4
Je me suis cogné la tête contre le bureau en essayant de faire fonctionner un middleware personnalisé aujourd'hui, et cela 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 levées dans «suivant» ne sont pas prises dans le middleware. Comme vous pouvez l'imaginer, c'est problématique. Que fais-je de mal / manquant? Des conseils ou des suggestions?
brappleye3
5
@ brappleye3 - J'ai compris quel était le problème. Je venais d'enregistrer le middleware au mauvais endroit dans la classe Startup.cs. J'ai déménagé app.UseMiddleware<ErrorHandlingMiddleware>();juste avant app.UseStaticFiles();. L'exception semble être interceptée correctement maintenant. Cela m'amène à croire que app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); app.UseBrowserLink();vous devez faire du piratage de middleware magique interne pour obtenir le bon ordre du middleware.
Jamadan
4
Je suis d'accord que le middleware personnalisé peut être très utile mais remettrait en question l'utilisation d'exceptions pour les situations NotFound, Unauthorized et BadRequest. Pourquoi ne pas simplement définir le code d'état (à l'aide de NotFound (), etc.), puis le gérer dans votre middleware personnalisé ou via UseStatusCodePagesWithReExecute? Voir devtrends.co.uk/blog/handling-errors-in-asp.net-core-web-api pour plus d'informations
Paul Hiles
4
C'est mauvais car il est toujours sérialisé en JSON, ignorant complètement la négociation de contenu.
Konrad
5
@Konrad valide point. C'est pourquoi j'ai dit que cet exemple est l'endroit où vous pouvez commencer, et non le résultat final. Pour 99% des API, JSON est largement suffisant. Si vous pensez que cette réponse n'est pas assez bonne, n'hésitez pas à contribuer.
Andrei
61

Le dernier Asp.Net Core(au moins de 2.2, probablement plus tôt) a un middleware intégré qui le rend un peu plus facile par rapport à l'implémentation dans la réponse acceptée:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

Il devrait faire à peu près la même chose, juste un peu moins de code à écrire.

Important: N'oubliez pas de l'ajouter avant UseMvc(ou UseRoutingdans .Net Core 3) car l'ordre est important.

Ilya Chernomordik
la source
Prend-il en charge DI comme argument du gestionnaire, ou faudrait-il utiliser un modèle de localisateur de service au sein du gestionnaire?
lp
33

Votre meilleur pari est d'utiliser un middleware pour obtenir la journalisation que vous recherchez. Vous souhaitez placer votre journalisation des exceptions dans un middleware, puis gérer vos pages d'erreur affichées pour l'utilisateur dans un middleware différent. Cela permet de séparer la logique et suit la conception que Microsoft a conçue avec les 2 composants middleware. Voici un bon lien vers la documentation de Microsoft: Gestion des erreurs dans ASP.Net Core

Pour votre exemple spécifique, vous souhaiterez peut-être utiliser l'une des extensions du middleware StatusCodePage ou lancer la vôtre comme ceci .

Vous pouvez trouver un exemple ici pour la 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 également utiliser ELM Middleware , 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 fonctionne pas pour vos besoins, vous pouvez toujours lancer votre propre composant Middleware en regardant leurs implémentations d'ExceptionHandlerMiddleware et ElmMiddleware pour saisir les concepts pour construire le vôtre.

Il est important d'ajouter le middleware de gestion des exceptions sous le middleware StatusCodePages mais surtout vos autres composants middleware. De cette façon, 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.

Ashley Lee
la source
De rien. J'ai également fourni un lien vers un exemple pour remplacer les UseStatusPages par défaut sur les cas marginaux qui pourraient mieux répondre à votre demande.
Ashley Lee
1
Notez qu'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. Pouvons-nous le conserver dans un fichier ou une base de données?
Michael Freidgeim
2
Le lien est maintenant rompu.
Mathias Lykkegaard Lorenzen
@AshleyLee, je doute que cela UseStatusCodePagessoit utile dans les implémentations de service d'API Web. Aucune vue ou HTML du tout, seulement des réponses JSON ...
Paul Michalik
23

Une réponse bien acceptée m'a beaucoup aidé mais je voulais passer HttpStatusCode dans mon middleware pour gérer le code d'état d'erreur lors de l'exécution.

D'après ce lien, j'ai eu l'idée de faire de même. J'ai donc fusionné la réponse Andrei avec cela. Donc, mon code final est ci-dessous:
1. Classe de base

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}

2. Type de classe d'exception personnalisée

 public class HttpStatusCodeException : Exception
{
    public HttpStatusCode StatusCode { get; set; }
    public string ContentType { get; set; } = @"text/plain";

    public HttpStatusCodeException(HttpStatusCode statusCode)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, string message) : base(message)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner) : this(statusCode, inner.ToString()) { }

    public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject) : this(statusCode, errorObject.ToString())
    {
        this.ContentType = @"application/json";
    }

}


3. Middleware d'exception personnalisé

public class CustomExceptionMiddleware
    {
        private readonly RequestDelegate next;

    public CustomExceptionMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (HttpStatusCodeException ex)
        {
            await HandleExceptionAsync(context, ex);
        }
        catch (Exception exceptionObj)
        {
            await HandleExceptionAsync(context, exceptionObj);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
    {
        string result = null;
        context.Response.ContentType = "application/json";
        if (exception is HttpStatusCodeException)
        {
            result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)exception.StatusCode }.ToString();
            context.Response.StatusCode = (int)exception.StatusCode;
        }
        else
        {
            result = new ErrorDetails() { Message = "Runtime Error", StatusCode = (int)HttpStatusCode.BadRequest }.ToString();
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
        return context.Response.WriteAsync(result);
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        string result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)HttpStatusCode.InternalServerError }.ToString();
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return context.Response.WriteAsync(result);
    }
}


4. Méthode d'extension

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
    {
        app.UseMiddleware<CustomExceptionMiddleware>();
    }

5. Configurer la méthode dans startup.cs

app.ConfigureCustomExceptionMiddleware();
app.UseMvc();

Maintenant, ma méthode de connexion dans le contrôleur de compte:

 try
        {
            IRepository<UserMaster> obj = new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
            var Result = obj.Get().AsQueryable().Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower() && sb.Password == objData.Password.ToEncrypt() && sb.Status == (int)StatusType.Active).FirstOrDefault();
            if (Result != null)//User Found
                return Result;
            else// Not Found
                throw new HttpStatusCodeException(HttpStatusCode.NotFound, "Please check username or password");
        }
        catch (Exception ex)
        {
            throw ex;
        }

Ci-dessus, vous pouvez voir si je n'ai pas trouvé l'utilisateur, puis lever l'exception HttpStatusCodeException dans laquelle j'ai passé le statut HttpStatusCode.NotFound et un message personnalisé
dans le middleware

catch (HttpStatusCodeException ex)

bloqué sera appelé qui passera le contrôle à

méthode privée Task HandleExceptionAsync (contexte HttpContext, exception HttpStatusCodeException)

.


Mais que faire si j'ai eu une erreur d'exécution avant? Pour cela, j'ai utilisé un bloc try catch qui lève une exception et sera pris dans le bloc catch (Exception exceptionObj) et passera le contrôle à

Task HandleExceptionAsync (contexte HttpContext, exception d'exception)

méthode.

J'ai utilisé une seule classe ErrorDetails pour l'uniformité.

Arjun
la source
Où mettre la méthode d'extension? Malheureusement , dans l' startup.csen void Configure(IapplicationBuilder app)je reçois une erreur IApplicationBuilder does not contain a definition for ConfigureCustomExceptionMiddleware. Et j'ai ajouté la référence, où CustomExceptionMiddleware.csest.
Spedo De La Rossa
vous ne voulez pas utiliser d'exceptions car elles ralentissent vos API. les exceptions sont très coûteuses.
lnaie
@Inaie, je ne peux pas en parler ... mais il semble que vous n'ayez jamais eu d'exception à gérer. Excellent travail
Arjun
19

Pour configurer le comportement de gestion des exceptions par type d'exception, vous pouvez utiliser le middleware des packages NuGet:

Échantillon 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();
}
Ihar Yakimush
la source
16

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

J'inclus le mien car c'est un échantillon plus complet et cela 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 de 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;
        }
    }
}
CountZero
la source
9

Tout d'abord, configurez ASP.NET Core 2 Startuppour qu'il s'exécute à nouveau sur une page d'erreur pour toutes les erreurs du serveur Web et toutes les exceptions non gérées.

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 générer 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 déboguer, donc un code d'état peut être tout ce que vous voulez fournir à un demandeur (potentiellement non fiable). Si vous souhaitez afficher plus d'informations, vous pouvez les améliorer HttpExceptionpour les fournir. Pour les erreurs d'API, vous pouvez mettre des informations d'erreur codées JSON dans le corps du message en les remplaçant return StatusCode...par return Json....

Edward Brey
la source
0

utiliser un middleware ou IExceptionHandlerPathFeature est très bien. il y a une autre façon dans eshop

créer un filtre d'exception et l'enregistrer

public class HttpGlobalExceptionFilter : IExceptionFilter
{
  public void OnException(ExceptionContext context)
  {...}
}
services.AddMvc(options =>
{
  options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})
ws_
la source