Gérer les erreurs dans les API web ASP.NET Core

Cet article explique comment gérer les erreurs et personnaliser la gestion des erreurs avec ASP.NET Core API web.

Page d’exceptions du développeur

La page d’exception du développeur affiche des traces détaillées de pile pour les erreurs de serveur. Il utilise DeveloperExceptionPageMiddleware pour capturer des exceptions synchrones et asynchrones à partir du pipeline HTTP et pour générer des réponses d’erreur. Par exemple, considérez l’action de contrôleur suivante, qui lève une exception :

[HttpGet("Throw")]
public IActionResult Throw() =>
    throw new Exception("Sample exception.");

Lorsque la page d’exception du développeur détecte une exception non gérée, elle génère une réponse en texte brut par défaut similaire à l’exemple suivant :

HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

System.Exception: Sample exception.
   at HandleErrorsSample.Controllers.ErrorsController.Get() in ...
   at lambda_method1(Closure , Object , Object[] )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()

...

Si le client demande une réponse au format HTML, la page d’exception du développeur génère une réponse similaire à l’exemple suivant :

HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="utf-8" />
        <title>Internal Server Error</title>
        <style>
            body {
    font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif;
    font-size: .813em;
    color: #222;
    background-color: #fff;
}

h1 {
    color: #44525e;
    margin: 15px 0 15px 0;
}

...

Pour demander une réponse au format HTML, définissez l’en-tête Accept de requête HTTP sur text/html.

Avertissement

Activez la Page d’exception de développeur uniquement quand l’application est en cours d’exécution dans l’environnement de développement. Il n’est pas souhaitable de partager publiquement des informations détaillées sur les exceptions quand l’application s’exécute en production. Pour plus d’informations sur la configuration selon l’environnement, consultez Utiliser plusieurs environnements dans ASP.NET Core.

Gestionnaire d’exceptions

Dans les environnements hors développement, utilisez l’intergiciel de gestion des exceptions pour produire une erreur :

  1. Dans Program.cs, appelez UseExceptionHandler pour ajouter le middleware de gestion des exceptions :

    var app = builder.Build();
    
    app.UseHttpsRedirection();
    
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error");
    }
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    
  2. Configurez une action de contrôleur pour répondre à l’itinéraire /error :

    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

L’action précédente HandleError envoie une charge utile conforme À la norme RFC 7807 au client.

Avertissement

Ne marquez pas la méthode d’action du gestionnaire d’erreurs avec des attributs de méthode HTTP, tels que HttpGet. Les verbes explicites empêchent certaines demandes d’atteindre la méthode d’action.

Pour les API web qui utilisent Swagger/OpenAPI, marquez l’action du gestionnaire d’erreurs avec l’attribut [ApiExplorerSettings] et définissez sa propriété IgnoreApi sur true. Cette configuration d’attribut exclut l’action de gestionnaire d’erreurs de la spécification OpenAPI de l’application :

[ApiExplorerSettings(IgnoreApi = true)]

Autorisez l’accès anonyme à la méthode si les utilisateurs non authentifiés doivent consulter l’erreur.

L’intergiciel de gestion des exceptions peut également être utilisé dans l’environnement de développement pour produire un format de charge utile cohérent dans tous les environnements :

  1. Dans Program.cs, inscrivez les instances d’intergiciels de gestion des exceptions spécifiques à l’environnement :

    if (app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error-development");
    }
    else
    {
        app.UseExceptionHandler("/error");
    }
    

    Dans le code précédent, l’intergiciel est inscrit avec :

    • Itinéraire de /error-development dans l’environnement de développement.
    • Itinéraire de /error dans les environnements non-développement.

  2. Ajoutez des actions de contrôleur pour les routes Développement et Non Développement :

    [Route("/error-development")]
    public IActionResult HandleErrorDevelopment(
        [FromServices] IHostEnvironment hostEnvironment)
    {
        if (!hostEnvironment.IsDevelopment())
        {
            return NotFound();
        }
    
        var exceptionHandlerFeature =
            HttpContext.Features.Get<IExceptionHandlerFeature>()!;
    
        return Problem(
            detail: exceptionHandlerFeature.Error.StackTrace,
            title: exceptionHandlerFeature.Error.Message);
    }
    
    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

