Gérer les erreurs dans ASP.NET Core

Remarque

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 8 de cet article.

Par Tom Dykstra

Cet article aborde des approches courantes pour gérer les erreurs dans les applications web ASP.NET Core. Consultez également Gérer les erreurs dans les API web ASP.NET Core et Gérer les erreurs dans les applications API minimales.

Page d’exceptions du développeur

La Page d’exception du développeur affiche des informations détaillées sur les exceptions des requêtes non prises en charge. Les applications ASP.NET Core activent la page d’exception du développeur par défaut en présence des deux éléments suivants :

La page d’exception du développeur s’exécute au début du pipeline intergiciel, de manière qu’elle puisse intercepter les exceptions non prises en charge levées dans l’intergiciel qui suit.

Des informations détaillées sur les exceptions ne doivent pas être affichées publiquement lorsque l’application s’exécute dans l’environnement de production. Pour plus d’informations sur la configuration des environnements, consultez Utiliser plusieurs environnements dans ASP.NET Core.

La page d’exception du développeur peut inclure les informations suivantes sur l’exception et la requête :

  • Trace de pile
  • Paramètres de la chaîne de requête, le cas échéant
  • Cookie, le cas échéant
  • En-têtes
  • Métadonnées du point de terminaison, le cas échéant

L’image suivante montre un exemple de page d’exception du développeur avec le Routage sélectionné et les métadonnées du point de terminaison affichées :

Page d’exception du développeur avec le routage sélectionné et les métadonnées du point de terminaison affichées

Il n’est pas garanti que la page d’exception du développeur fournisse des informations. Utilisez la journalisation pour obtenir des informations complètes sur l’erreur.

Page Gestionnaire d’exceptions

Pour configurer une page de gestion des erreurs personnalisée en fonction de l’environnement de production, appelez UseExceptionHandler. Cet intergiciel de gestion des exceptions :

  • Intercepte et consigne les exceptions non prises en charge.
  • Réexécute la requête dans un pipeline de remplacement à l’aide du chemin d’accès indiqué. La demande n’est pas réexécutée si la réponse a démarré. Le code généré par le modèle réexécute la requête à l’aide du chemin d’accès /Error.

Warning

Si le pipeline de remplacement lève une exception qui lui est propre, l’intergiciel de gestion des exception lève à nouveau l’exception d’origine.

Étant donné que cet intergiciel peut réexécuter le pipeline de requête :

  • Les intergiciels doivent gérer la réentrance avec la même requête. Cela signifie normalement le nettoyage de leur état après l’appel _next ou la mise en cache de leur traitement sur HttpContext pour éviter de le rétablir. Lorsque vous traitez le corps de la requête, cela peut signifier une mise en mémoire tampon ou une mise en cache des résultats, comme le lecteur de formulaire.
  • Pour la surcharge UseExceptionHandler(IApplicationBuilder, String) utilisée dans les modèles, seul le chemin d’accès de la requête est modifié et les données de routage sont effacées. Les données de requête telles que les en-têtes, la méthode et les éléments sont toutes réutilisées telles quelles.
  • Les services délimités restent les mêmes.

Dans l’exemple suivant, UseExceptionHandler ajoute l’intergiciel de gestion des exceptions dans des environnements hors développement :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Le modèle d’application des pages Razor fournit une page d’erreur (.cshtml) et une classe PageModel (ErrorModel) dans le dossier Pages. Pour une application MVC, le modèle de projet comprend une méthode d’action Error et un affichage d’erreur pour le contrôleur Home.

L’intergiciel de gestion des exceptions réexécute la requête à l’aide de la méthode HTTP d’origine. Si un point de terminaison de gestionnaire d’erreurs est limité à un ensemble spécifique de méthodes HTTP, il ne s’exécute que pour ces méthodes HTTP. Par exemple, une action de contrôleur MVC qui utilise l’attribut [HttpGet] s’exécute uniquement pour les requêtes GET. Pour garantir que toutes les requêtes atteignent la page de gestion des erreurs personnalisée, ne les limitez pas à un ensemble spécifique de méthodes HTTP.

Pour gérer les exceptions différemment en fonction de la méthode HTTP d’origine :

  • Pour les pages Razor, créez plusieurs méthodes de gestionnaire. Par exemple, utilisez OnGet pour gérer les exceptions GET et OnPost pour gérer les exceptions POST.
  • Pour MVC, appliquez des attributs de verbe HTTP à plusieurs actions. Vous pouvez par exemple utiliser [HttpGet] pour gérer les exceptions GET et [HttpPost] pour gérer les exceptions POST.

Pour autoriser les utilisateurs non authentifiés à afficher la page de gestion des erreurs personnalisée, assurez-vous qu’elle prend en charge l’accès anonyme.

Accéder à l'exception

Utilisez IExceptionHandlerPathFeature pour accéder à l’exception et au chemin d’accès de la requête d’origine dans un gestionnaire d’erreurs. L’exemple suivant utilise IExceptionHandlerPathFeature pour obtenir plus d’informations sur l’exception levée :

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}

Warning

Ne communiquez pas d’informations sensibles sur les erreurs aux clients. Cela représenterait un risque de sécurité.

Expression lambda Gestionnaire d’exceptions

En dehors d’une page de gestionnaire d’exceptions personnalisée, il est possible de fournir une expression lambda à UseExceptionHandler, ce qui permet d’accéder à l’erreur avant de retourner la réponse.

Le code suivant utilise une expression lambda pour la gestion des exceptions :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}

Warning

Ne communiquez pas d’informations sensibles sur les erreurs aux clients. Cela représenterait un risque de sécurité.

IExceptionHandler

IExceptionHandler est une interface qui donne au développeur un rappel pour la gestion des exceptions connues dans un emplacement central.

Les implémentations IExceptionHandler sont inscrites en appelant IServiceCollection.AddExceptionHandler<T>. La durée de vie d’une instance de IExceptionHandler est singleton. Plusieurs implémentations peuvent être ajoutées et elles sont appelées dans l’ordre inscrit.

Si un gestionnaire d’exceptions gère une requête, il peut renvoyer true pour arrêter le traitement. Si une exception n’est gérée par aucun gestionnaire d’exceptions, le contrôle revient au comportement et aux options par défaut de l’intergiciel. Différentes métriques et journaux d’activité sont émis pour les exceptions gérées et non gérées.

L'exemple suivant montre une implémentation de IExceptionHandler :

using Microsoft.AspNetCore.Diagnostics;

namespace ErrorHandlingSample
{
    public class CustomExceptionHandler : IExceptionHandler
    {
        private readonly ILogger<CustomExceptionHandler> logger;
        public CustomExceptionHandler(ILogger<CustomExceptionHandler> logger)
        {
            this.logger = logger;
        }
        public ValueTask<bool> TryHandleAsync(
            HttpContext httpContext,
            Exception exception,
            CancellationToken cancellationToken)
        {
            var exceptionMessage = exception.Message;
            logger.LogError(
                "Error Message: {exceptionMessage}, Time of occurrence {time}",
                exceptionMessage, DateTime.UtcNow);
            // Return false to continue with the default behavior
            // - or - return true to signal that this exception is handled
            return ValueTask.FromResult(false);
        }
    }
}

L’exemple suivant montre comment inscrire une implémentation de IExceptionHandler pour l’injection de dépendances :

using ErrorHandlingSample;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// Remaining Program.cs code omitted for brevity

Lorsque le code précédent est exécuté dans l'environnement de développement :

  • La CustomExceptionHandler est appelée en premier pour gérer une exception.
  • Après avoir enregistré l'exception, la méthode TryHandleException retourne false, de sorte que la page d’exception développeur s’affiche.

Dans d’autres environnements :

  • La CustomExceptionHandler est appelée en premier pour gérer une exception.
  • Après avoir enregistré l'exception, la méthode TryHandleException retourne false, de sorte que la /Errorpage s’affiche.

UseStatusCodePages

Par défaut, une application ASP.NET Core ne fournit pas une page de codes d’état pour les codes d’état des erreurs HTTP, comme 404 – Introuvable. Lorsque l’application définit un code d’état d’erreur HTTP 400-599 qui n’a pas de corps, elle retourne le code d’état et un corps de réponse vide. Pour activer les gestionnaires de texte uniquement par défaut pour les codes d’état d’erreur courants, appelez UseStatusCodePages dans Program.cs :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();

Appelez UseStatusCodePages avant l’intergiciel de gestion des requêtes. Par exemple, appelez UseStatusCodePages avant l’intergiciel de fichier statique et l’intergiciel des points de terminaison.

Quand UseStatusCodePages n’est pas utilisé, la navigation vers une URL sans point de terminaison retourne un message d’erreur dépendant du navigateur indiquant que le point de terminaison est introuvable. Quand UseStatusCodePages est appelé, le navigateur retourne la réponse suivante :

Status Code: 404; Not Found

UseStatusCodePages n’est généralement pas utilisé en production, car il retourne un message qui n’est pas utile pour les utilisateurs.

Remarque

L’intergiciel des pages de codes d’état n’intercepte pas les exceptions. Pour fournir une page de gestion des erreurs personnalisée, utilisez la page du gestionnaire d’exception.

UseStatusCodePages avec chaîne de format

Pour personnaliser le texte et le type de contenu de la réponse, utilisez la surcharge de UseStatusCodePages qui prend un type de contenu et une chaîne de format :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");

Dans le code précédent, {0} est un espace réservé pour le code d’erreur.

UseStatusCodePages avec une chaîne de format n’est généralement pas utilisé en production, car il retourne un message qui n’est pas utile pour les utilisateurs.

UseStatusCodePages avec expression lambda

Pour spécifier un code personnalisé de gestion des erreurs et d’écriture de la réponse, utilisez la surcharge de UseStatusCodePages qui prend une expression lambda :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});

UseStatusCodePages avec une expression lambda n’est généralement pas utilisé en production, car il retourne un message qui n’est pas utile pour les utilisateurs.

UseStatusCodePagesWithRedirects

La méthode d’extension UseStatusCodePagesWithRedirects :

  • Envoie un code d’état 302 - Trouvé au client.
  • Redirige le client vers le point de terminaison de gestion des erreurs fourni dans le modèle d’URL. Le point de terminaison de gestion des erreurs affiche généralement les informations d’erreur et retourne HTTP 200.
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

Le modèle d’URL peut comporter un espace réservé {0} pour le code d’état, comme indiqué dans le code précédent. Si le modèle d’URL commence par un ~ (tilde), le ~ est remplacé par le PathBase de l’application. Lorsque vous spécifiez un point de terminaison dans l’application, créez une vue MVC ou une page Razor pour le point de terminaison.

Cette méthode est généralement utilisée lorsque l’application :

  • Doit rediriger le client vers un autre point de terminaison, généralement dans les cas où une autre application traite l’erreur. Pour les applications web, la barre d’adresses du navigateur client reflète le point de terminaison redirigé.
  • Ne doit pas conserver ni retourner le code d’état d’origine avec la réponse de redirection initiale.

UseStatusCodePagesWithReExecute

La méthode d’extension UseStatusCodePagesWithReExecute :

  • Génère le corps de la réponse en réexécutant le pipeline de requête avec un autre chemin.
  • Ne modifie pas le code d’état avant ou après la réexécution du pipeline.

L’exécution du nouveau pipeline peut modifier le code d’état de la réponse, car le nouveau pipeline dispose d’un contrôle total sur le code d’état. Si le nouveau pipeline ne modifie pas le code d’état, le code d’état d’origine sera envoyé au client.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");

Si un point de terminaison dans l’application est spécifié, créez une vue MVC ou une page Razor pour le point de terminaison.

Cette méthode est généralement utilisée lorsque l’application doit :

  • Traiter la demande sans la rediriger vers un autre point de terminaison. Pour les applications web, la barre d’adresses du navigateur client reflète le point de terminaison demandé à l’origine.
  • Conserver et retourner le code d’état d’origine avec la réponse.

Le modèle d’URL doit commencer par / et peut inclure un espace réservé {0} pour le code d’état. Pour faire passer le code d’état en tant que paramètre de chaîne de requête, passez un deuxième argument dans UseStatusCodePagesWithReExecute. Exemple :

var app = builder.Build();  
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");

Le point de terminaison qui traite l’erreur peut récupérer l’URL d’origine qui a généré l’erreur, comme dans l’exemple suivant :

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = string.Join(
                statusCodeReExecuteFeature.OriginalPathBase,
                statusCodeReExecuteFeature.OriginalPath,
                statusCodeReExecuteFeature.OriginalQueryString);
        }
    }
}

Étant donné que cet intergiciel peut réexécuter le pipeline de requête :

  • Les intergiciels doivent gérer la réentrance avec la même requête. Cela signifie normalement le nettoyage de leur état après l’appel _next ou la mise en cache de leur traitement sur HttpContext pour éviter de le rétablir. Lorsque vous traitez le corps de la requête, cela peut signifier une mise en mémoire tampon ou une mise en cache des résultats, comme le lecteur de formulaire.
  • Les services délimités restent les mêmes.

Désactiver les pages de codes d’état

Pour désactiver les pages de codes d’état d’un contrôleur MVC ou d’une méthode d’action, utilisez l’attribut [SkipStatusCodePages].

Pour désactiver les pages de codes d’état pour des requêtes spécifiques dans une méthode de gestionnaire des pages IStatusCodePagesFeature ou dans un contrôleur MVC, utilisez Razor :

public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

Code de gestion des exceptions

Le code dans les pages de gestion des exceptions peut également lever des exceptions. Les pages d’erreur de production doivent être testées minutieusement en faisant particulièrement attention à éviter de lever leurs propres exceptions.

En-têtes de réponse

Une fois les en-têtes d’une réponse envoyés :

  • L’application ne peut pas modifier le code d’état de la réponse.
  • Il est impossible d’exécuter les pages d’exception ou les gestionnaires. La réponse doit être accomplie ou la connexion abandonnée.

Gestion des exceptions de serveur

En plus de la logique de gestion des exceptions d’une application, l’implémentation de serveur HTTP peut gérer certaines exceptions. Si le serveur intercepte une exception avant que les en-têtes de réponse ne soient envoyés, le serveur envoie une réponse 500 - Internal Server Error sans corps de réponse. Si le serveur intercepte une exception une fois que les en-têtes de réponse ont été envoyés, il ferme la connexion. Les requêtes qui ne sont pas gérées par l’application sont gérées par le serveur. Toute exception qui se produit tandis que le serveur traite la demande est gérée par le dispositif de gestion des exceptions du serveur. Ni les pages d’erreur personnalisées de l’application, ni les intergiciels (middleware) de gestion des exceptions, ni les filtres n’ont d’incidence sur ce comportement.

Gestion des exceptions de démarrage

