Fehlerbehandlung in ASP.NET Core-Web-APIs

Dieser Artikel beschreibt die Verarbeitung und Anpassung der Fehlerbehandlung bei ASP.NET Core-Web-APIs.

Anzeigen oder Herunterladen von Beispielcode (Herunterladen von)

Seite mit Ausnahmen für Entwickler

Die Seite mit Ausnahmen für Entwickler ist ein nützliches Tool, um detaillierte Stapelüberwachungen für Serverfehler zu erhalten. Es verwendet DeveloperExceptionPageMiddleware, um synchrone und asynchrone Ausnahmen aus der HTTP-Pipeline zu erfassen und um Fehlerantworten zu generieren. Betrachten Sie zur Darstellung die folgende Controlleraktion:

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

Führen Sie den folgenden curl-Befehl aus, um die vorherige Aktion zu testen:

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

Die Seite mit Ausnahmen für Entwickler zeigt in ASP.NET Core 3.0 und später eine Nur-Text-Antwort an, wenn der Client keine HTML-formatierte Ausgabe anfordert. Die folgende Ausgabe wird angezeigt:

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

System.ArgumentException: We don't offer a weather forecast for chicago. (Parameter 'city')
   at WebApiSample.Controllers.WeatherForecastController.Get(String city) in C:\working_folder\aspnet\AspNetCore.Docs\aspnetcore\web-api\handle-errors\samples\3.x\Controllers\WeatherForecastController.cs:line 34
   at lambda_method(Closure , Object , Object[] )
   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

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

Legen Sie den HTTP-Anforderungsheader Accept auf den Medientyp text/html fest, um stattdessen eine HTML-formatierte Antwort anzuzeigen. Beispiel:

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

Beachten Sie den folgenden Auszug aus der HTTP-Antwort:

In ASP.NET Core 2.2 und früher wird auf der Seite mit Ausnahmen für Entwickler eine HTML-formatierte Antwort angezeigt. Beachten Sie beispielsweise den folgenden Auszug aus der HTTP-Antwort:

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

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

Die HTML-formatierte Antwort wird beim Durchführen von Tests in Tools wie etwa Postman nützlich. Auf der folgenden Bildschirmaufnahme sind jeweils die in Klartext verfassten und die HTML-formatierte Antworten in Postman dargestellt:

Testen auf der Seite mit Ausnahmen für Entwickler in Postman

Warnung

Aktivieren Sie die Seite mit Ausnahmen für Entwickler nur dann, wenn die App in der Entwicklungsumgebung ausgeführt wird. Geben Sie keine ausführlichen Ausnahmeinformationen öffentlich weiter, wenn die App in der Produktion ausgeführt wird. Weitere Informationen zum Konfigurieren der Umgebungen finden Sie unter Verwenden von mehreren Umgebungen in ASP.NET Core.

Markieren Sie die Aktionsmethode für die Fehlerbehandlung nicht mit HTTP-Methodenattributen wie HttpGet. Durch explizite Verben könnte bei einigen Anforderungen verhindert werden, dass diese Aktionsmethode zum Einsatz kommt. Lassen Sie anonymen Zugriff auf die -Methode zu, wenn nicht authentifizierte Benutzer den Fehler sehen sollten.

Ausnahmehandler

In nicht zur Entwicklung dienenden Umgebungen kann Middleware zur Ausnahmebehandlung verwendet werden, um eine Fehlernutzlast zu erzeugen:

  1. Rufen Sie UseExceptionHandler in Startup.Configure auf, um die Middleware zu verwenden:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/error");
        }
    
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/error");
            app.UseHsts();
        }
    
        app.UseHttpsRedirection();
        app.UseMvc();
    }
    
  2. Konfigurieren Sie eine Controlleraktion, um auf die /error-Route zu antworten:

    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error")]
        public IActionResult Error() => Problem();
    }
    
    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error")]
        public ActionResult Error([FromServices] IHostingEnvironment webHostEnvironment)
        {
            var feature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
            var ex = feature?.Error;
            var isDev = webHostEnvironment.IsDevelopment();
            var problemDetails = new ProblemDetails
            {
                Status = (int)HttpStatusCode.InternalServerError,
                Instance = feature?.Path,
                Title = isDev ? $"{ex.GetType().Name}: {ex.Message}" : "An error occurred.",
                Detail = isDev ? ex.StackTrace : null,
            };
    
            return StatusCode(problemDetails.Status.Value, problemDetails);
        }
    }
    

