处理 ASP.NET Core Web API 中的错误Handle errors in ASP.NET Core web APIs

本文介绍如何处理和自定义 ASP.NET Core Web API 的错误处理。This article describes how to handle and customize error handling with ASP.NET Core web APIs.

查看或下载示例代码如何下载View or download sample code (How to download)

开发人员异常页Developer Exception Page

开发人员异常页是一种用于获取服务器错误详细堆栈跟踪的有用工具。The Developer Exception Page is a useful tool to get detailed stack traces for server errors. 它使用 DeveloperExceptionPageMiddleware 来捕获 HTTP 管道中的同步和异步异常,并生成错误响应。It uses DeveloperExceptionPageMiddleware to capture synchronous and asynchronous exceptions from the HTTP pipeline and to generate error responses. 为了进行说明,请考虑以下控制器操作:To illustrate, consider the following controller action:

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

运行以下 curl 命令以测试前面的操作:Run the following curl command to test the preceding action:

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

在 ASP.NET Core 3.0 及更高版本中,如果客户端不请求 HTML 格式的输出,则开发人员异常页将显示纯文本响应。In ASP.NET Core 3.0 and later, the Developer Exception Page displays a plain-text response if the client doesn't request HTML-formatted output. 将显示以下输出:The following output appears:

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

要改为显示 HTML 格式的响应,请将 Accept HTTP 请求头设置为 text/html 媒体类型。To display an HTML-formatted response instead, set the Accept HTTP request header to the text/html media type. 例如:For example:

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

请考虑以下 HTTP 响应摘录:Consider the following excerpt from the HTTP response:

在 ASP.NET Core 2.2 及更低版本中,开发人员异常页将显示 HTML 格式的响应。In ASP.NET Core 2.2 and earlier, the Developer Exception Page displays an HTML-formatted response. 例如,请考虑以下 HTTP 响应摘录:For example, consider the following excerpt from the HTTP response:

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

通过 Postman 等工具进行测试时,HTML 格式的响应会很有用。The HTML-formatted response becomes useful when testing via tools like Postman. 以下屏幕截图显示了 Postman 中的纯文本和 HTML 格式的响应:The following screen capture shows both the plain-text and the HTML-formatted responses in Postman:

Postman 中的开发人员异常页测试

警告

仅当应用程序在开发环境中运行时才启用开发人员异常页 。Enable the Developer Exception Page only when the app is running in the Development environment. 否则当应用程序在生产环境中运行时,详细的异常信息会向公众泄露You don't want to share detailed exception information publicly when the app runs in production. 有关配置环境的详细信息,请参阅 在 ASP.NET Core 中使用多个环境For more information on configuring environments, see 在 ASP.NET Core 中使用多个环境.

异常处理程序Exception handler

在非开发环境中,可使用异常处理中间件来生成错误负载:In non-development environments, Exception Handling Middleware can be used to produce an error payload:

  1. Startup.Configure 中,调用 UseExceptionHandler 以使用中间件:In Startup.Configure, invoke UseExceptionHandler to use the middleware:

    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. 配置控制器操作以响应 /error 路由:Configure a controller action to respond to the /error route:

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

前面的 Error 操作向客户端发送与 RFC7807 兼容的负载。The preceding Error action sends an RFC7807-compliant payload to the client.

异常处理中间件还可以在本地开发环境中提供更详细的内容协商输出。Exception Handling Middleware can also provide more detailed content-negotiated output in the local development environment. 使用以下步骤在开发和生产环境中生成一致的负载格式:Use the following steps to produce a consistent payload format across development and production environments:

  1. Startup.Configure 中,注册特定于环境的异常处理中间件实例:In Startup.Configure, register environment-specific Exception Handling Middleware instances:

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

    在上述代码中,通过以下方法注册了中间件:In the preceding code, the middleware is registered with:

    • 开发环境中 /error-local-development 的路由。A route of /error-local-development in the Development environment.
    • 非开发环境中 /error 的路由。A route of /error in environments that aren't Development.
  2. 将属性路由应用于控制器操作:Apply attribute routing to controller actions:

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

使用异常来修改响应Use exceptions to modify the response