Seule la couche d’hébergement peut gérer les exceptions qui se produisent au démarrage de l’application. L’hôte peut être configuré pour capturer les erreurs de démarrage et capturer les erreurs détaillées.

La couche d’hébergement ne peut afficher la page d’une erreur de démarrage capturée que si celle-ci se produit une fois la liaison établie entre l’adresse d’hôte et le port. Si la liaison échoue :

  • La couche d’hébergement journalise une exception critique.
  • Le processus dotnet tombe en panne.
  • Aucune page d’erreur ne s’affiche lorsque le serveur HTTP est Kestrel.

En cas d’exécution sur IIS (ou Azure App Service) ou IIS Express, une réponse 502.5 - Échec du processus est retournée par le module ASP.NET Core si le processus ne peut pas démarrer. Pour plus d’informations, consultez Résoudre les problèmes liés à ASP.NET Core sur Azure App Service et IIS.

Page d’erreur de base de données

Le filtre d’exception de la page du développeur de la base de données AddDatabaseDeveloperPageExceptionFilter capture les exceptions liées aux bases de données qui peuvent être résolues par des migrations Entity Framework Core. Lorsque ces exceptions se produisent, une réponse HTML est générée, avec les détails des actions possibles pour résoudre le problème. Cette page n’est activée que dans l’environnement de développement. Le code suivant ajoute le filtre d’exception de la page du développeur de la base de données :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();

Filtres d’exceptions

Dans les applications MVC, vous pouvez configurer les filtres d’exception globalement, contrôleur par contrôleur ou action par action. Dans les applications des pages Razor, ils peuvent être configurés globalement ou par modèle de page. Ces filtres gèrent toutes les exceptions non prises en charge qui se produisent pendant l’exécution d’une action de contrôleur ou d’un autre filtre. Pour plus d’informations, consultez Filtres dans ASP.NET Core.

Les filtres d’exception sont utiles pour intercepter les exceptions qui se produisent dans les actions MVC, mais n’offrent pas la même souplesse que l’intergiciel de gestion d’exceptions intégré, UseExceptionHandler. Nous recommandons l’utilisation de UseExceptionHandler, sauf si vous devez gérer les erreurs différemment en fonction de l’action MVC choisie.

Erreurs d’état de modèle

Pour plus d’informations sur la gestion des erreurs d’état de modèle, voir Liaison de modèles et Validation de modèles.

Détails du problème

Les détails du problème ne sont pas le seul format de réponse à décrire une erreur d’API HTTP. Toutefois, ils sont couramment utilisés pour signaler des erreurs pour les API HTTP.

Le service des détails du problème implémente l’interface IProblemDetailsService, qui prend en charge la création de détails de problème dans ASP.NET Core. La méthode d’extension AddProblemDetails sur IServiceCollection enregistre l’implémentation IProblemDetailsService par défaut.

Dans les applications ASP.NET Core, l’intergiciel suivant génère des réponses HTTP sur les détails de problème lorsque AddProblemDetails est appelé, sauf si l’en-tête HTTP de requêteAccept n’inclut pas l’un des types de contenu pris en charge par le IProblemDetailsWriter inscrit (par défaut : application/json) :

  • ExceptionHandlerMiddleware : génère une réponse sur les détails du problème lorsqu’un gestionnaire personnalisé n’est pas défini.
  • StatusCodePagesMiddleware : génère une réponse de détails de problème par défaut.
  • DeveloperExceptionPageMiddlewareHTTP : génère une réponse sur les détails du problème lors du développement lorsque l’en-tête HTTP de la requête Accept n’inclut pas text/html.

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 :

builder.Services.AddProblemDetails();

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

La section suivante montre comment personnaliser le corps de la réponse sur les détails du problème.

Personnaliser les détails du problème

La création automatique d’un ProblemDetails peut être personnalisée à l’aide de l’une des options suivantes :

  1. Utilisez ProblemDetailsOptions.CustomizeProblemDetails
  2. Utiliser un IProblemDetailsWriter personnalisé
  3. Appeler IProblemDetailsService dans un intergiciel

Opération CustomizeProblemDetails

Les détails du problème généré peuvent être personnalisés à l’aide de CustomizeProblemDetails et les personnalisations sont appliquées à tous les détails du problème générés automatiquement.

Le code suivant utilise ProblemDetailsOptions pour définir CustomizeProblemDetails :

builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

Par exemple, un résultat de point de terminaison HTTP Status 400 Bad Request génère le corps de réponse sur les détails du problème suivant :

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "nodeId": "my-machine-name"
}

IProblemDetailsWriter personnalisé

Une implémentation IProblemDetailsWriter peut être créée pour les personnalisations avancées.

public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
    // Indicates that only responses with StatusCode == 400
    // are handled by this writer. All others are
    // handled by different registered writers if available.
    public bool CanWrite(ProblemDetailsContext context)
        => context.HttpContext.Response.StatusCode == 400;

    public ValueTask WriteAsync(ProblemDetailsContext context)
    {
        // Additional customizations.

        // Write to the response.
        var response = context.HttpContext.Response;
        return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
    }
}

Remarque : Lors de l’utilisation d’un IProblemDetailsWriter personnalisé, le IProblemDetailsWriter personnalisé doit être inscrit avant d’appeler AddRazorPages, AddControllers, AddControllersWithViewsou AddMvc :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();

var app = builder.Build();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsWriter>() is
            { } problemDetailsService)
        {

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

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = "Bad Input",
                        Detail = details.Detail,
                        Type = details.Type
                    }
                });
            }
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.DivisionByZeroError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.NegativeRadicandError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

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

app.Run();

Détails du problème de l’intergiciel

Une autre approche de l’utilisation de ProblemDetailsOptions avec CustomizeProblemDetails consiste à définir le ProblemDetails dans l’intergiciel. Une réponse sur les détails du problème peut être écrite en appelant IProblemDetailsService.WriteAsync :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStatusCodePages();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsService>() is
                                                           { } problemDetailsService)
        {
            (string Detail, string Type) details = mathErrorFeature.MathError switch
            {
                MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                "https://en.wikipedia.org/wiki/Division_by_zero"),
                _ => ("Negative or complex numbers are not valid input.", 
                "https://en.wikipedia.org/wiki/Square_root")
            };

            await problemDetailsService.WriteAsync(new ProblemDetailsContext
            {
                HttpContext = context,
                ProblemDetails =
                {
                    Title = "Bad Input",
                    Detail = details.Detail,
                    Type = details.Type
                }
            });
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.DivisionByZeroError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.NegativeRadicandError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

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

app.MapControllers();

app.Run();

Dans le code précédent, les points de terminaison d’API minimaux /divide et /squareroot retournent la réponse de problème personnalisée attendue lors de l’entrée d’erreur.

Les points de terminaison du contrôleur d’API retournent la réponse au problème par défaut lors de l’entrée d’erreur, et non la réponse au problème personnalisée. La réponse au problème par défaut est retournée, car le contrôleur d’API a écrit dans le flux de réponse, Détails du problème pour les codes d’erreur d’état, avant que IProblemDetailsService.WriteAsync ne soit appelé et la réponse n’est plus écrite.

La valeur ValuesController suivante retourne BadRequestResult, qui écrit dans le flux de réponse et empêche donc le retour de la réponse au problème personnalisé.

[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));
    }

}

La valeur suivante Values3Controller retourne ControllerBase.Problem afin que le résultat du problème personnalisé attendu soit retourné :

[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
    // /api/values3/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 Problem(
                title: "Bad Input",
                detail: "Divison by zero is not defined.",
                type: "https://en.wikipedia.org/wiki/Division_by_zero",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values3/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Negative or complex numbers are not valid input.",
                type: "https://en.wikipedia.org/wiki/Square_root",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

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

}

Produire une charge utile ProblemDetails pour les exceptions

Prenons l’application suivante :

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();

Dans les environnements hors développement, lorsqu’une exception se produit, la réponse ProblemDetails standard qui est retournée au client est la suivante :

{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}

Pour la plupart des applications, le code précédent est suffisant pour les exceptions. Toutefois, la section suivante montre une manière d’obtenir des réponses plus détaillées aux problèmes.

En dehors d’une page de gestionnaire d’exceptions personnalisée, il est possible de fournir une expression lambda à UseExceptionHandler, L’utilisation d’une expression lambda permet d’accéder à l’erreur et d’écrire une réponse sur les détails du problème avec IProblemDetailsService.WriteAsync :

using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;

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();
}
else
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = Text.Plain;

            var title = "Bad Input";
            var detail = "Invalid input";
            var type = "https://errors.example.com/badInput";

            if (context.RequestServices.GetService<IProblemDetailsService>() is
                { } problemDetailsService)
            {
                var exceptionHandlerFeature =
               context.Features.Get<IExceptionHandlerFeature>();

                var exceptionType = exceptionHandlerFeature?.Error;
                if (exceptionType != null &&
                   exceptionType.Message.Contains("infinity"))
                {
                    title = "Argument exception";
                    detail = "Invalid input";
                    type = "https://errors.example.com/argumentException";
                }

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                {
                    Title = title,
                    Detail = detail,
                    Type = type
                }
                });
            }
        });
    });
}

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

Warning

Ne communiquez pas d’informations sensibles sur les erreurs aux clients. Cela représenterait un risque de sécurité.

Une autre approche à la génération des détails d’un problème consiste à utiliser le package NuGet tiers Hellang.Middleware.ProblemDetails qui peut être utilisé pour mapper des exceptions et des erreurs client aux détails du problème.

Ressources supplémentaires

Par Tom Dykstra

Cet article aborde des approches courantes pour gérer les erreurs dans les applications web ASP.NET Core. Consultez également Gérer les erreurs dans les API web ASP.NET Core et Gérer les erreurs dans les applications API minimales.

Page d’exceptions du développeur

La Page d’exception du développeur affiche des informations détaillées sur les exceptions des requêtes non prises en charge. Les applications ASP.NET Core activent la page d’exception du développeur par défaut en présence des deux éléments suivants :

La page d’exception du développeur s’exécute au début du pipeline intergiciel, de manière qu’elle puisse intercepter les exceptions non prises en charge levées dans l’intergiciel qui suit.

Des informations détaillées sur les exceptions ne doivent pas être affichées publiquement lorsque l’application s’exécute dans l’environnement de production. Pour plus d’informations sur la configuration des environnements, consultez Utiliser plusieurs environnements dans ASP.NET Core.

La page d’exception du développeur peut inclure les informations suivantes sur l’exception et la requête :

  • Trace de pile
  • Paramètres de la chaîne de requête, le cas échéant
  • Cookie, le cas échéant
  • En-têtes

Il n’est pas garanti que la page d’exception du développeur fournisse des informations. Utilisez la journalisation pour obtenir des informations complètes sur l’erreur.

Page Gestionnaire d’exceptions

Pour configurer une page de gestion des erreurs personnalisée en fonction de l’environnement de production, appelez UseExceptionHandler. Cet intergiciel de gestion des exceptions :

  • Intercepte et consigne les exceptions non prises en charge.
  • Réexécute la requête dans un pipeline de remplacement à l’aide du chemin d’accès indiqué. La demande n’est pas réexécutée si la réponse a démarré. Le code généré par le modèle réexécute la requête à l’aide du chemin d’accès /Error.

Warning

Si le pipeline de remplacement lève une exception qui lui est propre, l’intergiciel de gestion des exception lève à nouveau l’exception d’origine.

Étant donné que cet intergiciel peut réexécuter le pipeline de requête :

  • Les intergiciels doivent gérer la réentrance avec la même requête. Cela signifie normalement le nettoyage de leur état après l’appel _next ou la mise en cache de leur traitement sur HttpContext pour éviter de le rétablir. Lorsque vous traitez le corps de la requête, cela peut signifier une mise en mémoire tampon ou une mise en cache des résultats, comme le lecteur de formulaire.
  • Pour la surcharge UseExceptionHandler(IApplicationBuilder, String) utilisée dans les modèles, seul le chemin d’accès de la requête est modifié et les données de routage sont effacées. Les données de requête telles que les en-têtes, la méthode et les éléments sont toutes réutilisées telles quelles.
  • Les services délimités restent les mêmes.

Dans l’exemple suivant, UseExceptionHandler ajoute l’intergiciel de gestion des exceptions dans des environnements hors développement :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Le modèle d’application des pages Razor fournit une page d’erreur (.cshtml) et une classe PageModel (ErrorModel) dans le dossier Pages. Pour une application MVC, le modèle de projet comprend une méthode d’action Error et un affichage d’erreur pour le contrôleur Home.

L’intergiciel de gestion des exceptions réexécute la requête à l’aide de la méthode HTTP d’origine. Si un point de terminaison de gestionnaire d’erreurs est limité à un ensemble spécifique de méthodes HTTP, il ne s’exécute que pour ces méthodes HTTP. Par exemple, une action de contrôleur MVC qui utilise l’attribut [HttpGet] s’exécute uniquement pour les requêtes GET. Pour garantir que toutes les requêtes atteignent la page de gestion des erreurs personnalisée, ne les limitez pas à un ensemble spécifique de méthodes HTTP.

Pour gérer les exceptions différemment en fonction de la méthode HTTP d’origine :

  • Pour les pages Razor, créez plusieurs méthodes de gestionnaire. Par exemple, utilisez OnGet pour gérer les exceptions GET et OnPost pour gérer les exceptions POST.
  • Pour MVC, appliquez des attributs de verbe HTTP à plusieurs actions. Vous pouvez par exemple utiliser [HttpGet] pour gérer les exceptions GET et [HttpPost] pour gérer les exceptions POST.

Pour autoriser les utilisateurs non authentifiés à afficher la page de gestion des erreurs personnalisée, assurez-vous qu’elle prend en charge l’accès anonyme.

Accéder à l'exception

Utilisez IExceptionHandlerPathFeature pour accéder à l’exception et au chemin d’accès de la requête d’origine dans un gestionnaire d’erreurs. L’exemple suivant utilise IExceptionHandlerPathFeature pour obtenir plus d’informations sur l’exception levée :

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}

Warning

Ne communiquez pas d’informations sensibles sur les erreurs aux clients. Cela représenterait un risque de sécurité.

Expression lambda Gestionnaire d’exceptions

En dehors d’une page de gestionnaire d’exceptions personnalisée, il est possible de fournir une expression lambda à UseExceptionHandler, ce qui permet d’accéder à l’erreur avant de retourner la réponse.

Le code suivant utilise une expression lambda pour la gestion des exceptions :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}

Warning

Ne communiquez pas d’informations sensibles sur les erreurs aux clients. Cela représenterait un risque de sécurité.

IExceptionHandler

IExceptionHandler est une interface qui donne au développeur un rappel pour la gestion des exceptions connues dans un emplacement central.

Les implémentations IExceptionHandler sont inscrites en appelant IServiceCollection.AddExceptionHandler<T>. La durée de vie d’une instance de IExceptionHandler est singleton. Plusieurs implémentations peuvent être ajoutées et elles sont appelées dans l’ordre inscrit.