Utiliser des exceptions pour modifier la réponse

Le contenu de la réponse peut être modifié à l’extérieur du contrôleur à l’aide d’une exception personnalisée et d’un filtre d’action :

  1. Créez un type d’exception connu nommé HttpResponseException :

    public class HttpResponseException : Exception
    {
        public HttpResponseException(int statusCode, object? value = null) =>
            (StatusCode, Value) = (statusCode, value);
    
        public int StatusCode { get; }
    
        public object? Value { get; }
    }
    
  2. Créez un filtre d’action nommé HttpResponseExceptionFilter :

    public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
    {
        public int Order => int.MaxValue - 10;
    
        public void OnActionExecuting(ActionExecutingContext context) { }
    
        public void OnActionExecuted(ActionExecutedContext context)
        {
            if (context.Exception is HttpResponseException httpResponseException)
            {
                context.Result = new ObjectResult(httpResponseException.Value)
                {
                    StatusCode = httpResponseException.StatusCode
                };
    
                context.ExceptionHandled = true;
            }
        }
    }
    

    Le filtre précédent spécifie un Order de la valeur entière maximale moins 10. Ce Order permet à d’autres filtres de s’exécuter à la fin du pipeline.

  3. Dans Program.cs, ajoutez le filtre d’action à la collection filtres :

    builder.Services.AddControllers(options =>
    {
        options.Filters.Add<HttpResponseExceptionFilter>();
    });
    

Erreur de défaillance de validation des données

Pour les contrôleurs d’API web, MVC répond avec un ValidationProblemDetails type de réponse en cas d’échec de la validation du modèle. MVC utilise les résultats de InvalidModelStateResponseFactory pour construire la réponse d’erreur pour un échec de validation. L’exemple suivant remplace la fabrique par défaut par une implémentation qui prend également en charge la mise en forme des réponses au format XML, dans Program.cs :

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
            new BadRequestObjectResult(context.ModelState)
            {
                ContentTypes =
                {
                    // using static System.Net.Mime.MediaTypeNames;
                    Application.Json,
                    Application.Xml
                }
            };
    })
    .AddXmlSerializerFormatters();

Réponse d’erreur du client

Un résultat d’erreur est défini comme résultat avec un code d’état HTTP 400 ou supérieur. Pour les contrôleurs d’API web, MVC transforme un résultat d’erreur pour produire un ProblemDetails.

La création automatique d’un pour les ProblemDetails codes d’état d’erreur est activée par défaut, mais les réponses d’erreur peuvent être configurées de l’une des manières suivantes :

  1. Utiliser le service des détails du problème
  2. Implémenter ProblemDetailsFactory
  3. Utiliser ApiBehaviorOptions.ClientErrorMapping

Réponse des détails du problème par défaut

Le fichier suivant Program.cs a été généré par les modèles d’application web pour les contrôleurs d’API :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Considérez le contrôleur suivant, qui retourne BadRequest lorsque l’entrée n’est pas valide :

[Route("api/[controller]/[action]")]
[ApiController]
public class Values2Controller : ControllerBase
{
    // /api/values2/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values2 /squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }
}

Une réponse des détails du problème est générée avec le code précédent lorsque l’une des conditions suivantes s’applique :

  • Le point de terminaison /api/values2/divide est appelé avec un dénominateur zéro.
  • Le point de terminaison /api/values2/squareroot est appelé avec un radicand inférieur à zéro.

Le corps de la réponse des détails du problème par défaut a les valeurs , typeet titlestatus suivantes :

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "Bad Request",
  "status": 400,
  "traceId": "00-84c1fd4063c38d9f3900d06e56542d48-85d1d4-00"
}

