【译】ASP.NET Core Web API中的异常处理

原文链接:传送门

这篇文章描述了在ASP.NET Core Web API中如何处理并自定义异常处理。

开发者异常页

开发者异常页是一个获得服务器错误详细跟踪栈的很有用的工具。它会使用DeveloperExceptionPageMiddleware 来捕获来自于HTTP管道的同步及异步异常并生成错误响应。为了演示,请考虑如下的控制器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 命令来测试上述代码:

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

在ASP.NET Core 3.0及以后的版本中,如果客户端不请求基于HTTP格式的响应,那么开发者异常页便会显示纯文本的响应。如下输出会显示出来:

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_folderaspnetAspNetCore.Docsaspnetcoreweb-apihandle-errorssamples3.xControllersWeatherForecastController.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请求头设置为text/html媒体类型,比如:

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

考虑如下来自于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格式的响应;

 【不支持动图,请跳转至原文观看】

警告:仅当app运行于开发环境时,启用开发者异常页。当app运行于生产环境时,你不希望将详细的异常信息公开分享出来。关于配置环境的更多信息,请参考 Use multiple environments in ASP.NET Core

异常处理

在非开发环境中,Exception Handling Middleware  可以被用来产生一个错误负载。

  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();
        });
    }
  2. 配置控制器Action来响应/error路由。
    [ApiController]
    public class ErrorController : ControllerBase
    {
        [Route("/error")]
        public IActionResult Error() => Problem();
    }

上述Error Action向客户端发送一个兼容 RFC 7807的负载。

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

  1. 在Startup.Configure中,注册环境特定的异常处理中间件实例。
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseExceptionHandler("/error-local-development");
        }
        else
        {
            app.UseExceptionHandler("/error");
        }
    }

    在上述代码中,中间件用如下方式来注册:

    1. 开发环境中的/error-local-development 路由
    2. 非开发环境中的/error路由
  2. 向控制器的Action应用属性路由。
    [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();
    } 

使用异常来更改响应

响应的内容可以从控制器的外面被改变。在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. 创建一个Action过滤器,名为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;
            }
        }
    }

    在上述的过滤器中,魔法数字10被从最大整形值中减去。减去这个值可以允许其他过滤器运行在管道的末尾。

  3. 在Startup.ConfigureServices中,将Action过滤器添加到过滤器集合中。
    services.AddControllers(options =>
        options.Filters.Add(new HttpResponseExceptionFilter()));

验证失败错误响应

对于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 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的结果。

错误结果可以通过如下方式之一进行配置:

  1. Implement ProblemDetailsFactory
  2. Use ApiBehaviorOptions.ClientErrorMapping

实现ProblemDetailsFactory

MVC使用 Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory 来产生 ProblemDetails 和 ValidationProblemDetails 的所有的实例。这包含客户端错误响应,验证失败错误响应,以及 ControllerBase.Problem 和 ControllerBase.ValidationProblem 帮助器方法。

为了自定义问题详细响应,在Startup.ConfigureServices:中注册一个ProblemDetailsFactory 类的自定义实现。

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

使用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[StatusCodes.Status404NotFound].Link =
            "https://httpstatuses.com/404";
    });
原文地址:https://www.cnblogs.com/qianxingmu/p/14023023.html