Si un gestionnaire d’exceptions gère une requête, il peut renvoyer true pour arrêter le traitement. Si une exception n’est gérée par aucun gestionnaire d’exceptions, le contrôle revient au comportement et aux options par défaut de l’intergiciel. Différentes métriques et journaux d’activité sont émis pour les exceptions gérées et non gérées.

L'exemple suivant montre une implémentation de IExceptionHandler :

using Microsoft.AspNetCore.Diagnostics;

namespace ErrorHandlingSample
{
    public class CustomExceptionHandler : IExceptionHandler
    {
        private readonly ILogger<CustomExceptionHandler> logger;
        public CustomExceptionHandler(ILogger<CustomExceptionHandler> logger)
        {
            this.logger = logger;
        }
        public ValueTask<bool> TryHandleAsync(
            HttpContext httpContext,
            Exception exception,
            CancellationToken cancellationToken)
        {
            var exceptionMessage = exception.Message;
            logger.LogError(
                "Error Message: {exceptionMessage}, Time of occurrence {time}",
                exceptionMessage, DateTime.UtcNow);
            // Return false to continue with the default behavior
            // - or - return true to signal that this exception is handled
            return ValueTask.FromResult(false);
        }
    }
}

L’exemple suivant montre comment inscrire une implémentation de IExceptionHandler pour l’injection de dépendances :

using ErrorHandlingSample;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();
builder.Services.AddExceptionHandler<CustomExceptionHandler>();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// Remaining Program.cs code omitted for brevity

Lorsque le code précédent est exécuté dans l'environnement de développement :

  • La CustomExceptionHandler est appelée en premier pour gérer une exception.
  • Après avoir enregistré l'exception, la méthode TryHandleException retourne false, de sorte que la page d’exception développeur s’affiche.

Dans d’autres environnements :

  • La CustomExceptionHandler est appelée en premier pour gérer une exception.
  • Après avoir enregistré l'exception, la méthode TryHandleException retourne false, de sorte que la /Errorpage s’affiche.

UseStatusCodePages

Par défaut, une application ASP.NET Core ne fournit pas une page de codes d’état pour les codes d’état des erreurs HTTP, comme 404 – Introuvable. Lorsque l’application définit un code d’état d’erreur HTTP 400-599 qui n’a pas de corps, elle retourne le code d’état et un corps de réponse vide. Pour activer les gestionnaires de texte uniquement par défaut pour les codes d’état d’erreur courants, appelez UseStatusCodePages dans Program.cs :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();

Appelez UseStatusCodePages avant l’intergiciel de gestion des requêtes. Par exemple, appelez UseStatusCodePages avant l’intergiciel de fichier statique et l’intergiciel des points de terminaison.

Quand UseStatusCodePages n’est pas utilisé, la navigation vers une URL sans point de terminaison retourne un message d’erreur dépendant du navigateur indiquant que le point de terminaison est introuvable. Quand UseStatusCodePages est appelé, le navigateur retourne la réponse suivante :

Status Code: 404; Not Found

UseStatusCodePages n’est généralement pas utilisé en production, car il retourne un message qui n’est pas utile pour les utilisateurs.

Remarque

L’intergiciel des pages de codes d’état n’intercepte pas les exceptions. Pour fournir une page de gestion des erreurs personnalisée, utilisez la page du gestionnaire d’exception.

UseStatusCodePages avec chaîne de format

Pour personnaliser le texte et le type de contenu de la réponse, utilisez la surcharge de UseStatusCodePages qui prend un type de contenu et une chaîne de format :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");

Dans le code précédent, {0} est un espace réservé pour le code d’erreur.

UseStatusCodePages avec une chaîne de format n’est généralement pas utilisé en production, car il retourne un message qui n’est pas utile pour les utilisateurs.

UseStatusCodePages avec expression lambda

Pour spécifier un code personnalisé de gestion des erreurs et d’écriture de la réponse, utilisez la surcharge de UseStatusCodePages qui prend une expression lambda :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});

UseStatusCodePages avec une expression lambda n’est généralement pas utilisé en production, car il retourne un message qui n’est pas utile pour les utilisateurs.

UseStatusCodePagesWithRedirects

La méthode d’extension UseStatusCodePagesWithRedirects :

  • Envoie un code d’état 302 - Trouvé au client.
  • Redirige le client vers le point de terminaison de gestion des erreurs fourni dans le modèle d’URL. Le point de terminaison de gestion des erreurs affiche généralement les informations d’erreur et retourne HTTP 200.
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

Le modèle d’URL peut comporter un espace réservé {0} pour le code d’état, comme indiqué dans le code précédent. Si le modèle d’URL commence par un ~ (tilde), le ~ est remplacé par le PathBase de l’application. Lorsque vous spécifiez un point de terminaison dans l’application, créez une vue MVC ou une page Razor pour le point de terminaison.

Cette méthode est généralement utilisée lorsque l’application :

  • Doit rediriger le client vers un autre point de terminaison, généralement dans les cas où une autre application traite l’erreur. Pour les applications web, la barre d’adresses du navigateur client reflète le point de terminaison redirigé.
  • Ne doit pas conserver ni retourner le code d’état d’origine avec la réponse de redirection initiale.

UseStatusCodePagesWithReExecute

La méthode d’extension UseStatusCodePagesWithReExecute :

  • Génère le corps de la réponse en réexécutant le pipeline de requête avec un autre chemin.
  • Ne modifie pas le code d’état avant ou après la réexécution du pipeline.

L’exécution du nouveau pipeline peut modifier le code d’état de la réponse, car le nouveau pipeline dispose d’un contrôle total sur le code d’état. Si le nouveau pipeline ne modifie pas le code d’état, le code d’état d’origine sera envoyé au client.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");

Si un point de terminaison dans l’application est spécifié, créez une vue MVC ou une page Razor pour le point de terminaison.

Cette méthode est généralement utilisée lorsque l’application doit :

  • Traiter la demande sans la rediriger vers un autre point de terminaison. Pour les applications web, la barre d’adresses du navigateur client reflète le point de terminaison demandé à l’origine.
  • Conserver et retourner le code d’état d’origine avec la réponse.

Le modèle d’URL doit commencer par / et peut inclure un espace réservé {0} pour le code d’état. Pour faire passer le code d’état en tant que paramètre de chaîne de requête, passez un deuxième argument dans UseStatusCodePagesWithReExecute. Exemple :

var app = builder.Build();  
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");

Le point de terminaison qui traite l’erreur peut récupérer l’URL d’origine qui a généré l’erreur, comme dans l’exemple suivant :

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = string.Join(
                statusCodeReExecuteFeature.OriginalPathBase,
                statusCodeReExecuteFeature.OriginalPath,
                statusCodeReExecuteFeature.OriginalQueryString);
        }
    }
}

Étant donné que cet intergiciel peut réexécuter le pipeline de requête :

  • Les intergiciels doivent gérer la réentrance avec la même requête. Cela signifie normalement le nettoyage de leur état après l’appel _next ou la mise en cache de leur traitement sur HttpContext pour éviter de le rétablir. Lorsque vous traitez le corps de la requête, cela peut signifier une mise en mémoire tampon ou une mise en cache des résultats, comme le lecteur de formulaire.
  • Les services délimités restent les mêmes.

Désactiver les pages de codes d’état

Pour désactiver les pages de codes d’état d’un contrôleur MVC ou d’une méthode d’action, utilisez l’attribut [SkipStatusCodePages].

Pour désactiver les pages de codes d’état pour des requêtes spécifiques dans une méthode de gestionnaire des pages IStatusCodePagesFeature ou dans un contrôleur MVC, utilisez Razor :

public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

Code de gestion des exceptions

Le code dans les pages de gestion des exceptions peut également lever des exceptions. Les pages d’erreur de production doivent être testées minutieusement en faisant particulièrement attention à éviter de lever leurs propres exceptions.

En-têtes de réponse

Une fois les en-têtes d’une réponse envoyés :

  • L’application ne peut pas modifier le code d’état de la réponse.
  • Il est impossible d’exécuter les pages d’exception ou les gestionnaires. La réponse doit être accomplie ou la connexion abandonnée.

Gestion des exceptions de serveur

En plus de la logique de gestion des exceptions d’une application, l’implémentation de serveur HTTP peut gérer certaines exceptions. Si le serveur intercepte une exception avant que les en-têtes de réponse ne soient envoyés, le serveur envoie une réponse 500 - Internal Server Error sans corps de réponse. Si le serveur intercepte une exception une fois que les en-têtes de réponse ont été envoyés, il ferme la connexion. Les requêtes qui ne sont pas gérées par l’application sont gérées par le serveur. Toute exception qui se produit tandis que le serveur traite la demande est gérée par le dispositif de gestion des exceptions du serveur. Ni les pages d’erreur personnalisées de l’application, ni les intergiciels (middleware) de gestion des exceptions, ni les filtres n’ont d’incidence sur ce comportement.

Gestion des exceptions de démarrage

Seule la couche d’hébergement peut gérer les exceptions qui se produisent au démarrage de l’application. L’hôte peut être configuré pour capturer les erreurs de démarrage et capturer les erreurs détaillées.

La couche d’hébergement ne peut afficher la page d’une erreur de démarrage capturée que si celle-ci se produit une fois la liaison établie entre l’adresse d’hôte et le port. Si la liaison échoue :

  • La couche d’hébergement journalise une exception critique.
  • Le processus dotnet tombe en panne.
  • Aucune page d’erreur ne s’affiche lorsque le serveur HTTP est Kestrel.

En cas d’exécution sur IIS (ou Azure App Service) ou IIS Express, une réponse 502.5 - Échec du processus est retournée par le module ASP.NET Core si le processus ne peut pas démarrer. Pour plus d’informations, consultez Résoudre les problèmes liés à ASP.NET Core sur Azure App Service et IIS.

Page d’erreur de base de données

Le filtre d’exception de la page du développeur de la base de données AddDatabaseDeveloperPageExceptionFilter capture les exceptions liées aux bases de données qui peuvent être résolues par des migrations Entity Framework Core. Lorsque ces exceptions se produisent, une réponse HTML est générée, avec les détails des actions possibles pour résoudre le problème. Cette page n’est activée que dans l’environnement de développement. Le code suivant ajoute le filtre d’exception de la page du développeur de la base de données :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();

Filtres d’exceptions

Dans les applications MVC, vous pouvez configurer les filtres d’exception globalement, contrôleur par contrôleur ou action par action. Dans les applications des pages Razor, ils peuvent être configurés globalement ou par modèle de page. Ces filtres gèrent toutes les exceptions non prises en charge qui se produisent pendant l’exécution d’une action de contrôleur ou d’un autre filtre. Pour plus d’informations, consultez Filtres dans ASP.NET Core.

Les filtres d’exception sont utiles pour intercepter les exceptions qui se produisent dans les actions MVC, mais n’offrent pas la même souplesse que l’intergiciel de gestion d’exceptions intégré, UseExceptionHandler. Nous recommandons l’utilisation de UseExceptionHandler, sauf si vous devez gérer les erreurs différemment en fonction de l’action MVC choisie.

Erreurs d’état de modèle

Pour plus d’informations sur la gestion des erreurs d’état de modèle, voir Liaison de modèles et Validation de modèles.

Détails du problème

Les détails du problème ne sont pas le seul format de réponse à décrire une erreur d’API HTTP. Toutefois, ils sont couramment utilisés pour signaler des erreurs pour les API HTTP.

Le service des détails du problème implémente l’interface IProblemDetailsService, qui prend en charge la création de détails de problème dans ASP.NET Core. La méthode d’extension AddProblemDetails sur IServiceCollection enregistre l’implémentation IProblemDetailsService par défaut.

Dans les applications ASP.NET Core, l’intergiciel suivant génère des réponses HTTP sur les détails de problème lorsque AddProblemDetails est appelé, sauf si l’en-tête HTTP de requêteAccept n’inclut pas l’un des types de contenu pris en charge par le IProblemDetailsWriter inscrit (par défaut : application/json) :

  • ExceptionHandlerMiddleware : génère une réponse sur les détails du problème lorsqu’un gestionnaire personnalisé n’est pas défini.
  • StatusCodePagesMiddleware : génère une réponse de détails de problème par défaut.
  • DeveloperExceptionPageMiddlewareHTTP : génère une réponse sur les détails du problème lors du développement lorsque l’en-tête HTTP de la requête Accept n’inclut pas text/html.

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 :

builder.Services.AddProblemDetails();

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

La section suivante montre comment personnaliser le corps de la réponse sur les détails du problème.

Personnaliser les détails du problème

La création automatique d’un ProblemDetails peut être personnalisée à l’aide de l’une des options suivantes :

  1. Utilisez ProblemDetailsOptions.CustomizeProblemDetails
  2. Utiliser un IProblemDetailsWriter personnalisé
  3. Appeler IProblemDetailsService dans un intergiciel

Opération CustomizeProblemDetails

Les détails du problème généré peuvent être personnalisés à l’aide de CustomizeProblemDetails et les personnalisations sont appliquées à tous les détails du problème générés automatiquement.

Le code suivant utilise ProblemDetailsOptions pour définir CustomizeProblemDetails :

builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

Par exemple, un résultat de point de terminaison HTTP Status 400 Bad Request génère le corps de réponse sur les détails du problème suivant :

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "nodeId": "my-machine-name"
}

IProblemDetailsWriter personnalisé

Une implémentation IProblemDetailsWriter peut être créée pour les personnalisations avancées.

public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
    // Indicates that only responses with StatusCode == 400
    // are handled by this writer. All others are
    // handled by different registered writers if available.
    public bool CanWrite(ProblemDetailsContext context)
        => context.HttpContext.Response.StatusCode == 400;

    public ValueTask WriteAsync(ProblemDetailsContext context)
    {
        // Additional customizations.

        // Write to the response.
        var response = context.HttpContext.Response;
        return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
    }
}

Remarque : Lors de l’utilisation d’un IProblemDetailsWriter personnalisé, le IProblemDetailsWriter personnalisé doit être inscrit avant d’appeler AddRazorPages, AddControllers, AddControllersWithViewsou AddMvc :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();

var app = builder.Build();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsWriter>() is
            { } problemDetailsService)
        {

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

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = "Bad Input",
                        Detail = details.Detail,
                        Type = details.Type
                    }
                });
            }
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.DivisionByZeroError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.NegativeRadicandError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

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

app.Run();

Détails du problème de l’intergiciel