Service détails du problème

ASP.NET Core prend en charge la création de détails du problème pour les API HTTP à l’aide de IProblemDetailsService. Pour plus d’informations, consultez le service Détails du problème.

Le code suivant configure l’application pour générer une réponse sur les détails du problème pour toutes les réponses d’erreur de serveur et de client HTTP qui n’ont pas encore de contenu de corps :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapControllers();
app.Run();

Considérez le contrôleur d’API de la section précédente, qui retourne BadRequest lorsque l’entrée n’est pas valide :

[Route("api/[controller]/[action]")]
[ApiController]
public class Values2Controller : ControllerBase
{
    // /api/values2/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values2 /squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }
}

Une réponse des détails du problème est générée avec le code précédent lorsque l’une des conditions suivantes s’applique :

  • Une entrée non valide est fournie.
  • L’URI n’a pas de point de terminaison correspondant.
  • Une exception non prise en charge se produit.

La création automatique d’un ProblemDetails pour les codes d’état d’erreur est désactivée quand la propriété SuppressMapClientErrors est définie sur true :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressMapClientErrors = true;
    });

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

À l’aide du code précédent, lorsqu’un contrôleur d’API retourne BadRequest, un état de réponse HTTP 400 est retourné sans corps de réponse. SuppressMapClientErrors empêche la création d’une ProblemDetails réponse, même lors de l’appel WriteAsync d’un point de terminaison de contrôleur d’API. WriteAsync est expliqué plus loin dans cet article.

La section suivante montre comment personnaliser le corps de la réponse des détails du problème, à l’aide de CustomizeProblemDetails, pour retourner une réponse plus utile. Pour plus d’options de personnalisation, consultez Personnalisation des détails du problème.

Personnaliser les détails du problème avec CustomizeProblemDetails

Le code suivant utilise ProblemDetailsOptions pour définir CustomizeProblemDetails :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails(options =>
        options.CustomizeProblemDetails = (context) =>
        {

            var mathErrorFeature = context.HttpContext.Features
                                                       .Get<MathErrorFeature>();
            if (mathErrorFeature is not null)
            {
                (string Detail, string Type) details = mathErrorFeature.MathError switch
                {
                    MathErrorType.DivisionByZeroError =>
                    ("Divison by zero is not defined.",
                                          "https://wikipedia.org/wiki/Division_by_zero"),
                    _ => ("Negative or complex numbers are not valid input.",
                                          "https://wikipedia.org/wiki/Square_root")
                };

                context.ProblemDetails.Type = details.Type;
                context.ProblemDetails.Title = "Bad Input";
                context.ProblemDetails.Detail = details.Detail;
            }
        }
    );

var app = builder.Build();

app.UseHttpsRedirection();

app.UseStatusCodePages();

app.UseAuthorization();

app.MapControllers();

app.Run();

Contrôleur d’API mis à jour :

[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    // /api/values/divide/1/2
    [HttpGet("{Numerator}/{Denominator}")]
    public IActionResult Divide(double Numerator, double Denominator)
    {
        if (Denominator == 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.DivisionByZeroError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }

        return Ok(Math.Sqrt(radicand));
    }

}

Le code suivant contient les MathErrorFeature et MathErrorType, qui sont utilisés avec l’exemple précédent :

// Custom Http Request Feature
class MathErrorFeature
{
    public MathErrorType MathError { get; set; }
}

// Custom math errors
enum MathErrorType
{
    DivisionByZeroError,
    NegativeRadicandError
}

Une réponse des détails du problème est générée avec le code précédent lorsque l’une des conditions suivantes s’applique :

  • Le point de terminaison /divide est appelé avec un dénominateur zéro.
  • Le point de terminaison /squareroot est appelé avec un radicand inférieur à zéro.
  • L’URI n’a pas de point de terminaison correspondant.

