处理 ASP.NET Core Web API 中的错误

本文介绍如何处理和自定义 ASP.NET Core Web API 的错误处理。

查看或下载示例代码如何下载

开发人员异常页

开发人员异常页是一种用于获取服务器错误详细堆栈跟踪的有用工具。 它使用 DeveloperExceptionPageMiddleware 来捕获 HTTP 管道中的同步和异步异常,并生成错误响应。 为了进行说明,请考虑以下控制器操作:

[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 命令以测试前面的操作:

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

在 ASP.NET Core 3.0 及更高版本中,如果客户端不请求 HTML 格式的输出,则开发人员异常页将显示纯文本响应。 将显示以下输出:

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 媒体类型。 例如:

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

请考虑以下 HTTP 响应摘录:

在 ASP.NET Core 2.2 及更低版本中,开发人员异常页将显示 HTML 格式的响应。 例如,请考虑以下 HTTP 响应摘录:

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

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

通过 Postman 等工具进行测试时,HTML 格式的响应会很有用。 以下屏幕截图显示了 Postman 中的纯文本和 HTML 格式的响应:

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

警告

仅当应用程序在开发环境中运行时才启用开发人员异常页 。 否则当应用程序在生产环境中运行时,详细的异常信息会向公众泄露 有关配置环境的详细信息,请参阅 在 ASP.NET Core 中使用多个环境

异常处理程序

在非开发环境中,可使用异常处理中间件来生成错误负载:

  1. Startup.Configure 中,调用 UseExceptionHandler 以使用中间件:

    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 路由:

    [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 操作向客户端发送符合 RFC 7807 的负载。

异常处理中间件还可以在本地开发环境中提供更详细的内容协商输出。 使用以下步骤在开发和生产环境中生成一致的负载格式:

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

    在上述代码中,通过以下方法注册了中间件:

    • 开发环境中 /error-local-development 的路由。
    • 非开发环境中 /error 的路由。
  2. 将属性路由应用于控制器操作:

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

使用异常来修改响应

可以从控制器之外修改响应的内容。 在 ASP.NET 4.x Web API 中,执行此操作的一种方法是使用 HttpResponseException 类型。 ASP.NET Core 不包括等效类型。 可以使用以下步骤来添加对 HttpResponseException 的支持:

  1. 创建名为 HttpResponseException 的已知异常类型:

    public class HttpResponseException : Exception
    {
        public int Status { get; set; } = 500;
    
        public object Value { get; set; }
    }
    
  2. 创建名为 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 中,将操作筛选器添加到筛选器集合:

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

验证失败错误响应

对于 Web API 控制器,如果模型验证失败,MVC 将使用 ValidationProblemDetails 响应类型做出响应。 MVC 使用 InvalidModelStateResponseFactory 的结果来构造验证失败的错误响应。 下面的示例使用工厂在 Startup.ConfigureServices 中将默认响应类型更改为 SerializableError

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

客户端错误响应

错误结果 定义为具有 HTTP 状态代码 400 或更高的结果。 对于 Web API 控制器,MVC 会将错误结果转换为具有 ProblemDetails 的结果。

重要

ASP.NET Core 2.1 生成一个基本符合 RFC 7807 的问题详细信息响应。 如果需要达到百分百的符合性,请将项目升级到 ASP.NET Core 2.2 或更高版本。

可以通过以下方式之一配置错误响应:

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

实现 ProblemDetailsFactory

MVC 使用 Microsoft.AspNetCore.Mvc.ProblemDetailsFactory 生成 ProblemDetailsValidationProblemDetails 的所有实例。 这包括客户端错误响应、验证失败错误响应以及 Microsoft.AspNetCore.Mvc.ControllerBase.ProblemValidationProblem() 帮助程序方法。

若要自定义问题详细信息响应,请在 Startup.ConfigureServices 中注册 ProblemDetailsFactory 的自定义实现:

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

可以按照使用 ApiBehaviorOptions.ClientErrorMapping 部分所述的方式配置错误响应。

使用 ApiBehaviorOptions.ClientErrorMapping

使用 ClientErrorMapping 属性配置 ProblemDetails 响应的内容。 例如,Startup.ConfigureServices 中的以下代码会更新 404 响应的 type 属性:

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

上一篇:使用 Web API 约定

下一篇:使用 HTTP REPL 测试 Web API

关注微信小程序
程序员编程王-随时随地学编程

扫描二维码
程序员编程王

扫一扫关注最新编程教程