Une autre approche de l’utilisation de ProblemDetailsOptions avec CustomizeProblemDetails consiste à définir le ProblemDetails dans l’intergiciel. Une réponse sur les détails du problème peut être écrite en appelant IProblemDetailsService.WriteAsync :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStatusCodePages();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsService>() is
                                                           { } problemDetailsService)
        {
            (string Detail, string Type) details = mathErrorFeature.MathError switch
            {
                MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                "https://en.wikipedia.org/wiki/Division_by_zero"),
                _ => ("Negative or complex numbers are not valid input.", 
                "https://en.wikipedia.org/wiki/Square_root")
            };

            await problemDetailsService.WriteAsync(new ProblemDetailsContext
            {
                HttpContext = context,
                ProblemDetails =
                {
                    Title = "Bad Input",
                    Detail = details.Detail,
                    Type = details.Type
                }
            });
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.DivisionByZeroError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.NegativeRadicandError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

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

app.MapControllers();

app.Run();

Dans le code précédent, les points de terminaison d’API minimaux /divide et /squareroot retournent la réponse de problème personnalisée attendue lors de l’entrée d’erreur.

Les points de terminaison du contrôleur d’API retournent la réponse au problème par défaut lors de l’entrée d’erreur, et non la réponse au problème personnalisée. La réponse au problème par défaut est retournée, car le contrôleur d’API a écrit dans le flux de réponse, Détails du problème pour les codes d’erreur d’état, avant que IProblemDetailsService.WriteAsync ne soit appelé et la réponse n’est plus écrite.

La valeur ValuesController suivante retourne BadRequestResult, qui écrit dans le flux de réponse et empêche donc le retour de la réponse au problème personnalisé.

[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));
    }

}

La valeur suivante Values3Controller retourne ControllerBase.Problem afin que le résultat du problème personnalisé attendu soit retourné :

[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
    // /api/values3/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 Problem(
                title: "Bad Input",
                detail: "Divison by zero is not defined.",
                type: "https://en.wikipedia.org/wiki/Division_by_zero",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values3/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Negative or complex numbers are not valid input.",
                type: "https://en.wikipedia.org/wiki/Square_root",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

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

}

Produire une charge utile ProblemDetails pour les exceptions

Prenons l’application suivante :

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();

Dans les environnements hors développement, lorsqu’une exception se produit, la réponse ProblemDetails standard qui est retournée au client est la suivante :

{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}

Pour la plupart des applications, le code précédent est suffisant pour les exceptions. Toutefois, la section suivante montre une manière d’obtenir des réponses plus détaillées aux problèmes.

En dehors d’une page de gestionnaire d’exceptions personnalisée, il est possible de fournir une expression lambda à UseExceptionHandler, L’utilisation d’une expression lambda permet d’accéder à l’erreur et d’écrire une réponse sur les détails du problème avec IProblemDetailsService.WriteAsync :

using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;

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();
}
else
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = Text.Plain;

            var title = "Bad Input";
            var detail = "Invalid input";
            var type = "https://errors.example.com/badInput";

            if (context.RequestServices.GetService<IProblemDetailsService>() is
                { } problemDetailsService)
            {
                var exceptionHandlerFeature =
               context.Features.Get<IExceptionHandlerFeature>();

                var exceptionType = exceptionHandlerFeature?.Error;
                if (exceptionType != null &&
                   exceptionType.Message.Contains("infinity"))
                {
                    title = "Argument exception";
                    detail = "Invalid input";
                    type = "https://errors.example.com/argumentException";
                }

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                {
                    Title = title,
                    Detail = detail,
                    Type = type
                }
                });
            }
        });
    });
}

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

Warning

Ne communiquez pas d’informations sensibles sur les erreurs aux clients. Cela représenterait un risque de sécurité.

Une autre approche à la génération des détails d’un problème consiste à utiliser le package NuGet tiers Hellang.Middleware.ProblemDetails qui peut être utilisé pour mapper des exceptions et des erreurs client aux détails du problème.

Ressources supplémentaires

Par Tom Dykstra

Cet article aborde des approches courantes pour gérer les erreurs dans les applications web ASP.NET Core. Consultez également Gérer les erreurs dans les API web ASP.NET Core et Gérer les erreurs dans les applications API minimales.

Page d’exceptions du développeur

La Page d’exception du développeur affiche des informations détaillées sur les exceptions des requêtes non prises en charge. Les applications ASP.NET Core activent la page d’exception du développeur par défaut en présence des deux éléments suivants :

La page d’exception du développeur s’exécute au début du pipeline intergiciel, de manière qu’elle puisse intercepter les exceptions non prises en charge levées dans l’intergiciel qui suit.

Des informations détaillées sur les exceptions ne doivent pas être affichées publiquement lorsque l’application s’exécute dans l’environnement de production. Pour plus d’informations sur la configuration des environnements, consultez Utiliser plusieurs environnements dans ASP.NET Core.

La page d’exception du développeur peut inclure les informations suivantes sur l’exception et la requête :

  • Trace de pile
  • Paramètres de la chaîne de requête, le cas échéant
  • Cookie, le cas échéant
  • En-têtes

Il n’est pas garanti que la page d’exception du développeur fournisse des informations. Utilisez la journalisation pour obtenir des informations complètes sur l’erreur.

Page Gestionnaire d’exceptions

Pour configurer une page de gestion des erreurs personnalisée en fonction de l’environnement de production, appelez UseExceptionHandler. Cet intergiciel de gestion des exceptions :

  • Intercepte et consigne les exceptions non prises en charge.
  • Réexécute la requête dans un pipeline de remplacement à l’aide du chemin d’accès indiqué. La demande n’est pas réexécutée si la réponse a démarré. Le code généré par le modèle réexécute la requête à l’aide du chemin d’accès /Error.

Warning

Si le pipeline de remplacement lève une exception qui lui est propre, l’intergiciel de gestion des exception lève à nouveau l’exception d’origine.

Étant donné que cet intergiciel peut réexécuter le pipeline de requête :

  • Les intergiciels doivent gérer la réentrance avec la même requête. Cela signifie normalement le nettoyage de leur état après l’appel _next ou la mise en cache de leur traitement sur HttpContext pour éviter de le rétablir. Lorsque vous traitez le corps de la requête, cela peut signifier une mise en mémoire tampon ou une mise en cache des résultats, comme le lecteur de formulaire.
  • Pour la surcharge UseExceptionHandler(IApplicationBuilder, String) utilisée dans les modèles, seul le chemin d’accès de la requête est modifié et les données de routage sont effacées. Les données de requête telles que les en-têtes, la méthode et les éléments sont toutes réutilisées telles quelles.
  • Les services délimités restent les mêmes.

Dans l’exemple suivant, UseExceptionHandler ajoute l’intergiciel de gestion des exceptions dans des environnements hors développement :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Le modèle d’application des pages Razor fournit une page d’erreur (.cshtml) et une classe PageModel (ErrorModel) dans le dossier Pages. Pour une application MVC, le modèle de projet comprend une méthode d’action Error et un affichage d’erreur pour le contrôleur Home.

L’intergiciel de gestion des exceptions réexécute la requête à l’aide de la méthode HTTP d’origine. Si un point de terminaison de gestionnaire d’erreurs est limité à un ensemble spécifique de méthodes HTTP, il ne s’exécute que pour ces méthodes HTTP. Par exemple, une action de contrôleur MVC qui utilise l’attribut [HttpGet] s’exécute uniquement pour les requêtes GET. Pour garantir que toutes les requêtes atteignent la page de gestion des erreurs personnalisée, ne les limitez pas à un ensemble spécifique de méthodes HTTP.

Pour gérer les exceptions différemment en fonction de la méthode HTTP d’origine :

  • Pour les pages Razor, créez plusieurs méthodes de gestionnaire. Par exemple, utilisez OnGet pour gérer les exceptions GET et OnPost pour gérer les exceptions POST.
  • Pour MVC, appliquez des attributs de verbe HTTP à plusieurs actions. Vous pouvez par exemple utiliser [HttpGet] pour gérer les exceptions GET et [HttpPost] pour gérer les exceptions POST.

Pour autoriser les utilisateurs non authentifiés à afficher la page de gestion des erreurs personnalisée, assurez-vous qu’elle prend en charge l’accès anonyme.

Accéder à l'exception

Utilisez IExceptionHandlerPathFeature pour accéder à l’exception et au chemin d’accès de la requête d’origine dans un gestionnaire d’erreurs. L’exemple suivant utilise IExceptionHandlerPathFeature pour obtenir plus d’informations sur l’exception levée :

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}

Warning

Ne communiquez pas d’informations sensibles sur les erreurs aux clients. Cela représenterait un risque de sécurité.

Expression lambda Gestionnaire d’exceptions

En dehors d’une page de gestionnaire d’exceptions personnalisée, il est possible de fournir une expression lambda à UseExceptionHandler, ce qui permet d’accéder à l’erreur avant de retourner la réponse.

Le code suivant utilise une expression lambda pour la gestion des exceptions :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}

Warning

Ne communiquez pas d’informations sensibles sur les erreurs aux clients. Cela représenterait un risque de sécurité.

UseStatusCodePages

Par défaut, une application ASP.NET Core ne fournit pas une page de codes d’état pour les codes d’état des erreurs HTTP, comme 404 – Introuvable. Lorsque l’application définit un code d’état d’erreur HTTP 400-599 qui n’a pas de corps, elle retourne le code d’état et un corps de réponse vide. Pour activer les gestionnaires de texte uniquement par défaut pour les codes d’état d’erreur courants, appelez UseStatusCodePages dans Program.cs :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();

Appelez UseStatusCodePages avant l’intergiciel de gestion des requêtes. Par exemple, appelez UseStatusCodePages avant l’intergiciel de fichier statique et l’intergiciel des points de terminaison.

Quand UseStatusCodePages n’est pas utilisé, la navigation vers une URL sans point de terminaison retourne un message d’erreur dépendant du navigateur indiquant que le point de terminaison est introuvable. Quand UseStatusCodePages est appelé, le navigateur retourne la réponse suivante :

Status Code: 404; Not Found

UseStatusCodePages n’est généralement pas utilisé en production, car il retourne un message qui n’est pas utile pour les utilisateurs.

Remarque

L’intergiciel des pages de codes d’état n’intercepte pas les exceptions. Pour fournir une page de gestion des erreurs personnalisée, utilisez la page du gestionnaire d’exception.

UseStatusCodePages avec chaîne de format

Pour personnaliser le texte et le type de contenu de la réponse, utilisez la surcharge de UseStatusCodePages qui prend un type de contenu et une chaîne de format :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");

Dans le code précédent, {0} est un espace réservé pour le code d’erreur.

UseStatusCodePages avec une chaîne de format n’est généralement pas utilisé en production, car il retourne un message qui n’est pas utile pour les utilisateurs.

UseStatusCodePages avec expression lambda

Pour spécifier un code personnalisé de gestion des erreurs et d’écriture de la réponse, utilisez la surcharge de UseStatusCodePages qui prend une expression lambda :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});

UseStatusCodePages avec une expression lambda n’est généralement pas utilisé en production, car il retourne un message qui n’est pas utile pour les utilisateurs.

UseStatusCodePagesWithRedirects

La méthode d’extension UseStatusCodePagesWithRedirects :

  • Envoie un code d’état 302 - Trouvé au client.
  • Redirige le client vers le point de terminaison de gestion des erreurs fourni dans le modèle d’URL. Le point de terminaison de gestion des erreurs affiche généralement les informations d’erreur et retourne HTTP 200.
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

Le modèle d’URL peut comporter un espace réservé {0} pour le code d’état, comme indiqué dans le code précédent. Si le modèle d’URL commence par un ~ (tilde), le ~ est remplacé par le PathBase de l’application. Lorsque vous spécifiez un point de terminaison dans l’application, créez une vue MVC ou une page Razor pour le point de terminaison.

Cette méthode est généralement utilisée lorsque l’application :

  • Doit rediriger le client vers un autre point de terminaison, généralement dans les cas où une autre application traite l’erreur. Pour les applications web, la barre d’adresses du navigateur client reflète le point de terminaison redirigé.
  • Ne doit pas conserver ni retourner le code d’état d’origine avec la réponse de redirection initiale.

UseStatusCodePagesWithReExecute

La méthode d’extension UseStatusCodePagesWithReExecute :

  • Génère le corps de la réponse en réexécutant le pipeline de requête avec un autre chemin.
  • Ne modifie pas le code d’état avant ou après la réexécution du pipeline.

L’exécution du nouveau pipeline peut modifier le code d’état de la réponse, car le nouveau pipeline dispose d’un contrôle total sur le code d’état. Si le nouveau pipeline ne modifie pas le code d’état, le code d’état d’origine sera envoyé au client.

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");

Si un point de terminaison dans l’application est spécifié, créez une vue MVC ou une page Razor pour le point de terminaison.

Cette méthode est généralement utilisée lorsque l’application doit :

  • Traiter la demande sans la rediriger vers un autre point de terminaison. Pour les applications web, la barre d’adresses du navigateur client reflète le point de terminaison demandé à l’origine.
  • Conserver et retourner le code d’état d’origine avec la réponse.

Le modèle d’URL doit commencer par / et peut inclure un espace réservé {0} pour le code d’état. Pour faire passer le code d’état en tant que paramètre de chaîne de requête, passez un deuxième argument dans UseStatusCodePagesWithReExecute. Exemple :

var app = builder.Build();  
app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");

Le point de terminaison qui traite l’erreur peut récupérer l’URL d’origine qui a généré l’erreur, comme dans l’exemple suivant :

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = string.Join(
                statusCodeReExecuteFeature.OriginalPathBase,
                statusCodeReExecuteFeature.OriginalPath,
                statusCodeReExecuteFeature.OriginalQueryString);
        }
    }
}

Étant donné que cet intergiciel peut réexécuter le pipeline de requête :

  • Les intergiciels doivent gérer la réentrance avec la même requête. Cela signifie normalement le nettoyage de leur état après l’appel _next ou la mise en cache de leur traitement sur HttpContext pour éviter de le rétablir. Lorsque vous traitez le corps de la requête, cela peut signifier une mise en mémoire tampon ou une mise en cache des résultats, comme le lecteur de formulaire.
  • Les services délimités restent les mêmes.

Désactiver les pages de codes d’état

Pour désactiver les pages de codes d’état d’un contrôleur MVC ou d’une méthode d’action, utilisez l’attribut [SkipStatusCodePages].

Pour désactiver les pages de codes d’état pour des requêtes spécifiques dans une méthode de gestionnaire des pages IStatusCodePagesFeature ou dans un contrôleur MVC, utilisez Razor :

public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

Code de gestion des exceptions

Le code dans les pages de gestion des exceptions peut également lever des exceptions. Les pages d’erreur de production doivent être testées minutieusement en faisant particulièrement attention à éviter de lever leurs propres exceptions.

En-têtes de réponse

Une fois les en-têtes d’une réponse envoyés :

  • L’application ne peut pas modifier le code d’état de la réponse.
  • Il est impossible d’exécuter les pages d’exception ou les gestionnaires. La réponse doit être accomplie ou la connexion abandonnée.