Le corps de la réponse des détails du problème contient les éléments suivants lorsque l’un des points de terminaison squarerootest appelé avec un radicand inférieur à zéro :

{
  "type": "https://en.wikipedia.org/wiki/Square_root",
  "title": "Bad Input",
  "status": 400,
  "detail": "Negative or complex numbers are not allowed."
}

Afficher ou télécharger l’exemple de code

Implémentez ProblemDetailsFactory

MVC utilise Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory pour produire toutes les instances de ProblemDetails et ValidationProblemDetails. Cette fabrique est utilisée pour :

Pour personnaliser la réponse aux détails du problème, inscrivez une implémentation personnalisée de ProblemDetailsFactory dans Program.cs :

builder.Services.AddControllers();
builder.Services.AddTransient<ProblemDetailsFactory, SampleProblemDetailsFactory>();

Utilisez ApiBehaviorOptions.ClientErrorMapping.

Utilisez la propriété ClientErrorMapping pour configurer le contenu de la réponse ProblemDetails. Par exemple, le code suivant dans Program.cs met à jour la propriété Link pour les réponses 404 :

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });

Ressources supplémentaires

Cet article explique comment gérer les erreurs et personnaliser la gestion des erreurs avec ASP.NET Core API web.

Page d’exceptions du développeur

La page d’exception du développeur affiche des traces détaillées de pile pour les erreurs de serveur. Il utilise DeveloperExceptionPageMiddleware pour capturer des exceptions synchrones et asynchrones à partir du pipeline HTTP et pour générer des réponses d’erreur. Par exemple, considérez l’action de contrôleur suivante, qui lève une exception :

[HttpGet("Throw")]
public IActionResult Throw() =>
    throw new Exception("Sample exception.");

Lorsque la page d’exception du développeur détecte une exception non gérée, elle génère une réponse en texte brut par défaut similaire à l’exemple suivant :

HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

System.Exception: Sample exception.
   at HandleErrorsSample.Controllers.ErrorsController.Get() in ...
   at lambda_method1(Closure , Object , Object[] )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()

...

Si le client demande une réponse au format HTML, la page d’exception du développeur génère une réponse similaire à l’exemple suivant :

HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="utf-8" />
        <title>Internal Server Error</title>
        <style>
            body {
    font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif;
    font-size: .813em;
    color: #222;
    background-color: #fff;
}

h1 {
    color: #44525e;
    margin: 15px 0 15px 0;
}

...

Pour demander une réponse au format HTML, définissez l’en-tête Accept de requête HTTP sur text/html.

Avertissement

Activez la Page d’exception de développeur uniquement quand l’application est en cours d’exécution dans l’environnement de développement. Il n’est pas souhaitable de partager publiquement des informations détaillées sur les exceptions quand l’application s’exécute en production. Pour plus d’informations sur la configuration selon l’environnement, consultez Utiliser plusieurs environnements dans ASP.NET Core.

Gestionnaire d’exceptions

Dans les environnements hors développement, utilisez l’intergiciel de gestion des exceptions pour produire une erreur :

  1. Dans Program.cs, appelez UseExceptionHandler pour ajouter le middleware de gestion des exceptions :

    var app = builder.Build();
    
    app.UseHttpsRedirection();
    
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error");
    }
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    
  2. Configurez une action de contrôleur pour répondre à l’itinéraire /error :

    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

L’action précédente HandleError envoie une charge utile conforme À la norme RFC 7807 au client.

Avertissement

Ne marquez pas la méthode d’action du gestionnaire d’erreurs avec des attributs de méthode HTTP, tels que HttpGet. Les verbes explicites empêchent certaines demandes d’atteindre la méthode d’action.

Pour les API web qui utilisent Swagger/OpenAPI, marquez l’action du gestionnaire d’erreurs avec l’attribut [ApiExplorerSettings] et définissez sa propriété IgnoreApi sur true. Cette configuration d’attribut exclut l’action de gestionnaire d’erreurs de la spécification OpenAPI de l’application :

