web-dev-qa-db-fra.com

Gestion des exceptions API Web ASP.NET Core

J'ai commencé à utiliser ASP.NET Core pour mon nouveau projet d'API REST après avoir utilisé l'API Web ASP.NET standard pendant de nombreuses années. Je ne vois pas le meilleur 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 lorsqu'une exception survient dans mon AuthorizationFilter, elle n'est pas gérée par ErrorHandlingFilter. Je m'attendais à ce qu'il soit capturé là, tout comme cela 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?

215
Andrei

Middleware de gestion des exceptions

Après de nombreuses expériences avec différentes approches de gestion des exceptions, j'ai finalement utilisé un middleware. Cela a fonctionné de manière optimale pour mon application API Web ASP.NET Core. Il gère les exceptions d'application ainsi que les exceptions des filtres d'action et je contrôle totalement la gestion des exceptions et la réponse HTTP. Voici mon middleware de gestion des exceptions:

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 dans Startup class:

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 autre élément de votre choix. Très souple. Voici un exemple de réponse à une exception:

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

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

Approche 2

Il existe une autre API non évidente appelée UseExceptionHandler qui fonctionne "ok" pour un scénario simple:

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 à celle-ci, car j’ai plus de contrôle avec la possibilité d’injecter les dépendances nécessaires.

436
Andrei

Votre meilleur choix est d’utiliser Middleware pour réaliser la journalisation que vous recherchez. Vous souhaitez mettre votre exception en journalisation dans un middleware, puis gérer les pages d'erreur affichées à l'utilisateur dans un middleware différent. Cela permet de séparer la logique et suit la conception que Microsoft a définie avec les 2 composants du middleware. Voici un bon lien vers la documentation de Microsoft: Traitement des erreurs dans ASP.Net Core

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

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 répond pas à vos besoins, vous pouvez toujours lancer votre propre composant Middleware en examinant leurs implémentations d'ExceptionHandlerMiddleware et d'ElmMiddleware pour saisir les concepts permettant de créer le vôtre.

Il est important d'ajouter le middleware de gestion des exceptions au-dessous du middleware StatusCodePages, mais surtout à vos autres composants. Ainsi, votre middleware Exception capturera l'exception, la journalisera, puis autorisera la demande à passer au middleware StatusCodePage qui affichera la page d'erreur conviviale à l'intention de l'utilisateur.

27
Ashley Lee

Le dernier Asp.Net Core (au moins à partir de 2.2, probablement plus tôt) a un middleware intégré qui le rend un peu plus facile par rapport à la mise en œuvre 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);
}));

Cela devrait faire à peu près la même chose, juste un peu moins de code à écrire. N'oubliez pas de l'ajouter avant UseMvc car l'ordre est important.

25
Ilya Chernomordik

Pour configurer le comportement de gestion des exceptions par type d'exception, vous pouvez utiliser le middleware à partir des packages 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();
}
18
Ihar Yakimush

La 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 au moment de l'exécution.

Selon ce lien j'ai eu une idée de faire la même chose. J'ai donc fusionné la réponse Andrei avec ceci. 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. Configurez 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 en soulevant l'exception HttpStatusCodeException dans laquelle j'ai transmis 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 Hand HandExceptionAsync (contexte HttpContext, exception HttpStatusCodeException)

.


Mais que se passe-t-il si j’ai une erreur d’exécution auparavant? Pour cela j’ai utilisé try catch block qui lève une exception et sera attrapé dans le bloc catch (Exception exceptionObj) et passera le contrôle à

Task HandleExceptionAsync (contexte HttpContext, exception Exception)

méthode.

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

17
Arjun

Tout d’abord, merci à Andrei d’avoir basé sa solution sur son exemple.

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

L'approche d'Andrei est limitée par le fait qu'elle ne gère pas la journalisation, la capture de variables de requête potentiellement utiles et la négociation de contenu (elle renvoie toujours le JSON, quelle que soit la demande du client - XML ​​/ texte brut, etc.).

Mon approche consiste à utiliser un objet ObjectResult, ce qui nous permet d’utiliser les fonctionnalités intégrées à 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;
        }
    }
}
15
CountZero

Commencez par configurer ASP.NET Core 2 Startup pour qu'il soit réexécuté sur une page d'erreur en cas d'erreur du serveur Web et d'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 de statut 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 indiquez si la réponse sera visualisée directement par l'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 enregistre les détails de l'erreur avec laquelle vous souhaitez déboguer. Par conséquent, un code d'état peut être tout ce que vous souhaitez fournir à un demandeur (potentiellement non approuvé). Si vous souhaitez afficher plus d'informations, vous pouvez améliorer HttpException pour le fournir. Pour les erreurs d'API, vous pouvez insérer des informations d'erreur codées JSON dans le corps du message en remplaçant return StatusCode... par return Json....

9
Edward Brey