Gestion des exceptions de serveur

En plus de la logique de gestion des exceptions d’une application, l’implémentation de serveur HTTP peut gérer certaines exceptions. Si le serveur intercepte une exception avant que les en-têtes de réponse ne soient envoyés, le serveur envoie une réponse 500 - Internal Server Error sans corps de réponse. Si le serveur intercepte une exception une fois que les en-têtes de réponse ont été envoyés, il ferme la connexion. Les requêtes qui ne sont pas gérées par l’application sont gérées par le serveur. Toute exception qui se produit tandis que le serveur traite la demande est gérée par le dispositif de gestion des exceptions du serveur. Ni les pages d’erreur personnalisées de l’application, ni les intergiciels (middleware) de gestion des exceptions, ni les filtres n’ont d’incidence sur ce comportement.

Gestion des exceptions de démarrage

Seule la couche d’hébergement peut gérer les exceptions qui se produisent au démarrage de l’application. L’hôte peut être configuré pour capturer les erreurs de démarrage et capturer les erreurs détaillées.

La couche d’hébergement ne peut afficher la page d’une erreur de démarrage capturée que si celle-ci se produit une fois la liaison établie entre l’adresse d’hôte et le port. Si la liaison échoue :

  • La couche d’hébergement journalise une exception critique.
  • Le processus dotnet tombe en panne.
  • Aucune page d’erreur ne s’affiche lorsque le serveur HTTP est Kestrel.

En cas d’exécution sur IIS (ou Azure App Service) ou IIS Express, une réponse 502.5 - Échec du processus est retournée par le module ASP.NET Core si le processus ne peut pas démarrer. Pour plus d’informations, consultez Résoudre les problèmes liés à ASP.NET Core sur Azure App Service et IIS.

Page d’erreur de base de données

Le filtre d’exception de la page du développeur de la base de données AddDatabaseDeveloperPageExceptionFilter capture les exceptions liées aux bases de données qui peuvent être résolues par des migrations Entity Framework Core. Lorsque ces exceptions se produisent, une réponse HTML est générée, avec les détails des actions possibles pour résoudre le problème. Cette page n’est activée que dans l’environnement de développement. Le code suivant ajoute le filtre d’exception de la page du développeur de la base de données :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();

Filtres d’exceptions

Dans les applications MVC, vous pouvez configurer les filtres d’exception globalement, contrôleur par contrôleur ou action par action. Dans les applications des pages Razor, ils peuvent être configurés globalement ou par modèle de page. Ces filtres gèrent toutes les exceptions non prises en charge qui se produisent pendant l’exécution d’une action de contrôleur ou d’un autre filtre. Pour plus d’informations, consultez Filtres dans ASP.NET Core.

Les filtres d’exception sont utiles pour intercepter les exceptions qui se produisent dans les actions MVC, mais n’offrent pas la même souplesse que l’intergiciel de gestion d’exceptions intégré, UseExceptionHandler. Nous recommandons l’utilisation de UseExceptionHandler, sauf si vous devez gérer les erreurs différemment en fonction de l’action MVC choisie.

Erreurs d’état de modèle

Pour plus d’informations sur la gestion des erreurs d’état de modèle, voir Liaison de modèles et Validation de modèles.

Détails du problème

Les détails du problème ne sont pas le seul format de réponse à décrire une erreur d’API HTTP. Toutefois, ils sont couramment utilisés pour signaler des erreurs pour les API HTTP.

Le service des détails du problème implémente l’interface IProblemDetailsService, qui prend en charge la création de détails de problème dans ASP.NET Core. La méthode d’extension AddProblemDetails sur IServiceCollection enregistre l’implémentation IProblemDetailsService par défaut.

Dans les applications ASP.NET Core, l’intergiciel suivant génère des réponses HTTP sur les détails de problème lorsque AddProblemDetails est appelé, sauf si l’en-tête HTTP de requêteAccept n’inclut pas l’un des types de contenu pris en charge par le IProblemDetailsWriter inscrit (par défaut : application/json) :

  • ExceptionHandlerMiddleware : génère une réponse sur les détails du problème lorsqu’un gestionnaire personnalisé n’est pas défini.
  • StatusCodePagesMiddleware : génère une réponse de détails de problème par défaut.
  • DeveloperExceptionPageMiddlewareHTTP : génère une réponse sur les détails du problème lors du développement lorsque l’en-tête HTTP de la requête Accept n’inclut pas text/html.

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 :

builder.Services.AddProblemDetails();

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

La section suivante montre comment personnaliser le corps de la réponse sur les détails du problème.

Personnaliser les détails du problème

La création automatique d’un ProblemDetails peut être personnalisée à l’aide de l’une des options suivantes :

  1. Utilisez ProblemDetailsOptions.CustomizeProblemDetails
  2. Utiliser un IProblemDetailsWriter personnalisé
  3. Appeler IProblemDetailsService dans un intergiciel

Opération CustomizeProblemDetails

Les détails du problème généré peuvent être personnalisés à l’aide de CustomizeProblemDetails et les personnalisations sont appliquées à tous les détails du problème générés automatiquement.

Le code suivant utilise ProblemDetailsOptions pour définir CustomizeProblemDetails :

builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));

var app = builder.Build();        

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
    app.UseHsts();
}

app.UseStatusCodePages();

Par exemple, un résultat de point de terminaison HTTP Status 400 Bad Request génère le corps de réponse sur les détails du problème suivant :

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "Bad Request",
  "status": 400,
  "nodeId": "my-machine-name"
}

IProblemDetailsWriter personnalisé

Une implémentation IProblemDetailsWriter peut être créée pour les personnalisations avancées.

public class SampleProblemDetailsWriter : IProblemDetailsWriter
{
    // Indicates that only responses with StatusCode == 400
    // are handled by this writer. All others are
    // handled by different registered writers if available.
    public bool CanWrite(ProblemDetailsContext context)
        => context.HttpContext.Response.StatusCode == 400;

    public ValueTask WriteAsync(ProblemDetailsContext context)
    {
        // Additional customizations.

        // Write to the response.
        var response = context.HttpContext.Response;
        return new ValueTask(response.WriteAsJsonAsync(context.ProblemDetails));
    }
}

Remarque : Lors de l’utilisation d’un IProblemDetailsWriter personnalisé, le IProblemDetailsWriter personnalisé doit être inscrit avant d’appeler AddRazorPages, AddControllers, AddControllersWithViewsou AddMvc :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddTransient<IProblemDetailsWriter, SampleProblemDetailsWriter>();

var app = builder.Build();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsWriter>() is
            { } problemDetailsService)
        {

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

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = "Bad Input",
                        Detail = details.Detail,
                        Type = details.Type
                    }
                });
            }
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.DivisionByZeroError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature
        {
            MathError = MathErrorType.NegativeRadicandError
        };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

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

app.Run();

Détails du problème de l’intergiciel

Une autre approche de l’utilisation de ProblemDetailsOptions avec CustomizeProblemDetails consiste à définir le ProblemDetails dans l’intergiciel. Une réponse sur les détails du problème peut être écrite en appelant IProblemDetailsService.WriteAsync :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStatusCodePages();

// Middleware to handle writing problem details to the response.
app.Use(async (context, next) =>
{
    await next(context);
    var mathErrorFeature = context.Features.Get<MathErrorFeature>();
    if (mathErrorFeature is not null)
    {
        if (context.RequestServices.GetService<IProblemDetailsService>() is
                                                           { } problemDetailsService)
        {
            (string Detail, string Type) details = mathErrorFeature.MathError switch
            {
                MathErrorType.DivisionByZeroError => ("Divison by zero is not defined.",
                "https://en.wikipedia.org/wiki/Division_by_zero"),
                _ => ("Negative or complex numbers are not valid input.", 
                "https://en.wikipedia.org/wiki/Square_root")
            };

            await problemDetailsService.WriteAsync(new ProblemDetailsContext
            {
                HttpContext = context,
                ProblemDetails =
                {
                    Title = "Bad Input",
                    Detail = details.Detail,
                    Type = details.Type
                }
            });
        }
    }
});

// /divide?numerator=2&denominator=4
app.MapGet("/divide", (HttpContext context, double numerator, double denominator) =>
{
    if (denominator == 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.DivisionByZeroError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

    return Results.Ok(numerator / denominator);
});

// /squareroot?radicand=16
app.MapGet("/squareroot", (HttpContext context, double radicand) =>
{
    if (radicand < 0)
    {
        var errorType = new MathErrorFeature { MathError =
                                               MathErrorType.NegativeRadicandError };
        context.Features.Set(errorType);
        return Results.BadRequest();
    }

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

app.MapControllers();

app.Run();

Dans le code précédent, les points de terminaison d’API minimaux /divide et /squareroot retournent la réponse de problème personnalisée attendue lors de l’entrée d’erreur.

Les points de terminaison du contrôleur d’API retournent la réponse au problème par défaut lors de l’entrée d’erreur, et non la réponse au problème personnalisée. La réponse au problème par défaut est retournée, car le contrôleur d’API a écrit dans le flux de réponse, Détails du problème pour les codes d’erreur d’état, avant que IProblemDetailsService.WriteAsync ne soit appelé et la réponse n’est plus écrite.

La valeur ValuesController suivante retourne BadRequestResult, qui écrit dans le flux de réponse et empêche donc le retour de la réponse au problème personnalisé.

[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));
    }

}

La valeur suivante Values3Controller retourne ControllerBase.Problem afin que le résultat du problème personnalisé attendu soit retourné :

[Route("api/[controller]/[action]")]
[ApiController]
public class Values3Controller : ControllerBase
{
    // /api/values3/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 Problem(
                title: "Bad Input",
                detail: "Divison by zero is not defined.",
                type: "https://en.wikipedia.org/wiki/Division_by_zero",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

        return Ok(Numerator / Denominator);
    }

    // /api/values3/squareroot/4
    [HttpGet("{radicand}")]
    public IActionResult Squareroot(double radicand)
    {
        if (radicand < 0)
        {
            var errorType = new MathErrorFeature
            {
                MathError = MathErrorType.NegativeRadicandError
            };
            HttpContext.Features.Set(errorType);
            return Problem(
                title: "Bad Input",
                detail: "Negative or complex numbers are not valid input.",
                type: "https://en.wikipedia.org/wiki/Square_root",
                statusCode: StatusCodes.Status400BadRequest
                );
        }

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

}

Produire une charge utile ProblemDetails pour les exceptions

Prenons l’application suivante :

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();

Dans les environnements hors développement, lorsqu’une exception se produit, la réponse ProblemDetails standard qui est retournée au client est la suivante :

{
"type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title":"An error occurred while processing your request.",
"status":500,"traceId":"00-b644<snip>-00"
}

Pour la plupart des applications, le code précédent est suffisant pour les exceptions. Toutefois, la section suivante montre une manière d’obtenir des réponses plus détaillées aux problèmes.

En dehors d’une page de gestionnaire d’exceptions personnalisée, il est possible de fournir une expression lambda à UseExceptionHandler, L’utilisation d’une expression lambda permet d’accéder à l’erreur et d’écrire une réponse sur les détails du problème avec IProblemDetailsService.WriteAsync :

using Microsoft.AspNetCore.Diagnostics;
using static System.Net.Mime.MediaTypeNames;

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();
}
else
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            context.Response.ContentType = Text.Plain;

            var title = "Bad Input";
            var detail = "Invalid input";
            var type = "https://errors.example.com/badInput";

            if (context.RequestServices.GetService<IProblemDetailsService>() is
                { } problemDetailsService)
            {
                var exceptionHandlerFeature =
               context.Features.Get<IExceptionHandlerFeature>();

                var exceptionType = exceptionHandlerFeature?.Error;
                if (exceptionType != null &&
                   exceptionType.Message.Contains("infinity"))
                {
                    title = "Argument exception";
                    detail = "Invalid input";
                    type = "https://errors.example.com/argumentException";
                }

                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                {
                    Title = title,
                    Detail = detail,
                    Type = type
                }
                });
            }
        });
    });
}

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

Warning

Ne communiquez pas d’informations sensibles sur les erreurs aux clients. Cela représenterait un risque de sécurité.

Une autre approche à la génération des détails d’un problème consiste à utiliser le package NuGet tiers Hellang.Middleware.ProblemDetails qui peut être utilisé pour mapper des exceptions et des erreurs client aux détails du problème.

Ressources supplémentaires

Par Tom Dykstra

Cet article aborde des approches courantes pour gérer les erreurs dans les applications web ASP.NET Core. Consultez Gérer les erreurs dans les API web ASP.NET Core pour les API web.

Page d’exceptions du développeur

La Page d’exception du développeur affiche des informations détaillées sur les exceptions des requêtes non prises en charge. Les applications ASP.NET Core activent la page d’exception du développeur par défaut en présence des deux éléments suivants :

La page d’exception du développeur s’exécute au début du pipeline intergiciel, de manière qu’elle puisse intercepter les exceptions non prises en charge levées dans l’intergiciel qui suit.

Des informations détaillées sur les exceptions ne doivent pas être affichées publiquement lorsque l’application s’exécute dans l’environnement de production. Pour plus d’informations sur la configuration des environnements, consultez Utiliser plusieurs environnements dans ASP.NET Core.

La page d’exception du développeur peut inclure les informations suivantes sur l’exception et la requête :

  • Trace de pile
  • Paramètres de la chaîne de requête, le cas échéant
  • Cookie, le cas échéant
  • En-têtes

Il n’est pas garanti que la page d’exception du développeur fournisse des informations. Utilisez la journalisation pour obtenir des informations complètes sur l’erreur.

Page Gestionnaire d’exceptions

Pour configurer une page de gestion des erreurs personnalisée en fonction de l’environnement de production, appelez UseExceptionHandler. Cet intergiciel de gestion des exceptions :

  • Intercepte et consigne les exceptions non prises en charge.
  • Réexécute la requête dans un pipeline de remplacement à l’aide du chemin d’accès indiqué. La demande n’est pas réexécutée si la réponse a démarré. Le code généré par le modèle réexécute la requête à l’aide du chemin d’accès /Error.

Warning

Si le pipeline de remplacement lève une exception qui lui est propre, l’intergiciel de gestion des exception lève à nouveau l’exception d’origine.

Dans l’exemple suivant, UseExceptionHandler ajoute l’intergiciel de gestion des exceptions dans des environnements hors développement :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Le modèle d’application des pages Razor fournit une page d’erreur (.cshtml) et une classe PageModel (ErrorModel) dans le dossier Pages. Pour une application MVC, le modèle de projet comprend une méthode d’action Error et un affichage d’erreur pour le contrôleur Home.