[ApiExplorerSettings(IgnoreApi = true)]

Autorisez l’accès anonyme à la méthode si les utilisateurs non authentifiés doivent consulter l’erreur.

L’intergiciel de gestion des exceptions peut également être utilisé dans l’environnement de développement pour produire un format de charge utile cohérent dans tous les environnements :

  1. Dans Program.cs, inscrivez les instances d’intergiciels de gestion des exceptions spécifiques à l’environnement :

    if (app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/error-development");
    }
    else
    {
        app.UseExceptionHandler("/error");
    }
    

    Dans le code précédent, l’intergiciel est inscrit avec :

    • Itinéraire de /error-development dans l’environnement de développement.
    • Itinéraire de /error dans les environnements non-développement.

  2. Ajoutez des actions de contrôleur pour les routes Développement et Non Développement :

    [Route("/error-development")]
    public IActionResult HandleErrorDevelopment(
        [FromServices] IHostEnvironment hostEnvironment)
    {
        if (!hostEnvironment.IsDevelopment())
        {
            return NotFound();
        }
    
        var exceptionHandlerFeature =
            HttpContext.Features.Get<IExceptionHandlerFeature>()!;
    
        return Problem(
            detail: exceptionHandlerFeature.Error.StackTrace,
            title: exceptionHandlerFeature.Error.Message);
    }
    
    [Route("/error")]
    public IActionResult HandleError() =>
        Problem();
    

Utiliser des exceptions pour modifier la réponse

Le contenu de la réponse peut être modifié à l’extérieur du contrôleur à l’aide d’une exception personnalisée et d’un filtre d’action :

  1. Créez un type d’exception connu nommé HttpResponseException :

    public class HttpResponseException : Exception
    {
        public HttpResponseException(int statusCode, object? value = null) =>
            (StatusCode, Value) = (statusCode, value);
    
        public int StatusCode { get; }
    
        public object? Value { get; }
    }
    
  2. Créez un filtre d’action nommé HttpResponseExceptionFilter :

    public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
    {
        public int Order => int.MaxValue - 10;
    
        public void OnActionExecuting(ActionExecutingContext context) { }
    
        public void OnActionExecuted(ActionExecutedContext context)
        {
            if (context.Exception is HttpResponseException httpResponseException)
            {
                context.Result = new ObjectResult(httpResponseException.Value)
                {
                    StatusCode = httpResponseException.StatusCode
                };
    
                context.ExceptionHandled = true;
            }
        }
    }
    

    Le filtre précédent spécifie un Order de la valeur entière maximale moins 10. Ce Order permet à d’autres filtres de s’exécuter à la fin du pipeline.

  3. Dans Program.cs, ajoutez le filtre d’action à la collection filtres :

    builder.Services.AddControllers(options =>
    {
        options.Filters.Add<HttpResponseExceptionFilter>();
    });
    

Erreur de défaillance de validation des données

Pour les contrôleurs d’API web, MVC répond avec un ValidationProblemDetails type de réponse en cas d’échec de la validation du modèle. MVC utilise les résultats de InvalidModelStateResponseFactory pour construire la réponse d’erreur pour un échec de validation. L’exemple suivant remplace la fabrique par défaut par une implémentation qui prend également en charge la mise en forme des réponses au format XML, dans Program.cs :

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
            new BadRequestObjectResult(context.ModelState)
            {
                ContentTypes =
                {
                    // using static System.Net.Mime.MediaTypeNames;
                    Application.Json,
                    Application.Xml
                }
            };
    })
    .AddXmlSerializerFormatters();

Réponse d’erreur du client

Un résultat d’erreur est défini comme résultat avec un code d’état HTTP 400 ou supérieur. Pour les contrôleurs d’API web, MVC transforme un résultat d’erreur pour produire un ProblemDetails.