Die oben stehende Error-Aktion sendet eine mit RFC 7807 konforme Nutzlast an den Client.

Middleware zur Ausnahmebehandlung kann auch eine detailliertere Ausgabe der Inhaltsaushandlung in der lokalen Entwicklungsumgebung bieten. Führen Sie die folgenden Schritte aus, um für ein konsistentes Nutzlastformat in Entwicklungs- und Produktionsumgebungen zu sorgen:

  1. Registrieren Sie umgebungsspezifische Middleware zur Ausnahmebehandlung in Startup.Configure:

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

    Im oben stehenden Code ist die Middleware mit Folgendem registriert:

    • Route /error-local-development in der Entwicklungsumgebung.
    • Route /error in einer nicht zur Entwicklung dienenden Umgebung.

  2. Wenden Sie das Attributrouting auf Controlleraktionen an:

    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error-local-development")]
        public IActionResult ErrorLocalDevelopment(
            [FromServices] IWebHostEnvironment webHostEnvironment)
        {
            if (webHostEnvironment.EnvironmentName != "Development")
            {
                throw new InvalidOperationException(
                    "This shouldn't be invoked in non-development environments.");
            }
    
            var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
    
            return Problem(
                detail: context.Error.StackTrace,
                title: context.Error.Message);
        }
    
        [Route("/error")]
        public IActionResult Error() => Problem();
    }
    
    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error-local-development")]
        public IActionResult ErrorLocalDevelopment(
            [FromServices] IHostingEnvironment webHostEnvironment)
        {
            if (!webHostEnvironment.IsDevelopment())
            {
                throw new InvalidOperationException(
                    "This shouldn't be invoked in non-development environments.");
            }
    
            var feature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
            var ex = feature?.Error;
    
            var problemDetails = new ProblemDetails
            {
                Status = (int)HttpStatusCode.InternalServerError,
                Instance = feature?.Path,
                Title = ex.GetType().Name,
                Detail = ex.StackTrace,
            };
    
            return StatusCode(problemDetails.Status.Value, problemDetails);
        }
    
        [Route("/error")]
        public ActionResult Error(
            [FromServices] IHostingEnvironment webHostEnvironment)
        {
            var feature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
            var ex = feature?.Error;
            var isDev = webHostEnvironment.IsDevelopment();
            var problemDetails = new ProblemDetails
            {
                Status = (int)HttpStatusCode.InternalServerError,
                Instance = feature?.Path,
                Title = isDev ? $"{ex.GetType().Name}: {ex.Message}" : "An error occurred.",
                Detail = isDev ? ex.StackTrace : null,
            };
    
            return StatusCode(problemDetails.Status.Value, problemDetails);
        }
    }
    

    Der vorangehende Code ruft ControllerBase.Problem auf, um eine Antwort zu ProblemDetails erstellen.

Verwenden von Ausnahmen zum Ändern der Antwort

Der Inhalt der Antwort kann außerhalb des Controllers geändert werden. In der ASP.NET 4.x-Web-API war die einzige Möglichkeit hierfür die Verwendung des Typs HttpResponseException. ASP.NET Core enthält keinen entsprechenden Typ. Die Unterstützung für HttpResponseException kann mit folgenden Schritten hinzugefügt werden:

  1. Erstellen Sie einen bekannten Ausnahmetyp namens HttpResponseException:

    public class HttpResponseException : Exception
    {
        public int Status { get; set; } = 500;
    
        public object Value { get; set; }
    }
    
  2. Erstellen Sie einen Aktionsfilter namens HttpResponseExceptionFilter:

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

    Der obige Filter gibt eine des maximalen ganzzahligen Werts Order minus 10 an. Dadurch können andere Filter am Ende der Pipeline ausgeführt werden.

  3. Fügen Sie den Aktionsfilter in Startup.ConfigureServices der Filtersammlung hinzu:

    services.AddControllers(options =>
        options.Filters.Add(new HttpResponseExceptionFilter()));
    
    services.AddMvc(options =>
            options.Filters.Add(new HttpResponseExceptionFilter()))
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    
    services.AddMvc(options =>
            options.Filters.Add(new HttpResponseExceptionFilter()))
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    