L’intergiciel de gestion des exceptions réexécute la requête à l’aide de la méthode HTTP d’origine. Si un point de terminaison de gestionnaire d’erreurs est limité à un ensemble spécifique de méthodes HTTP, il ne s’exécute que pour ces méthodes HTTP. Par exemple, une action de contrôleur MVC qui utilise l’attribut [HttpGet] s’exécute uniquement pour les requêtes GET. Pour garantir que toutes les requêtes atteignent la page de gestion des erreurs personnalisée, ne les limitez pas à un ensemble spécifique de méthodes HTTP.

Pour gérer les exceptions différemment en fonction de la méthode HTTP d’origine :

  • Pour les pages Razor, créez plusieurs méthodes de gestionnaire. Par exemple, utilisez OnGet pour gérer les exceptions GET et OnPost pour gérer les exceptions POST.
  • Pour MVC, appliquez des attributs de verbe HTTP à plusieurs actions. Vous pouvez par exemple utiliser [HttpGet] pour gérer les exceptions GET et [HttpPost] pour gérer les exceptions POST.

Pour autoriser les utilisateurs non authentifiés à afficher la page de gestion des erreurs personnalisée, assurez-vous qu’elle prend en charge l’accès anonyme.

Accéder à l'exception

Utilisez IExceptionHandlerPathFeature pour accéder à l’exception et au chemin d’accès de la requête d’origine dans un gestionnaire d’erreurs. L’exemple suivant utilise IExceptionHandlerPathFeature pour obtenir plus d’informations sur l’exception levée :

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string? RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string? ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();

        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "The file was not found.";
        }

        if (exceptionHandlerPathFeature?.Path == "/")
        {
            ExceptionMessage ??= string.Empty;
            ExceptionMessage += " Page: Home.";
        }
    }
}

Warning

Ne communiquez pas d’informations sensibles sur les erreurs aux clients. Cela représenterait un risque de sécurité.

Expression lambda Gestionnaire d’exceptions

En dehors d’une page de gestionnaire d’exceptions personnalisée, il est possible de fournir une expression lambda à UseExceptionHandler, ce qui permet d’accéder à l’erreur avant de retourner la réponse.

Le code suivant utilise une expression lambda pour la gestion des exceptions :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;

            // using static System.Net.Mime.MediaTypeNames;
            context.Response.ContentType = Text.Plain;

            await context.Response.WriteAsync("An exception was thrown.");

            var exceptionHandlerPathFeature =
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync(" The file was not found.");
            }

            if (exceptionHandlerPathFeature?.Path == "/")
            {
                await context.Response.WriteAsync(" Page: Home.");
            }
        });
    });

    app.UseHsts();
}

Warning

Ne communiquez pas d’informations sensibles sur les erreurs aux clients. Cela représenterait un risque de sécurité.

UseStatusCodePages

Par défaut, une application ASP.NET Core ne fournit pas une page de codes d’état pour les codes d’état des erreurs HTTP, comme 404 – Introuvable. Lorsque l’application définit un code d’état d’erreur HTTP 400-599 qui n’a pas de corps, elle retourne le code d’état et un corps de réponse vide. Pour activer les gestionnaires de texte uniquement par défaut pour les codes d’état d’erreur courants, appelez UseStatusCodePages dans Program.cs :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages();

Appelez UseStatusCodePages avant l’intergiciel de gestion des requêtes. Par exemple, appelez UseStatusCodePages avant l’intergiciel de fichier statique et l’intergiciel des points de terminaison.

Quand UseStatusCodePages n’est pas utilisé, la navigation vers une URL sans point de terminaison retourne un message d’erreur dépendant du navigateur indiquant que le point de terminaison est introuvable. Quand UseStatusCodePages est appelé, le navigateur retourne la réponse suivante :

Status Code: 404; Not Found

UseStatusCodePages n’est généralement pas utilisé en production, car il retourne un message qui n’est pas utile pour les utilisateurs.

Remarque

L’intergiciel des pages de codes d’état n’intercepte pas les exceptions. Pour fournir une page de gestion des erreurs personnalisée, utilisez la page du gestionnaire d’exception.

UseStatusCodePages avec chaîne de format

Pour personnaliser le texte et le type de contenu de la réponse, utilisez la surcharge de UseStatusCodePages qui prend un type de contenu et une chaîne de format :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

// using static System.Net.Mime.MediaTypeNames;
app.UseStatusCodePages(Text.Plain, "Status Code Page: {0}");

Dans le code précédent, {0} est un espace réservé pour le code d’erreur.

UseStatusCodePages avec une chaîne de format n’est généralement pas utilisé en production, car il retourne un message qui n’est pas utile pour les utilisateurs.

UseStatusCodePages avec expression lambda

Pour spécifier un code personnalisé de gestion des erreurs et d’écriture de la réponse, utilisez la surcharge de UseStatusCodePages qui prend une expression lambda :

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePages(async statusCodeContext =>
{
    // using static System.Net.Mime.MediaTypeNames;
    statusCodeContext.HttpContext.Response.ContentType = Text.Plain;

    await statusCodeContext.HttpContext.Response.WriteAsync(
        $"Status Code Page: {statusCodeContext.HttpContext.Response.StatusCode}");
});

UseStatusCodePages avec une expression lambda n’est généralement pas utilisé en production, car il retourne un message qui n’est pas utile pour les utilisateurs.

UseStatusCodePagesWithRedirects

La méthode d’extension UseStatusCodePagesWithRedirects :

  • Envoie un code d’état 302 - Trouvé au client.
  • Redirige le client vers le point de terminaison de gestion des erreurs fourni dans le modèle d’URL. Le point de terminaison de gestion des erreurs affiche généralement les informations d’erreur et retourne HTTP 200.
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");

Le modèle d’URL peut comporter un espace réservé {0} pour le code d’état, comme indiqué dans le code précédent. Si le modèle d’URL commence par un ~ (tilde), le ~ est remplacé par le PathBase de l’application. Lorsque vous spécifiez un point de terminaison dans l’application, créez une vue MVC ou une page Razor pour le point de terminaison.

Cette méthode est généralement utilisée lorsque l’application :

  • Doit rediriger le client vers un autre point de terminaison, généralement dans les cas où une autre application traite l’erreur. Pour les applications web, la barre d’adresses du navigateur client reflète le point de terminaison redirigé.
  • Ne doit pas conserver ni retourner le code d’état d’origine avec la réponse de redirection initiale.

UseStatusCodePagesWithReExecute

La méthode d’extension UseStatusCodePagesWithReExecute :

  • Retourne le code d’état d’origine au client.
  • Génère le corps de la réponse en réexécutant le pipeline de requête avec un autre chemin.
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStatusCodePagesWithReExecute("/StatusCode/{0}");

Si un point de terminaison dans l’application est spécifié, créez une vue MVC ou une page Razor pour le point de terminaison.

Cette méthode est généralement utilisée lorsque l’application doit :

  • Traiter la demande sans la rediriger vers un autre point de terminaison. Pour les applications web, la barre d’adresses du navigateur client reflète le point de terminaison demandé à l’origine.
  • Conserver et retourner le code d’état d’origine avec la réponse.

Le modèle d’URL doit commencer par / et peut inclure un espace réservé {0} pour le code d’état. Pour faire passer le code d’état en tant que paramètre de chaîne de requête, passez un deuxième argument dans UseStatusCodePagesWithReExecute. Exemple :

app.UseStatusCodePagesWithReExecute("/StatusCode", "?statusCode={0}");

Le point de terminaison qui traite l’erreur peut récupérer l’URL d’origine qui a généré l’erreur, comme dans l’exemple suivant :

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class StatusCodeModel : PageModel
{
    public int OriginalStatusCode { get; set; }

    public string? OriginalPathAndQuery { get; set; }

    public void OnGet(int statusCode)
    {
        OriginalStatusCode = statusCode;

        var statusCodeReExecuteFeature =
            HttpContext.Features.Get<IStatusCodeReExecuteFeature>();

        if (statusCodeReExecuteFeature is not null)
        {
            OriginalPathAndQuery = string.Join(
                statusCodeReExecuteFeature.OriginalPathBase,
                statusCodeReExecuteFeature.OriginalPath,
                statusCodeReExecuteFeature.OriginalQueryString);
        }
    }
}

Désactiver les pages de codes d’état

Pour désactiver les pages de codes d’état d’un contrôleur MVC ou d’une méthode d’action, utilisez l’attribut [SkipStatusCodePages].

Pour désactiver les pages de codes d’état pour des requêtes spécifiques dans une méthode de gestionnaire des pages IStatusCodePagesFeature ou dans un contrôleur MVC, utilisez Razor :

public void OnGet()
{
    var statusCodePagesFeature =
        HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature is not null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

Code de gestion des exceptions

Le code dans les pages de gestion des exceptions peut également lever des exceptions. Les pages d’erreur de production doivent être testées minutieusement en faisant particulièrement attention à éviter de lever leurs propres exceptions.

En-têtes de réponse

Une fois les en-têtes d’une réponse envoyés :

  • L’application ne peut pas modifier le code d’état de la réponse.
  • Il est impossible d’exécuter les pages d’exception ou les gestionnaires. La réponse doit être accomplie ou la connexion abandonnée.

Gestion des exceptions de serveur

En plus de la logique de gestion des exceptions d’une application, l’implémentation de serveur HTTP peut gérer certaines exceptions. Si le serveur intercepte une exception avant que les en-têtes de réponse ne soient envoyés, le serveur envoie une réponse 500 - Internal Server Error sans corps de réponse. Si le serveur intercepte une exception une fois que les en-têtes de réponse ont été envoyés, il ferme la connexion. Les requêtes qui ne sont pas gérées par l’application sont gérées par le serveur. Toute exception qui se produit tandis que le serveur traite la demande est gérée par le dispositif de gestion des exceptions du serveur. Ni les pages d’erreur personnalisées de l’application, ni les intergiciels (middleware) de gestion des exceptions, ni les filtres n’ont d’incidence sur ce comportement.

Gestion des exceptions de démarrage

Seule la couche d’hébergement peut gérer les exceptions qui se produisent au démarrage de l’application. L’hôte peut être configuré pour capturer les erreurs de démarrage et capturer les erreurs détaillées.

La couche d’hébergement ne peut afficher la page d’une erreur de démarrage capturée que si celle-ci se produit une fois la liaison établie entre l’adresse d’hôte et le port. Si la liaison échoue :

  • La couche d’hébergement journalise une exception critique.
  • Le processus dotnet tombe en panne.
  • Aucune page d’erreur ne s’affiche lorsque le serveur HTTP est Kestrel.

En cas d’exécution sur IIS (ou Azure App Service) ou IIS Express, une réponse 502.5 - Échec du processus est retournée par le module ASP.NET Core si le processus ne peut pas démarrer. Pour plus d’informations, consultez Résoudre les problèmes liés à ASP.NET Core sur Azure App Service et IIS.

Page d’erreur de base de données

Le filtre d’exception de la page du développeur de la base de données AddDatabaseDeveloperPageExceptionFilter capture les exceptions liées aux bases de données qui peuvent être résolues par des migrations Entity Framework Core. Lorsque ces exceptions se produisent, une réponse HTML est générée, avec les détails des actions possibles pour résoudre le problème. Cette page n’est activée que dans l’environnement de développement. Le code suivant ajoute le filtre d’exception de la page du développeur de la base de données :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddRazorPages();

Filtres d’exceptions

Dans les applications MVC, vous pouvez configurer les filtres d’exception globalement, contrôleur par contrôleur ou action par action. Dans les applications des pages Razor, ils peuvent être configurés globalement ou par modèle de page. Ces filtres gèrent toutes les exceptions non prises en charge qui se produisent pendant l’exécution d’une action de contrôleur ou d’un autre filtre. Pour plus d’informations, consultez Filtres dans ASP.NET Core.

Les filtres d’exception sont utiles pour intercepter les exceptions qui se produisent dans les actions MVC, mais n’offrent pas la même souplesse que l’intergiciel de gestion d’exceptions intégré, UseExceptionHandler. Nous recommandons l’utilisation de UseExceptionHandler, sauf si vous devez gérer les erreurs différemment en fonction de l’action MVC choisie.

Erreurs d’état de modèle

Pour plus d’informations sur la gestion des erreurs d’état de modèle, voir Liaison de modèles et Validation de modèles.

Ressources supplémentaires

Par Kirk Larkin, Tom Dykstra et Steve Smith

Cet article aborde des approches courantes pour gérer les erreurs dans les applications web ASP.NET Core. Consultez Gérer les erreurs dans les API web ASP.NET Core pour les API web.

Affichez ou téléchargez l’exemple de code. (Guide pratique de téléchargement.) L’onglet réseau des outils de développement du navigateur F12 est utile lors du test de l’exemple d’application.

Page d’exceptions du développeur

La Page d’exception du développeur affiche des informations détaillées sur les exceptions des requêtes non prises en charge. Les modèles ASP.NET Core génèrent le code suivant :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Le code précédent mis en surbrillance active la page d’exception de développeur quand l’application est en cours d’exécution dans l’environnement de développement.

Les modèles placent UseDeveloperExceptionPage au début du pipeline intergiciel de manière qu’il puisse intercepter les exceptions non prises en charge levées dans l’intergiciel qui suit.

Le code précédent active la page d’exception du développeur uniquement lorsque l’application s’exécute dans l’environnement de développement. Des informations détaillées sur les exceptions ne doivent pas être affichées publiquement lorsque l’application s’exécute dans l’environnement de production. Pour plus d’informations sur la configuration des environnements, consultez Utiliser plusieurs environnements dans ASP.NET Core.

La page d’exception du développeur peut inclure les informations suivantes sur l’exception et la requête :

  • Trace de pile
  • Paramètres de la chaîne de requête, le cas échéant
  • Cookie, le cas échéant
  • En-têtes

Il n’est pas garanti que la page d’exception du développeur fournisse des informations. Utilisez la journalisation pour obtenir des informations complètes sur l’erreur.

Page Gestionnaire d’exceptions

Pour configurer une page de gestion des erreurs personnalisée en fonction de l’environnement de production, appelez UseExceptionHandler. Cet intergiciel de gestion des exceptions :

  • Intercepte et consigne les exceptions non prises en charge.
  • Réexécute la requête dans un pipeline de remplacement à l’aide du chemin d’accès indiqué. La demande n’est pas réexécutée si la réponse a démarré. Le code généré par le modèle réexécute la requête à l’aide du chemin d’accès /Error.

Warning

Si le pipeline de remplacement lève une exception qui lui est propre, l’intergiciel de gestion des exception lève à nouveau l’exception d’origine.

Dans l’exemple suivant, UseExceptionHandler ajoute l’intergiciel de gestion des exceptions dans des environnements hors développement :

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Le modèle d’application des pages Razor fournit une page d’erreur (.cshtml) et une classe PageModel (ErrorModel) dans le dossier Pages. Pour une application MVC, le modèle de projet comprend une méthode d’action Error et un affichage d’erreur pour le contrôleur Home.

L’intergiciel de gestion des exceptions réexécute la requête à l’aide de la méthode HTTP d’origine. Si un point de terminaison de gestionnaire d’erreurs est limité à un ensemble spécifique de méthodes HTTP, il ne s’exécute que pour ces méthodes HTTP. Par exemple, une action de contrôleur MVC qui utilise l’attribut [HttpGet] s’exécute uniquement pour les requêtes GET. Pour garantir que toutes les requêtes atteignent la page de gestion des erreurs personnalisée, ne les limitez pas à un ensemble spécifique de méthodes HTTP.

Pour gérer les exceptions différemment en fonction de la méthode HTTP d’origine :

  • Pour les pages Razor, créez plusieurs méthodes de gestionnaire. Par exemple, utilisez OnGet pour gérer les exceptions GET et OnPost pour gérer les exceptions POST.
  • Pour MVC, appliquez des attributs de verbe HTTP à plusieurs actions. Vous pouvez par exemple utiliser [HttpGet] pour gérer les exceptions GET et [HttpPost] pour gérer les exceptions POST.

Pour autoriser les utilisateurs non authentifiés à afficher la page de gestion des erreurs personnalisée, assurez-vous qu’elle prend en charge l’accès anonyme.

Accéder à l'exception

Utilisez IExceptionHandlerPathFeature pour accéder à l’exception et au chemin d’accès de la requête d’origine dans un gestionnaire d’erreurs. Le code suivant ajoute ExceptionMessage à la valeur par défaut Pages/Error.cshtml.cs générée par les modèles ASP.NET Core :

[ResponseCache(Duration=0, Location=ResponseCacheLocation.None, NoStore=true)]
[IgnoreAntiforgeryToken]
public class ErrorModel : PageModel
{
    public string RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
    public string ExceptionMessage { get; set; }
    private readonly ILogger<ErrorModel> _logger;

    public ErrorModel(ILogger<ErrorModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
        HttpContext.Features.Get<IExceptionHandlerPathFeature>();
        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "File error thrown";
            _logger.LogError(ExceptionMessage);
        }
        if (exceptionHandlerPathFeature?.Path == "/index")
        {
            ExceptionMessage += " from home page";
        }
    }
}

