Gestion des exceptions personnalisées sur l'API .NET

· 5 min de lecture

Les exceptions sont mauvaises, nous le savons, n’est-ce pas ? Mais que se passe-t-il si nous devons les gérer ?

Que se passe-t-il lorsque nous avons une exception, par exemple sur une API, elle affiche un message de pile qui comprend de nombreuses informations que nous pourrions vouloir supprimer de la réponse que nos utilisateurs reçoivent.

Pour la démo, j’ai créé une API dotnet et ajouté une méthode qui lèvera une exception :

[HttpGet]
[Route("GetWithoutExceptionHandler")]
public Task GetWithoutExceptionHandler()
{
    throw new Exception("This is a custom exception!");
}

Si nous levons une exception, cela ressemble à ceci :

System.Exception: This is a custom exception!
   at CustomExceptionHandleDemo.Controllers.WeatherForecastController.GetWithoutExceptionHandler()
   at lambda_method16(Closure , Object , Object[] )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Cela semble normal, c’est ce que vous obtenez d’une exception, mais pour moi, cela affiche beaucoup d’informations, si vous avez des bibliothèques et autres, cela pourrait montrer des informations sensibles concernant votre client, votre projet ou d’autres éléments à quelqu’un qui pourrait essayer de voir des choses.

Voici à quoi cela ressemble sur Swagger :

Création d’une exception personnalisée

Cette étape pourrait être évitée, puisque nous savons quelle exception sera levée, dans ce cas un Exception, mais pour moi, avoir vos exceptions personnalisées est le mieux puisque vous avez plus de contrôle sur ce que vous lancez.

Dans ce cas, je viens de créer un objet CustomException qui hérite de Exception :

namespace CustomExceptionHandleDemo.Exceptions
{
    public class CustomException : Exception
    {
        /// <summary>
        /// Constructor for <see cref="CustomException"/>
        /// </summary>
        public CustomException() { }

        /// <summary>
        /// Constructor for <see cref="CustomException"/>
        /// </summary>
        /// <param cref="string" name="message">Parameter for message</param>
        public CustomException(string message) : base(message) { }

        /// <summary>
        /// Constructor for <see cref="CustomException"/>
        /// </summary>
        /// <param cref="string" name="message">Parameter for message</param>
        /// <param cref="Exception" name="inner">Parameter for inner</param>
        public CustomException(string message, Exception inner) : base(message, inner) { }
    }
}

Après avoir créé notre exception personnalisée, mettons à jour notre méthode pour lancer CustomException au lieu de Exception :

[HttpGet]
[Route("GetWithoutExceptionHandler")]
public Task GetWithoutExceptionHandler()
{
    throw new CustomException("This is a custom exception!");
}

Pour l’instant, cela ne changera rien mais la trace de pile montrera que l’objet qui a été lancé est un CustomException au lieu de Exception, jetez un œil au début de la trace de pile :

CustomExceptionHandleDemo.Exceptions.CustomException: This is a custom exception!
   at CustomExceptionHandleDemo.Controllers.WeatherForecastController.GetWithoutExceptionHandler()
   at lambda_method24(Closure , Object , Object[] )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Création d’un ExceptionFilterAttribute

Microsoft nous a donné un moyen de gérer les exceptions après leur émission, vous pouvez consulter plus d’informations ici.

Mais jusqu’à présent, la documentation qu’ils nous donnent est la suivante :

Un filtre abstrait qui s’exécute de manière asynchrone après qu’une action a levé une exception. Les sous-classes doivent remplacer OnException(ExceptionContext) ou OnExceptionAsync(ExceptionContext) mais pas les deux.

Alors créons-en un, nous allons créer CustomExceptionFilterAttribute dans lequel nous allons remplacer OnException :

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;
using CustomExceptionHandleDemo.Exceptions;

namespace CustomExceptionHandleDemo.ExceptionFilterAttributes
{
    public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
    {
        /// <summary>
        /// OnException
        /// </summary>
        /// <param cref="ExceptionContext" name="context">Parameter for context</param>
        public override void OnException(ExceptionContext context)
        {
            if (context.Exception is CustomException)
            {
                context.HttpContext.Response.StatusCode = 500;
                context.Result = new ObjectResult(context.Exception.Message);
            }
        }
    }
}

Comme vous pouvez le voir, nous examinons le ExceptionContext, lorsque l’exception est un type de CustomException, nous faisons quelque chose.

Ce quelque chose met à jour la réponse et le code d’état de ce que nous allons retourner.

Afin de mettre à jour le code d’état, nous devons mettre à jour context.HttpContext.Response.StatusCode et pour renvoyer le résultat, nous devons mettre à jour le context.Result en lui donnant un objet hérité de ActionResult.

Il s’agit d’un filtre, cela signifie donc que nous devons lui ajouter quelque chose en ajoutant [CustomExceptionFilter].

Utiliser le filtre

Maintenant, reproduisons la méthode que nous avons et ajoutons ce filtre pour qu’il agisse, notre point de terminaison d’API finira comme ceci :

using CustomExceptionHandleDemo.ExceptionFilterAttributes;
using CustomExceptionHandleDemo.Exceptions;
using Microsoft.AspNetCore.Mvc;

namespace CustomExceptionHandleDemo.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        public WeatherForecastController()
        {
        }

        [HttpGet]
        [Route("GetWithoutExceptionHandler")]
        public Task GetWithoutExceptionHandler()
        {
            throw new CustomException("This is a custom exception!");
        }

        [HttpGet]
        [Route("GetWithExceptionHandler")]
        [CustomExceptionFilter]
        public Task GetWithExceptionHandler()
        {
            throw new CustomException("This is a custom exception!");
        }
    }
}

Comme vous pouvez le voir, nous avons une nouvelle méthode appelée GetWithExceptionHandler, qui a la même logique que GetWithoutExceptionHandler, mais dans ce cas, nous avons ajouté le filtre [CustomExceptionFilter] à la méthode.

Le résultat est le suivant après avoir exécuté la méthode, j’afficherai une image, car elle n’affiche plus la trace de la pile :

Nous avons donc créé une exception personnalisée, un filtre pour remplacer ce qui se passe lorsque nous lançons une exception et l’utilisons sur une méthode.

Cela peut être utilisé pour beaucoup de choses comme la journalisation et savoir quoi, quand et où l’erreur se produit.