Antwort auf Überprüfungsfehler

Bei Web-API-Controllern antwortet MVC mit dem Antworttyp ValidationProblemDetails, wenn bei der Modellüberprüfung ein Fehler auftritt. MVC verwendet die Ergebnisse von InvalidModelStateResponseFactory, um die Fehlerantwort auf einen Überprüfungsfehler zu erstellen. Das folgende Beispiel verwendet die Factory, um den Standardantworttyp von SerializableError in Startup.ConfigureServices zu ändern:

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

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

            return result;
        };
    });
services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
    .ConfigureApiBehaviorOptions(options =>
    {
        options.InvalidModelStateResponseFactory = context =>
        {
            var result = new BadRequestObjectResult(context.ModelState);

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

            return result;
        };
    });
services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var result = new BadRequestObjectResult(context.ModelState);

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

        return result;
    };
});

Fehlerantwort des Clients

Ein Fehlerergebnis ist als Ergebnis mit dem HTTP-Statuscode 400 oder höher definiert. Bei Web-API-Controllern transformiert MVC ein Fehlerergebnis in ein Ergebnis mit ProblemDetails.

Wichtig

ASP.NET Core 2.1 generiert eine Antwort mit Details zum Problem, die nahezu mit RFC 7807 konform ist. Wenn eine 100-prozentige Konformität wichtig ist, führen Sie ein Upgrade des Projekts auf ASP.NET Core 2.2 oder höher durch.

Die Fehlerantwort kann auf eine der folgenden Arten konfiguriert werden:

  1. Implementieren der ProblemDetailsFactory
  2. Verwenden von ApiBehaviorOptions.ClientErrorMapping

Implementieren Sie ProblemDetailsFactory.

MVC verwendet die Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory, um alle Instanzen von ProblemDetails und ValidationProblemDetails zu generieren. Hierzu gehören Clientfehlerantworten, Antworten auf Überprüfungsfehler sowie die Hilfsmethoden ControllerBase.Problem und ControllerBase.ValidationProblem.

Um die Antwort mit Problemdetails anzupassen, registrieren Sie eine benutzerdefinierte Implementierung von ProblemDetailsFactory in Startup.ConfigureServices:

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

Die Fehlerantwort kann so konfiguriert werden wie im Abschnitt Verwenden von ApiBehaviorOptions.ClientErrorMapping beschrieben.

Verwenden von ApiBehaviorOptions.ClientErrorMapping

Verwenden Sie die ClientErrorMapping-Eigenschaft zum Konfigurieren des Inhalts der ProblemDetails-Antwort. Der folgende Code in Startup.ConfigureServices aktualisiert beispielsweise die type-Eigenschaft für 404-Antworten:

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

Benutzerdefinierte Middleware zum Behandeln von Ausnahmen

Die Standardwerte in der Middleware zur Ausnahmebehandlung funktionieren für die meisten Apps gut. Für Apps, die eine spezielle Ausnahmebehandlung erfordern, sollten Sie die Middleware für die Ausnahmebehandlung anpassen.

Erstellen einer ProblemDetails-Nutzlast für Ausnahmen

ASP.NET Core erzeugt keine standardisierte Fehlernutzlast, wenn auf dem Server eine nicht behandelte Ausnahme auftritt. In Szenarien, in denen es wünschenswert ist, eine standardisierte ProblemDetails-Antwort an den Client zurück zu geben, kann die Middleware ProblemDetails verwendet werden, um Ausnahmen und 404s einer ProblemDetails-Nutzlast zu zuordnen. Die Middleware zur Ausnahmebehandlung kann auch verwendet werden, um eine Nutzlast ProblemDetails für nicht behandelte Ausnahmen zurück zu geben.