La réponse d’erreur peut être configurée de l’une des manières suivantes :

  1. Implémenter ProblemDetailsFactory
  2. Utiliser ApiBehaviorOptions.ClientErrorMapping

Implémentez ProblemDetailsFactory

MVC utilise Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory pour produire toutes les instances de ProblemDetails et ValidationProblemDetails. Cette fabrique est utilisée pour :

Pour personnaliser la réponse aux détails du problème, inscrivez une implémentation personnalisée de ProblemDetailsFactory dans Program.cs :

builder.Services.AddControllers();
builder.Services.AddTransient<ProblemDetailsFactory, SampleProblemDetailsFactory>();

Utilisez ApiBehaviorOptions.ClientErrorMapping.

Utilisez la propriété ClientErrorMapping pour configurer le contenu de la réponse ProblemDetails. Par exemple, le code suivant dans Program.cs met à jour la propriété Link pour les réponses 404 :

builder.Services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });

Intergiciel personnalisé pour gérer les exceptions

Les valeurs par défaut dans le middleware de gestion des exceptions fonctionnent bien pour la plupart des applications. Pour les applications qui nécessitent une gestion spécialisée des exceptions, envisagez de personnaliser l’intergiciel de gestion des exceptions.

Produire une charge utile ProblemDetails pour les exceptions

ASP.NET Core ne produit pas de charge utile d’erreur standardisée lorsqu’une exception non prise en charge se produit. Pour les scénarios où il est souhaitable de retourner une réponse ProblemDetails standardisée au client, l’intergiciel ProblemDetails peut être utilisé pour mapper des exceptions et des réponses 404 à une charge utile ProblemDetails. Le middleware de gestion des exceptions peut également être utilisé pour renvoyer une charge utile ProblemDetails pour les exceptions non gérées.

Ressources supplémentaires

Cet article explique comment gérer et personnaliser la gestion des erreurs avec ASP.NET Core API web.

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

Page d’exceptions du développeur

La page d’exception du développeur est un outil utile pour obtenir des traces de pile détaillées pour les erreurs de serveur. Il utilise DeveloperExceptionPageMiddleware pour capturer des exceptions synchrones et asynchrones à partir du pipeline HTTP et pour générer des réponses d’erreur. Pour l’illustrer, considérez l’action de contrôleur suivante :

[HttpGet("{city}")]
public WeatherForecast Get(string city)
{
    if (!string.Equals(city?.TrimEnd(), "Redmond", StringComparison.OrdinalIgnoreCase))
    {
        throw new ArgumentException(
            $"We don't offer a weather forecast for {city}.", nameof(city));
    }
    
    return GetWeather().First();
}

Exécutez la commande suivante curl pour tester l’action précédente :

curl -i https://localhost:5001/weatherforecast/chicago

La page Exception du développeur affiche une réponse en texte brut si le client ne demande pas de sortie au format HTML. Vous obtenez la sortie suivante :

HTTP/1.1 500 Internal Server Error
Transfer-Encoding: chunked
Content-Type: text/plain
Server: Microsoft-IIS/10.0
X-Powered-By: ASP.NET
Date: Fri, 27 Sep 2019 16:13:16 GMT

System.ArgumentException: We don't offer a weather forecast for chicago. (Parameter 'city')
   at WebApiSample.Controllers.WeatherForecastController.Get(String city) in C:\working_folder\aspnet\AspNetCore.Docs\aspnetcore\web-api\handle-errors\samples\3.x\Controllers\WeatherForecastController.cs:line 34
   at lambda_method(Closure , Object , Object[] )
   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.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 where exception was thrown ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_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.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

