【译】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_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请求头设置为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 可以被用来产生一个错误负载。
- 在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(); }); }
- 配置控制器Action来响应/error路由。
[ApiController] public class ErrorController : ControllerBase { [Route("/error")] public IActionResult Error() => Problem(); }
上述Error Action向客户端发送一个兼容 RFC 7807的负载。
在本地的开发环境中,异常处理中间件也可以提供更加详细的内容协商输出。使用以下步骤来为开发环境和生产环境提供一致的负载格式。
- 在Startup.Configure中,注册环境特定的异常处理中间件实例。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseExceptionHandler("/error-local-development"); } else { app.UseExceptionHandler("/error"); } }
在上述代码中,中间件用如下方式来注册:
- 开发环境中的
/error-local-development
路由 - 非开发环境中的/error路由
- 开发环境中的
- 向控制器的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
的支持可使用如下的步骤添加:
- 创建一个众所周知的异常类型,名为HttpResponseException。
public class HttpResponseException : Exception { public int Status { get; set; } = 500; public object Value { get; set; } }
- 创建一个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被从最大整形值中减去。减去这个值可以允许其他过滤器运行在管道的末尾。
- 在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的结果。
错误结果可以通过如下方式之一进行配置:
实现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"; });