Warning

Ne communiquez pas d’informations sensibles sur les erreurs aux clients. Cela représenterait un risque de sécurité.

Pour tester l’exception dans l’exemple d’application :

  • Définissez l’environnement sur production.
  • Supprimez les commentaires de webBuilder.UseStartup<Startup>(); dans Program.cs.
  • Sélectionnez Déclencher une exception sur la page d’accueil.

Expression lambda Gestionnaire d’exceptions

En dehors d’une page de gestionnaire d’exceptions personnalisée, il est possible de fournir une expression lambda à UseExceptionHandler, ce qui permet d’accéder à l’erreur avant de retourner la réponse.

Le code suivant utilise une expression lambda pour la gestion des exceptions :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler(errorApp =>
        {
            errorApp.Run(async context =>
            {
                context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;;
                context.Response.ContentType = "text/html";

                await context.Response.WriteAsync("<html lang=\"en\"><body>\r\n");
                await context.Response.WriteAsync("ERROR!<br><br>\r\n");

                var exceptionHandlerPathFeature =
                    context.Features.Get<IExceptionHandlerPathFeature>();

                if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
                {
                    await context.Response.WriteAsync(
                                              "File error thrown!<br><br>\r\n");
                }

                await context.Response.WriteAsync(
                                              "<a href=\"/\">Home</a><br>\r\n");
                await context.Response.WriteAsync("</body></html>\r\n");
                await context.Response.WriteAsync(new string(' ', 512)); 
            });
        });
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Warning

Ne distribuez pas d’informations sensibles sur les erreurs de IExceptionHandlerFeature ou de IExceptionHandlerPathFeature aux clients. Cela représenterait un risque de sécurité.

Pour tester l’expression lambda de gestion des exceptions dans l’exemple d’application :

  • Définissez l’environnement sur production.
  • Supprimez les commentaires de webBuilder.UseStartup<StartupLambda>(); dans Program.cs.
  • Sélectionnez Déclencher une exception sur la page d’accueil.

UseStatusCodePages

Par défaut, une application ASP.NET Core ne fournit pas une page de codes d’état pour les codes d’état des erreurs HTTP, comme 404 – Introuvable. Lorsque l’application définit un code d’état d’erreur HTTP 400-599 qui n’a pas de corps, elle retourne le code d’état et un corps de réponse vide. Pour fournir des pages de codes d’état, utilisez l’intergiciel de pages de codes d’état. Pour activer les gestionnaires exclusivement textuels par défaut des codes d’état d’erreur courants, appelez UseStatusCodePages dans la méthode Startup.Configure :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePages();

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Appelez UseStatusCodePages avant l’intergiciel de gestion des requêtes. Par exemple, appelez UseStatusCodePages avant l’intergiciel de fichier statique et l’intergiciel des points de terminaison.

Quand UseStatusCodePages n’est pas utilisé, la navigation vers une URL sans point de terminaison retourne un message d’erreur dépendant du navigateur indiquant que le point de terminaison est introuvable. Par exemple, navigation vers Home/Privacy2. Quand UseStatusCodePages est appelé, le navigateur retourne :

Status Code: 404; Not Found

UseStatusCodePages n’est généralement pas utilisé en production, car il retourne un message qui n’est pas utile pour les utilisateurs.

Pour tester UseStatusCodePages dans l’exemple d’application :

  • Définissez l’environnement sur production.
  • Supprimez les commentaires de webBuilder.UseStartup<StartupUseStatusCodePages>(); dans Program.cs.
  • Sélectionnez les liens sur la page d’accueil de la page d’accueil.

Remarque

L’intergiciel des pages de codes d’état n’intercepte pas les exceptions. Pour fournir une page de gestion des erreurs personnalisée, utilisez la page du gestionnaire d’exception.

UseStatusCodePages avec chaîne de format

Pour personnaliser le texte et le type de contenu de la réponse, utilisez la surcharge de UseStatusCodePages qui prend un type de contenu et une chaîne de format :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePages(
        "text/plain", "Status code page, status code: {0}");

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Dans le code précédent, {0} est un espace réservé pour le code d’erreur.

UseStatusCodePages avec une chaîne de format n’est généralement pas utilisé en production, car il retourne un message qui n’est pas utile pour les utilisateurs.

Pour tester UseStatusCodePages dans l’exemple d’application, supprimez les commentaires de webBuilder.UseStartup<StartupFormat>(); dans Program.cs.

UseStatusCodePages avec expression lambda

Pour spécifier un code personnalisé de gestion des erreurs et d’écriture de la réponse, utilisez la surcharge de UseStatusCodePages qui prend une expression lambda :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePages(async context =>
    {
        context.HttpContext.Response.ContentType = "text/plain";

        await context.HttpContext.Response.WriteAsync(
            "Status code page, status code: " +
            context.HttpContext.Response.StatusCode);
    });

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

UseStatusCodePages avec une expression lambda n’est généralement pas utilisé en production, car il retourne un message qui n’est pas utile pour les utilisateurs.

Pour tester UseStatusCodePages dans l’exemple d’application, supprimez les commentaires de webBuilder.UseStartup<StartupStatusLambda>(); dans Program.cs.

UseStatusCodePagesWithRedirects

La méthode d’extension UseStatusCodePagesWithRedirects :

  • Envoie un code d’état 302 - Trouvé au client.
  • Redirige le client vers le point de terminaison de gestion des erreurs fourni dans le modèle d’URL. Le point de terminaison de gestion des erreurs affiche généralement les informations d’erreur et retourne HTTP 200.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePagesWithRedirects("/MyStatusCode?code={0}");

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Le modèle d’URL peut comporter un espace réservé {0} pour le code d’état, comme indiqué dans le code précédent. Si le modèle d’URL commence par un ~ (tilde), le ~ est remplacé par le PathBase de l’application. Lorsque vous spécifiez un point de terminaison dans l’application, créez une vue MVC ou une page Razor pour le point de terminaison. Pour consulter un exemple de pages Razor, voir Pages/StatusCode.cshtml dans l’exemple d’application.

Cette méthode est généralement utilisée lorsque l’application :

  • Doit rediriger le client vers un autre point de terminaison, généralement dans les cas où une autre application traite l’erreur. Pour les applications web, la barre d’adresses du navigateur client reflète le point de terminaison redirigé.
  • Ne doit pas conserver ni retourner le code d’état d’origine avec la réponse de redirection initiale.

Pour tester UseStatusCodePages dans l’exemple d’application, supprimez les commentaires de webBuilder.UseStartup<StartupSCredirect>(); dans Program.cs.

UseStatusCodePagesWithReExecute

La méthode d’extension UseStatusCodePagesWithReExecute :

  • Retourne le code d’état d’origine au client.
  • Génère le corps de la réponse en réexécutant le pipeline de requête avec un autre chemin.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseStatusCodePagesWithReExecute("/MyStatusCode2", "?code={0}");

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Si un point de terminaison dans l’application est spécifié, créez une vue MVC ou une page Razor pour le point de terminaison. Vérifiez que UseStatusCodePagesWithReExecute est placé avant UseRouting afin que la requête puisse être redirigée vers la page d’état. Pour consulter un exemple de pages Razor, voir Pages/StatusCode2.cshtml dans l’exemple d’application.

Cette méthode est généralement utilisée lorsque l’application doit :

  • Traiter la demande sans la rediriger vers un autre point de terminaison. Pour les applications web, la barre d’adresses du navigateur client reflète le point de terminaison demandé à l’origine.
  • Conserver et retourner le code d’état d’origine avec la réponse.

Les modèles d’URL et de chaîne de requête peuvent comporter un espace réservé {0} pour le code d’état. Le modèle d’URL doit commencer par /.

Le point de terminaison qui traite l’erreur peut récupérer l’URL d’origine qui a généré l’erreur, comme dans l’exemple suivant :

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class MyStatusCode2Model : PageModel
{
    public string RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public string ErrorStatusCode { get; set; }

    public string OriginalURL { get; set; }
    public bool ShowOriginalURL => !string.IsNullOrEmpty(OriginalURL);

    public void OnGet(string code)
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
        ErrorStatusCode = code;

        var statusCodeReExecuteFeature = HttpContext.Features.Get<
                                               IStatusCodeReExecuteFeature>();
        if (statusCodeReExecuteFeature != null)
        {
            OriginalURL =
                statusCodeReExecuteFeature.OriginalPathBase
                + statusCodeReExecuteFeature.OriginalPath
                + statusCodeReExecuteFeature.OriginalQueryString;
        }
    }
}

Pour consulter un exemple de pages Razor, voir Pages/StatusCode2.cshtml dans l’exemple d’application.

Pour tester UseStatusCodePages dans l’exemple d’application, supprimez les commentaires de webBuilder.UseStartup<StartupSCreX>(); dans Program.cs.

Désactiver les pages de codes d’état

Pour désactiver les pages de codes d’état d’un contrôleur MVC ou d’une méthode d’action, utilisez l’attribut [SkipStatusCodePages].

Pour désactiver les pages de codes d’état pour des requêtes spécifiques dans une méthode de gestionnaire des pages IStatusCodePagesFeature ou dans un contrôleur MVC, utilisez Razor :

public void OnGet()
{
    // using Microsoft.AspNetCore.Diagnostics;
    var statusCodePagesFeature = HttpContext.Features.Get<IStatusCodePagesFeature>();

    if (statusCodePagesFeature != null)
    {
        statusCodePagesFeature.Enabled = false;
    }
}

Code de gestion des exceptions

Le code dans les pages de gestion des exceptions peut également lever des exceptions. Les pages d’erreur de production doivent être testées minutieusement en faisant particulièrement attention à éviter de lever leurs propres exceptions.

En-têtes de réponse

Une fois les en-têtes d’une réponse envoyés :

  • L’application ne peut pas modifier le code d’état de la réponse.
  • Il est impossible d’exécuter les pages d’exception ou les gestionnaires. La réponse doit être accomplie ou la connexion abandonnée.

Gestion des exceptions de serveur

En plus de la logique de gestion des exceptions d’une application, l’implémentation de serveur HTTP peut gérer certaines exceptions. Si le serveur intercepte une exception avant que les en-têtes de réponse ne soient envoyés, le serveur envoie une réponse 500 - Internal Server Error sans corps de réponse. Si le serveur intercepte une exception une fois que les en-têtes de réponse ont été envoyés, il ferme la connexion. Les requêtes qui ne sont pas gérées par l’application sont gérées par le serveur. Toute exception qui se produit tandis que le serveur traite la demande est gérée par le dispositif de gestion des exceptions du serveur. Ni les pages d’erreur personnalisées de l’application, ni les intergiciels (middleware) de gestion des exceptions, ni les filtres n’ont d’incidence sur ce comportement.

Gestion des exceptions de démarrage

Seule la couche d’hébergement peut gérer les exceptions qui se produisent au démarrage de l’application. L’hôte peut être configuré pour capturer les erreurs de démarrage et capturer les erreurs détaillées.

La couche d’hébergement ne peut afficher la page d’une erreur de démarrage capturée que si celle-ci se produit une fois la liaison établie entre l’adresse d’hôte et le port. Si la liaison échoue :

  • La couche d’hébergement journalise une exception critique.
  • Le processus dotnet tombe en panne.
  • Aucune page d’erreur ne s’affiche lorsque le serveur HTTP est Kestrel.

En cas d’exécution sur IIS (ou Azure App Service) ou IIS Express, une réponse 502.5 - Échec du processus est retournée par le module ASP.NET Core si le processus ne peut pas démarrer. Pour plus d’informations, consultez Résoudre les problèmes liés à ASP.NET Core sur Azure App Service et IIS.

Page d’erreur de base de données

Le filtre d’exception de la page du développeur de la base de données AddDatabaseDeveloperPageExceptionFilter capture les exceptions liées aux bases de données qui peuvent être résolues par des migrations Entity Framework Core. Lorsque ces exceptions se produisent, une réponse HTML est générée, avec les détails des actions possibles pour résoudre le problème. Cette page n’est activée que dans l’environnement de développement. Le code suivant a été généré par les modèles de pages Razor ASP.NET Core lorsque des comptes d’utilisateur individuels ont été spécifiés :

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDatabaseDeveloperPageExceptionFilter();
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();
}

Filtres d’exceptions

Dans les applications MVC, vous pouvez configurer les filtres d’exception globalement, contrôleur par contrôleur ou action par action. Dans les applications des pages Razor, ils peuvent être configurés globalement ou par modèle de page. Ces filtres gèrent toutes les exceptions non prises en charge qui se produisent pendant l’exécution d’une action de contrôleur ou d’un autre filtre. Pour plus d’informations, consultez Filtres dans ASP.NET Core.