可以从控制器之外修改响应的内容。The contents of the response can be modified from outside of the controller. 在 ASP.NET 4.x Web API 中,执行此操作的一种方法是使用 HttpResponseException 类型。In ASP.NET 4.x Web API, one way to do this was using the HttpResponseException type. ASP.NET Core 不包括等效类型。ASP.NET Core doesn't include an equivalent type. 可以使用以下步骤来添加对 HttpResponseException 的支持:Support for HttpResponseException can be added with the following steps:

  1. 创建名为 HttpResponseException 的已知异常类型:Create a well-known exception type named HttpResponseException:

    public class HttpResponseException : Exception
    {
        public int Status { get; set; } = 500;
    
        public object Value { get; set; }
    }
    
  2. 创建名为 HttpResponseExceptionFilter 的操作筛选器:Create an action filter named HttpResponseExceptionFilter:

    public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
    {
        public int Order { get; set; } = 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;
            }
        }
    }
    
  3. Startup.ConfigureServices 中,将操作筛选器添加到筛选器集合:In Startup.ConfigureServices, add the action filter to the filters collection:

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

验证失败错误响应Validation failure error response

对于 Web API 控制器,如果模型验证失败,MVC 将使用 ValidationProblemDetails 响应类型做出响应。For web API controllers, MVC responds with a ValidationProblemDetails response type when model validation fails. MVC 使用 InvalidModelStateResponseFactory 的结果来构造验证失败的错误响应。MVC uses the results of InvalidModelStateResponseFactory to construct the error response for a validation failure. 下面的示例使用工厂在 Startup.ConfigureServices 中将默认响应类型更改为 SerializableErrorThe following example uses the factory to change the default response type to SerializableError in Startup.ConfigureServices:

services.AddControllers()
    .ConfigureApiBehaviorOptions(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;
        };
    });
services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
    .ConfigureApiBehaviorOptions(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;
        };
    });
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;
    };
});

客户端错误响应Client error response

错误结果 定义为具有 HTTP 状态代码 400 或更高的结果。An error result is defined as a result with an HTTP status code of 400 or higher. 对于 Web API 控制器,MVC 会将错误结果转换为具有 ProblemDetails 的结果。For web API controllers, MVC transforms an error result to a result with ProblemDetails.

可以通过以下方式之一配置错误响应:The error response can be configured in one of the following ways:

  1. 实现 ProblemDetailsFactoryImplement ProblemDetailsFactory
  2. 使用 ApiBehaviorOptions.ClientErrorMappingUse ApiBehaviorOptions.ClientErrorMapping

实现 ProblemDetailsFactoryImplement ProblemDetailsFactory

MVC 使用 Microsoft.AspNetCore.Mvc.ProblemDetailsFactory 生成 ProblemDetailsValidationProblemDetails 的所有实例。MVC uses Microsoft.AspNetCore.Mvc.ProblemDetailsFactory to produce all instances of ProblemDetails and ValidationProblemDetails. 这包括客户端错误响应、验证失败错误响应以及 Microsoft.AspNetCore.Mvc.ControllerBase.ProblemValidationProblem() 帮助程序方法。This includes client error responses, validation failure error responses, and the Microsoft.AspNetCore.Mvc.ControllerBase.Problem and ValidationProblem() helper methods.

若要自定义问题详细信息响应,请在 Startup.ConfigureServices 中注册 ProblemDetailsFactory 的自定义实现:To customize the problem details response, register a custom implementation of ProblemDetailsFactory in Startup.ConfigureServices:

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

可以按照使用 ApiBehaviorOptions.ClientErrorMapping 部分所述的方式配置错误响应。The error response can be configured as outlined in the Use ApiBehaviorOptions.ClientErrorMapping section.

使用 ApiBehaviorOptions.ClientErrorMappingUse ApiBehaviorOptions.ClientErrorMapping

使用 ClientErrorMapping 属性配置 ProblemDetails 响应的内容。Use the ClientErrorMapping property to configure the contents of the ProblemDetails response. 例如,Startup.ConfigureServices 中的以下代码会更新 404 响应的 type 属性:For example, the following code in Startup.ConfigureServices updates the type property for 404 responses:

services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = true;
        options.SuppressInferBindingSourcesForParameters = true;
        options.SuppressModelStateInvalidFilter = true;
        options.SuppressMapClientErrors = true;
        options.ClientErrorMapping[404].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";
    });