HEADERS
=======
Accept: */*
Host: localhost:44312
User-Agent: curl/7.55.1

Pour afficher une réponse au format HTML à la place, définissez Acceptl’en-tête de requête HTTP sur le type de média text/html. Par exemple :

curl -i -H "Accept: text/html" https://localhost:5001/weatherforecast/chicago

Considérez l’extrait suivant de la réponse HTTP :

HTTP/1.1 500 Internal Server Error
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf-8
Server: Microsoft-IIS/10.0
X-Powered-By: ASP.NET
Date: Fri, 27 Sep 2019 16:55:37 GMT

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="utf-8" />
        <title>Internal Server Error</title>
        <style>
            body {
    font-family: 'Segoe UI', Tahoma, Arial, Helvetica, sans-serif;
    font-size: .813em;
    color: #222;
    background-color: #fff;
}

La réponse au format HTML devient utile lors du test via des outils tels que Postman. La capture d’écran suivante montre à la fois le texte brut et les réponses au format HTML dans Postman :

Test the Developer Exception Page in Postman.

Avertissement

Activez la page d’exception de développeur uniquement quand l’application est en cours d’exécution dans l’environnement de développement. Il n’est pas souhaitable de partager publiquement des informations détaillées sur les exceptions quand l’application s’exécute en production. Pour plus d’informations sur la configuration selon l’environnement, consultez Utiliser plusieurs environnements dans ASP.NET Core.

Ne marquez pas la méthode d’action du gestionnaire d’erreurs avec des attributs de méthode HTTP, tels que HttpGet. Les verbes explicites empêchent certaines demandes d’atteindre la méthode d’action. Autorisez l’accès anonyme à la méthode si les utilisateurs non authentifiés doivent consulter l’erreur.

Gestionnaire d’exceptions

Dans les environnements hors développement, l’intergiciel de gestion des exceptions peut être utilisé pour produire une charge utile d’erreur :

  1. Dans Startup.Configure, appelez UseExceptionHandler pour utiliser l’intergiciel :

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/error");
        }
    
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  2. Configurez une action de contrôleur pour répondre à l’itinéraire /error :

    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error")]
        public IActionResult Error() => Problem();
    }
    

L’action précédente Error envoie une charge utile conforme À la norme RFC 7807 au client.

L’intergiciel de gestion des exceptions peut également fournir une sortie négociée de contenu plus détaillée dans l’environnement de développement local. Utilisez les étapes suivantes pour produire un format de charge utile cohérent dans les environnements de développement et de production :

  1. Dans Startup.Configure, inscrivez les instances d’intergiciels de gestion des exceptions spécifiques à l’environnement :

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseExceptionHandler("/error-local-development");
        }
        else
        {
            app.UseExceptionHandler("/error");
        }
    }
    

    Dans le code précédent, l’intergiciel est inscrit avec :

    • Itinéraire de /error-local-development dans l’environnement de développement.
    • Itinéraire de /error dans les environnements qui ne sont pas développement.

  2. Appliquez le routage d’attributs aux actions du contrôleur :

    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error-local-development")]
        public IActionResult ErrorLocalDevelopment(
            [FromServices] IWebHostEnvironment webHostEnvironment)
        {
            if (webHostEnvironment.EnvironmentName != "Development")
            {
                throw new InvalidOperationException(
                    "This shouldn't be invoked in non-development environments.");
            }
    
            var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
    
            return Problem(
                detail: context.Error.StackTrace,
                title: context.Error.Message);
        }
    
        [Route("/error")]
        public IActionResult Error() => Problem();
    }
    

    Le code précédent appelle ControllerBase.Problem pour créer une réponse ProblemDetails.

Utiliser des exceptions pour modifier la réponse

Le contenu de la réponse peut être modifié à partir de l’extérieur du contrôleur. Dans ASP.NET API web 4.x, une façon de procéder était d’utiliser le type HttpResponseException. ASP.NET Core n’inclut pas de type équivalent. La prise en charge de HttpResponseException peut être ajoutée en procédant comme suit :

  1. Créez un type d’exception connu nommé HttpResponseException :

    public class HttpResponseException : Exception
    {
        public int Status { get; set; } = 500;
    
        public object Value { get; set; }
    }
    
  2. Créez un filtre d’action nommé HttpResponseExceptionFilter :

    public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
    {
        public int Order { get; } = int.MaxValue - 10;
    
        public void OnActionExecuting(ActionExecutingContext context) { }
    
        public void OnActionExecuted(ActionExecutedContext context)
        {
            if (context.Exception is HttpResponseException exception)
            {
                context.Result = new ObjectResult(exception.Value)
                {
                    StatusCode = exception.Status,
                };
                context.ExceptionHandled = true;
            }
        }
    }
    

    Le filtre précédent spécifie un Order de la valeur entière maximale moins 10. Ce Order permet à d’autres filtres de s’exécuter à la fin du pipeline.

  3. Dans Startup.ConfigureServices, ajoutez le filtre d’action à la collection filtres :

    services.AddControllers(options =>
        options.Filters.Add(new HttpResponseExceptionFilter()));
    

Erreur de défaillance de validation des données

Pour les contrôleurs d’API web, MVC répond avec un ValidationProblemDetails type de réponse en cas d’échec de la validation du modèle. MVC utilise les résultats de InvalidModelStateResponseFactory pour construire la réponse d’erreur pour un échec de validation. L’exemple suivant utilise la fabrique pour modifier le type de réponse par défaut sur SerializableError dans Startup.ConfigureServices :

services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            var result = new BadRequestObjectResult(context.ModelState);

            // TODO: add `using System.Net.Mime;` to resolve MediaTypeNames
            result.ContentTypes.Add(MediaTypeNames.Application.Json);
            result.ContentTypes.Add(MediaTypeNames.Application.Xml);

            return result;
        };
    });

Réponse d’erreur du client

Un résultat d’erreur est défini comme résultat avec un code d’état HTTP 400 ou supérieur. Pour les contrôleurs d’API web, MVC transforme un résultat d’erreur en résultat avec ProblemDetails.

La réponse d’erreur peut être configurée de l’une des manières suivantes :

  1. Implémenter ProblemDetailsFactory
  2. Utiliser ApiBehaviorOptions.ClientErrorMapping

Implémentez ProblemDetailsFactory

MVC utilise Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory pour produire toutes les instances de ProblemDetails et ValidationProblemDetails. Cette fabrique est utilisée pour :

Pour personnaliser la réponse aux détails du problème, inscrivez une implémentation personnalisée de ProblemDetailsFactory dans Startup.ConfigureServices :

public void ConfigureServices(IServiceCollection serviceCollection)
{
    services.AddControllers();
    services.AddTransient<ProblemDetailsFactory, CustomProblemDetailsFactory>();
}

Utiliser ApiBehaviorOptions.ClientErrorMapping

Utilisez la propriété ClientErrorMapping pour configurer le contenu de la réponse ProblemDetails. Par exemple, le code suivant dans Startup.ConfigureServices met à jour la propriété type pour les réponses 404 :

services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
        options.DisableImplicitFromServicesParameters = true;
    });

Intergiciel personnalisé pour gérer les exceptions

Les valeurs par défaut dans le middleware de gestion des exceptions fonctionnent bien pour la plupart des applications. Pour les applications qui nécessitent une gestion spécialisée des exceptions, envisagez de personnaliser l’intergiciel de gestion des exceptions.

Production d’une charge utile ProblemDetails pour les exceptions

ASP.NET Core ne produit pas de charge utile d’erreur standardisée lorsqu’une exception non prise en charge se produit. Pour les scénarios où il est souhaitable de retourner une réponse ProblemDetails standardisée au client, l’intergiciel ProblemDetails peut être utilisé pour mapper des exceptions et des réponses 404 à une charge utile ProblemDetails. Le middleware de gestion des exceptions peut également être utilisé pour renvoyer une charge utile ProblemDetails pour les exceptions non gérées.