Les filtres d’exception sont utiles pour intercepter les exceptions qui se produisent dans les actions MVC, mais n’offrent pas la même souplesse que l’intergiciel de gestion d’exceptions intégré, UseExceptionHandler. Nous recommandons l’utilisation de UseExceptionHandler, sauf si vous devez gérer les erreurs différemment en fonction de l’action MVC choisie.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Erreurs d’état de modèle

Pour plus d’informations sur la gestion des erreurs d’état de modèle, voir Liaison de modèles et Validation de modèles.

Ressources supplémentaires

Par Tom Dykstra et Steve Smith

Cet article aborde des approches courantes pour gérer les erreurs dans les applications web ASP.NET Core. Consultez Gérer les erreurs dans les API web ASP.NET Core pour les API web.

Affichez ou téléchargez l’exemple de code. (Guide pratique de téléchargement.)

Page d’exceptions du développeur

La Page d’exceptions du développeur affiche des informations détaillées sur les exceptions des demandes. Les modèles ASP.NET Core génèrent le code suivant :

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Le code précédent active la page d’exception de développeur quand l’application est en cours d’exécution dans l’environnement de développement.

Les modèles placent UseDeveloperExceptionPage avant tout intergiciel afin que les exceptions soient interceptées dans l’intergiciel qui suit.

Le code précédent active la page d’exception du développeur uniquement quand l’application est en cours d’exécution dans l’environnement de développement. Les informations détaillées sur les exceptions ne doivent pas être affichées publiquement lorsque l’application s’exécute en production. Pour plus d’informations sur la configuration des environnements, consultez Utiliser plusieurs environnements dans ASP.NET Core.

La page d’exception du développeur comprend les informations suivantes sur l’exception et la requête :

  • Trace de pile
  • Paramètres de la chaîne de requête, le cas échéant
  • Cookie, le cas échéant
  • En-têtes

Page Gestionnaire d’exceptions

Pour configurer une page de gestion des erreurs personnalisée en fonction de l’environnement de production, utilisez le middleware de gestion des exceptions. Le middleware :

  • Intercepte et consigne les exceptions.
  • Réexécute la requête dans un autre pipeline pour la page ou le contrôleur indiqué(e). La demande n’est pas réexécutée si la réponse a démarré. Le code généré par le modèle réexécute la requête sur /Error.

Dans l’exemple suivant, UseExceptionHandler ajoute le middleware de gestion des exceptions dans des environnements autres que les environnements de développement :

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

Le modèle d’application des pages Razor fournit une page d’erreur (.cshtml) et une classe PageModel (ErrorModel) dans le dossier Pages. Pour une application MVC, le modèle de projet comprend une méthode d’action d’erreur et un affichage d’erreur dans le contrôleur Home.

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. Autorisez l’accès anonyme à la méthode si les utilisateurs non authentifiés doivent consulter la vue des erreurs.

Accéder à l'exception

Utilisez IExceptionHandlerPathFeature pour accéder à l’exception et au chemin de la demande d’origine dans une page ou un contrôleur de gestionnaire d’erreurs :

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel
{
    public string RequestId { get; set; }
    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
    public string ExceptionMessage { get; set; }

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;

        var exceptionHandlerPathFeature =
            HttpContext.Features.Get<IExceptionHandlerPathFeature>();
        if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
        {
            ExceptionMessage = "File error thrown";
        }
        if (exceptionHandlerPathFeature?.Path == "/index")
        {
            ExceptionMessage += " from home page";
        }
    }
}

Warning

Ne communiquez pas d’informations sensibles sur les erreurs aux clients. Cela représenterait un risque de sécurité.

Pour déclencher la page de gestion des exceptions précédente, définissez l’environnement sur productions et forcez une exception.

Expression lambda Gestionnaire d’exceptions

En dehors d’une page de gestionnaire d’exceptions personnalisée, il est possible de fournir une expression lambda à UseExceptionHandler, ce qui permet d’accéder à l’erreur avant de retourner la réponse.

Voici un exemple d’utilisation d’une expression lambda pour la gestion des exceptions :

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
   app.UseExceptionHandler(errorApp =>
   {
        errorApp.Run(async context =>
        {
            context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
            context.Response.ContentType = "text/html";

            await context.Response.WriteAsync("<html lang=\"en\"><body>\r\n");
            await context.Response.WriteAsync("ERROR!<br><br>\r\n");

            var exceptionHandlerPathFeature = 
                context.Features.Get<IExceptionHandlerPathFeature>();

            if (exceptionHandlerPathFeature?.Error is FileNotFoundException)
            {
                await context.Response.WriteAsync("File error thrown!<br><br>\r\n");
            }

            await context.Response.WriteAsync("<a href=\"/\">Home</a><br>\r\n");
            await context.Response.WriteAsync("</body></html>\r\n");
            await context.Response.WriteAsync(new string(' ', 512)); // IE padding
        });
    });
    app.UseHsts();
}

Dans le code précédent, await context.Response.WriteAsync(new string(' ', 512)); est ajouté afin que le navigateur Internet Explorer affiche le message d’erreur plutôt qu’un message d’erreur IE. Pour plus d’informations, consultez ce problème GitHub.

Warning

Ne distribuez pas d’informations sensibles sur les erreurs de IExceptionHandlerFeature ou de IExceptionHandlerPathFeature aux clients. Cela représenterait un risque de sécurité.

Pour afficher le résultat de l’expression lambda de gestion des exceptions dans l’exemple d’application, utilisez les directive de préprocesseur ProdEnvironment et ErrorHandlerLambda et sélectionnez Déclencher une exception sur la page d’accueil.

UseStatusCodePages

Par défaut, une application ASP.NET Core ne fournit pas une page de codes d’état pour les codes d’état HTTP, comme 404 - Introuvable. L’application retourne un code d’état et un corps de réponse vide. Pour fournir des pages de codes d’état, utilisez le middleware Pages de codes d’état.

L’intergiciel est mis à disposition par le package Microsoft.AspNetCore.Diagnostics.

Pour activer les gestionnaires exclusivement textuels par défaut des codes d’état d’erreur courants, appelez UseStatusCodePages dans la méthode Startup.Configure :

app.UseStatusCodePages();

Appelez UseStatusCodePages avant les middlewares de gestion des demandes (par exemple, le middleware de fichiers statiques et le middleware MVC).

Quand UseStatusCodePages n’est pas utilisé, la navigation vers une URL sans point de terminaison retourne un message d’erreur dépendant du navigateur indiquant que le point de terminaison est introuvable. Par exemple, navigation vers Home/Privacy2. Quand UseStatusCodePages est appelé, le navigateur retourne :

Status Code: 404; Not Found

UseStatusCodePages avec chaîne de format

Pour personnaliser le texte et le type de contenu de la réponse, utilisez la surcharge de UseStatusCodePages qui prend un type de contenu et une chaîne de format :

app.UseStatusCodePages(
    "text/plain", "Status code page, status code: {0}");

UseStatusCodePages avec expression lambda

Pour spécifier un code personnalisé de gestion des erreurs et d’écriture de la réponse, utilisez la surcharge de UseStatusCodePages qui prend une expression lambda :

app.UseStatusCodePages(async context =>
{
    context.HttpContext.Response.ContentType = "text/plain";

    await context.HttpContext.Response.WriteAsync(
        "Status code page, status code: " + 
        context.HttpContext.Response.StatusCode);
});

UseStatusCodePagesWithRedirects

La méthode d’extension UseStatusCodePagesWithRedirects :

  • Envoie un code d’état 302 - Trouvé au client.
  • Redirige le client à l’emplacement fourni dans le modèle d’URL.
app.UseStatusCodePagesWithRedirects("/StatusCode?code={0}");

Le modèle d’URL peut comporter un espace réservé {0} pour le code d’état, comme dans l’exemple. Si le modèle d’URL commence par un ~ (tilde), le ~ est remplacé par le PathBase de l’application. Si vous pointez vers un point de terminaison dans l’application, créez une vue MVC ou une page Razor pour le point de terminaison. Pour obtenir un exemple de pages Razor, consultez Pages/StatusCode.cshtml dans l’exemple d’application.

Cette méthode est généralement utilisée lorsque l’application :

  • Doit rediriger le client vers un autre point de terminaison, généralement dans les cas où une autre application traite l’erreur. Pour les applications web, la barre d’adresses du navigateur client reflète le point de terminaison redirigé.
  • Ne doit pas conserver ni retourner le code d’état d’origine avec la réponse de redirection initiale.

UseStatusCodePagesWithReExecute

La méthode d’extension UseStatusCodePagesWithReExecute :

  • Retourne le code d’état d’origine au client.
  • Génère le corps de la réponse en réexécutant le pipeline de requête avec un autre chemin.
app.UseStatusCodePagesWithReExecute("/StatusCode","?code={0}");

Si vous pointez vers un point de terminaison dans l’application, créez une vue MVC ou une page Razor pour le point de terminaison. Vérifiez que UseStatusCodePagesWithReExecute est placé avant UseRouting afin que la requête puisse être redirigée vers la page d’état. Pour obtenir un exemple de pages Razor, consultez Pages/StatusCode.cshtml dans l’exemple d’application.

Cette méthode est généralement utilisée lorsque l’application doit :

  • Traiter la demande sans la rediriger vers un autre point de terminaison. Pour les applications web, la barre d’adresses du navigateur client reflète le point de terminaison demandé à l’origine.
  • Conserver et retourner le code d’état d’origine avec la réponse.

Les modèles d’URL et de chaîne de requête peuvent comporter un espace réservé ({0}) pour le code d’état. Le modèle d’URL doit commencer par une barre oblique (/). Si vous utilisez un espace réservé dans le chemin, vérifiez que le point de terminaison (page ou contrôleur) peut traiter le segment de chemin. Par exemple, une page Razor pour les erreurs doit accepter la valeur du segment de chemin d’accès facultatif avec la directive @page :

@page "{code?}"

Le point de terminaison qui traite l’erreur peut récupérer l’URL d’origine qui a généré l’erreur, comme dans l’exemple suivant :

var statusCodeReExecuteFeature = HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
if (statusCodeReExecuteFeature != null)
{
    OriginalURL =
        statusCodeReExecuteFeature.OriginalPathBase
        + statusCodeReExecuteFeature.OriginalPath
        + statusCodeReExecuteFeature.OriginalQueryString;
}

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. Autorisez l’accès anonyme à la méthode si les utilisateurs non authentifiés doivent consulter la vue des erreurs.

Désactiver les pages de codes d’état

Pour désactiver les pages de codes d’état d’un contrôleur MVC ou d’une méthode d’action, utilisez l’attribut [SkipStatusCodePages].

Pour désactiver les pages de codes d’état pour des requêtes spécifiques dans une méthode de gestionnaire des pages IStatusCodePagesFeature ou dans un contrôleur MVC, utilisez Razor :

var statusCodePagesFeature = HttpContext.Features.Get<IStatusCodePagesFeature>();

if (statusCodePagesFeature != null)
{
    statusCodePagesFeature.Enabled = false;
}

Code de gestion des exceptions

Le code dans les pages de gestion des exceptions peut lever des exceptions. Il est souvent judicieux de mettre dans les pages d’erreur de production du contenu purement statique.

En-têtes de réponse

Une fois les en-têtes d’une réponse envoyés :

  • L’application ne peut pas modifier le code d’état de la réponse.
  • Il est impossible d’exécuter les pages d’exception ou les gestionnaires. La réponse doit être accomplie ou la connexion abandonnée.

Gestion des exceptions de serveur

En plus de la logique de gestion des exceptions de votre application, l’implémentation de serveur HTTP peut gérer certaines exceptions. Si le serveur intercepte une exception avant que les en-têtes de réponse ne soient envoyés, le serveur envoie une réponse 500 Erreur interne du serveur sans corps de réponse. Si le serveur intercepte une exception une fois que les en-têtes de réponse ont été envoyés, il ferme la connexion. Les demandes qui ne sont pas gérées par votre application sont gérées par le serveur. Toute exception qui se produit tandis que le serveur traite la demande est gérée par le dispositif de gestion des exceptions du serveur. Ni les pages d’erreur personnalisées de l’application, ni les intergiciels (middleware) de gestion des exceptions, ni les filtres n’ont d’incidence sur ce comportement.

Gestion des exceptions de démarrage

Seule la couche d’hébergement peut gérer les exceptions qui se produisent au démarrage de l’application. L’hôte peut être configuré pour capturer les erreurs de démarrage et capturer les erreurs détaillées.

La couche d’hébergement ne peut afficher la page d’une erreur de démarrage capturée que si celle-ci se produit une fois la liaison établie entre l’adresse d’hôte et le port. Si la liaison échoue :

  • La couche d’hébergement journalise une exception critique.
  • Le processus dotnet tombe en panne.
  • Aucune page d’erreur ne s’affiche lorsque le serveur HTTP est Kestrel.

En cas d’exécution sur IIS (ou Azure App Service) ou IIS Express, une réponse 502.5 - Échec du processus est retournée par le module ASP.NET Core si le processus ne peut pas démarrer. Pour plus d’informations, consultez Résoudre les problèmes liés à ASP.NET Core sur Azure App Service et IIS.

Page d’erreur de base de données

L’intergiciel de Page d’erreur de base de données capture les exceptions liées aux bases de données qui peuvent être résolues par des migrations Entity Framework. Lorsque ces exceptions se produisent, une réponse HTML comportant le détail des actions possibles pour résoudre le problème est générée. Cette page ne doit être activée que dans l’environnement de développement. Pour cela, ajoutez du code à Startup.Configure :

if (env.IsDevelopment())
{
    app.UseDatabaseErrorPage();
}

UseDatabaseErrorPage nécessite le package NuGet Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.

Filtres d’exceptions

Dans les applications MVC, vous pouvez configurer les filtres d’exception globalement, contrôleur par contrôleur ou action par action. Dans les applications des pages Razor, ils peuvent être configurés globalement ou par modèle de page. Ces filtres gèrent les exceptions non prises en charge qui se produisent pendant l’exécution d’une action de contrôleur ou d’un autre filtre. Pour plus d’informations, consultez Filtres dans ASP.NET Core.

Conseil

Les filtres d’exceptions sont utiles pour intercepter les exceptions qui se produisent dans les actions MVC, mais n’offrent pas la même souplesse que le middleware de gestion des exceptions. Nous vous recommandons d’utiliser le middleware. N’utilisez des filtres que si vous devez gérer les erreurs différemment en fonction de l’action MVC choisie.

Erreurs d’état de modèle

Pour plus d’informations sur la gestion des erreurs d’état de modèle, voir Liaison de modèles et Validation de modèles.

Ressources supplémentaires