public interface IDeveloperPageExceptionFilter { Task HandleExceptionAsync( ErrorContext errorContext, Func<ErrorContext, Task> next); } public class ErrorContext { public HttpContext HttpContext { get; } public Exception Exception { get; } public ErrorContext( HttpContext httpContext, Exception exception) ; }HandleExceptionAsync方法定义了errorContext和next两个参数,前者提供的ErrorContext对象是对HttpContext上下文的封装,并利用Exception属性提供待处理的异常;后者提供的Func<ErrorContext, Task>委托代表后续的异常处理任务。如果某个IDeveloperPageExceptionFilter对象没有将异常处理任务向后分发,开发者处理页面将不会呈现出来。如下的演示实例通过实现IDeveloperPageExceptionFilter接口定义了一个FakeExceptionFilter类型,并将其注册为依赖服务。
using Microsoft.AspNetCore.Diagnostics; var builder = WebApplication.CreateBuilder(); builder.Services .AddSingleton<IDeveloperPageExceptionFilter, FakeExceptionFilter>(); var app = builder.Build(); app.UseDeveloperExceptionPage(); app.MapGet("/", void () => throw new InvalidOperationException( "Manually thrown exception...")); app.Run(); // 堆代码 duidaima.com public class FakeExceptionFilter : IDeveloperPageExceptionFilter { public Task HandleExceptionAsync( ErrorContext errorContext, Func<ErrorContext, Task> next) => errorContext.HttpContext.Response.WriteAsync( "Unhandled exception occurred!"); }在FakeExceptionFilter类型实现的HandleExceptionAsync方法仅在响应的主体内容中写入了一条简单的错误消息(“Unhandled exception occurred!”),所以DeveloperExceptionPageMiddleware中间件默认提供的错误页面并不会呈现出来,取而代之的就是图1所示的由注册FakeExceptionFilter定制的错误页面。
针对编译异常的处理(默认)
我们编写的ASP.NET应用会编译成程序集进行部署,为什么运行过程中还会出现“编译异常”呢?这是因为处理这种“预编译”模式,ASP.NET还支持运行时动态编译。以MVC应用为例,我们可以在运行时修改它的视图文件,这样的修改就会触发动态编译。如果修改的内容没法通过编译,就会抛出编译异常。DeveloperExceptionPageMiddleware中间件在处理编译异常的时候会在错误页面中呈现不同的内容。<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net6</TargetFramework> <PreserveCompilationReferences>true</PreserveCompilationReferences> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.0" /> </ItemGroup> </Project>如下所示演示程序注册了DeveloperExceptionPageMiddleware中间件。为了支持针对Razor视图文件的运行时编译,在调用AddControllersWithViews扩展方法得到返回的IMvcBuilder对象之后,我们进一步调用该对象的AddRazorRuntimeCompilation扩展方法。
var builder = WebApplication.CreateBuilder(); builder.Services.AddControllersWithViews() .AddRazorRuntimeCompilation(); var app = builder.Build(); app.UseDeveloperExceptionPage(); app.MapControllers(); app.Run();我们定义了如下所示的HomeController,它的Action方法Index会直接调用View方法将默认的视图呈现出来。根据约定,Action方法Index呈现出来的视图文件对应的路径应该是“~/views/home/index.cshtml”,我们先不提供这个视图文件的内容。
public class HomeController : Controller { [HttpGet("/")] public IActionResult Index() => View(); }我们这个MVC应用启动再将视图文件的内容定义成如下的形式,为了让动态编译失败,这里指定的Foobar类型其实根本不存在。
@{ var value = new Foobar(); }当我们利用浏览器请求根路径时,获得到如图2所示的错误页面。这个错误页面显示的内容和结构与前面演示的实例是完全不一样的,在这里我们不仅可以得到导致编译失败的视图文件的路径“Views/Home/Index.cshtml”,还可以看到导致编译失败的代码。这个错误页面还直接将参与编译的源代码呈现出来。
public interface ICompilationException { IEnumerable<CompilationFailure> CompilationFailures { get; } } public class CompilationFailure { public string SourceFileContent { get; } public string SourceFilePath { get; } public string CompiledContent { get; } public IEnumerable<DiagnosticMessage> Messages { get; } ... }CompilationFailure类型的Messages属性返回一个元素类型为DiagnosticMessage的集合,DiagnosticMessage对象承载着一些描述编译错误的诊断信息。我们不仅可以借助该对象的相关属性得到描述编译错误的消息(Message和FormattedMessage属性),还可以得到发生编译错误所在源文件的路径(SourceFilePath)及范围,StartLine属性和StartColumn属性分别表示导致编译错误的源代码在源文件中开始的行与列。EndLine属性和EndColumn属性分别表示导致编译错误的源代码在源文件中结束的行与列(行数和列数分别从1与0开始计数)。
public class DiagnosticMessage { public string SourceFilePath { get; } public int StartLine { get; } public int StartColumn { get; } public int EndLine { get; } public int EndColumn { get; } public string Message { get; } public string FormattedMessage { get; } ... }从图2可以看出,错误页面会直接将导致编译失败的相关源代码显示出来。令我们更感到惊喜的是,它不仅将直接导致失败的源代码实现出来,还显示前后相邻的源代码。至于相邻源代码应该显示多少行,实际上是通过配置选项DeveloperExceptionPageOptions的SourceCodeLineCount属性控制的,而源文件的读取则是由该配置选项的FileProvider属性提供的IFileProvider对象完成的。
var builder = WebApplication.CreateBuilder(); builder.Services.AddControllersWithViews() .AddRazorRuntimeCompilation(); var app = builder.Build(); app.UseDeveloperExceptionPage( new DeveloperExceptionPageOptions { SourceCodeLineCount = 3}); app.MapControllers(); app.Run();对于前面演示的这个实例来说,如果将前后相邻的三行代码显示在错误页面上,我们可以采用如上所示的方式为DeveloperExceptionPageMiddleware中间件指定DeveloperExceptionPageOptions配置选项,并将它的SourceCodeLineCount属性设置为3。我们可以将视图文件(index.cshtml)改写成如下所示的形式,在导致编译失败的那一行代码前后分别添加4行代码。
1: 2: 3: 4: 5:@{ var value = new Foobar();} 6: 7: 8: 9:对于定义在视图文件中的9行代码,根据在注册DeveloperExceptionPageMiddleware中间件时指定的规则,最终显示在错误页面上的应该是第2行至第8行。如果利用浏览器访问相同的地址,这7行代码会以图3所示的形式出现在错误页面上。如果我们没有对SourceCodeLineCount属性做显式设置,它的默认值为6。
public class ExceptionHandlerMiddleware { … public async Task Invoke(HttpContext context) { try { await _next(context); } catch (Exception ex) { var edi = ExceptionDispatchInfo.Capture(ex); var originalPath = context.Request.Path; try { var feature = new ExceptionHandlerFeature() { Error = ex, Path = originalPath, Endpoint = context.GetEndpoint(), RouteValues = context.Features .Get<IRouteValuesFeature>() ?.RouteValues }; context.Features .Set<IExceptionHandlerFeature>(feature); context.Features .Set<IExceptionHandlerPathFeature>(feature); context.Response.StatusCode = 500; context.Response.Clear(); if (_options.ExceptionHandlingPath.HasValue) { context.Request.Path = _options.ExceptionHandlingPath; } var handler = _options.ExceptionHandler ?? _next; await handler(context); if (context.Response.StatusCode == 404 && !_options.AllowStatusCode404Response) { throw edi.SourceException; } } finally { context.Request.Path = originalPath; } } } }在进行异常处理时,我们可以从当前HttpContext上下文中提取ExceptionHandlerFeature特性对象,进而获取抛出的异常和原始请求路径。如下面的代码片段所示,我们利用HandleError方法来呈现一个定制的错误页面。在这个方法中,我们正是借助ExceptionHandlerFeature特性得到抛出的异常的,并将其类型、消息及堆栈追踪信息显示出来。
using Microsoft.AspNetCore.Diagnostics; var app = WebApplication.Create(); app.UseExceptionHandler("/error"); app.MapGet("/error", HandleError); app.MapGet("/", void () => throw new InvalidOperationException( "Manually thrown exception")); app.Run(); static IResult HandleError(HttpContext context) { var ex = context.Features.Get<IExceptionHandlerPathFeature>()!.Error; var html = $@" <html> <head><title>Error</title></head> <body> <h3>{ex.Message}</h3> <p>Type: {ex.GetType().FullName}</p> <p>StackTrace: {ex.StackTrace}</p> </body> </html>"; return Results.Content(html, "text/html"); }上面演示程序为路径 “/error”注册了一个采用HandleError作为处理方法的终结点。注册的ExceptionHandlerMiddleware中间件将该“/error”作为重定向路径。那么针对根路径的请求将会得到图4所示的错误页面。
using Microsoft.Net.Http.Headers; var _random = new Random(); var app = WebApplication.Create(); app.UseExceptionHandler(app2 => app2.Run( httpContext => httpContext.Response .WriteAsync("Error occurred!"))); app.MapGet("/", (HttpResponse response) => { response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue { MaxAge = TimeSpan.FromHours(1) }; if (_random.Next() % 2 == 0) { throw new InvalidOperationException( "Manually thrown exception..."); } return response.WriteAsync("Succeed..."); }); app.Run();如下所示的两个响应报文分别对应正常响应和抛出异常的情况,我们会发现程序中设置的缓存报头Cache-Control: max-age=3600只会出现在状态码为“200 OK”的响应中。在状态码为“500 Internal Server Error”的响应中,则会出现三个与缓存相关的报头(Cache-Control、Pragma和Expires),它们的目的都是禁止缓存或者将缓存标识为过期。
HTTP/1.1 200 OK Date: Mon, 08 Nov 2021 12:47:55 GMT Server: Kestrel Cache-Control: max-age=3600 Content-Length: 10 Succeed... HTTP/1.1 500 Internal Server Error Date: Mon, 08 Nov 2021 12:48:00 GMT Server: Kestrel Cache-Control: no-cache,no-store Expires: -1 Pragma: no-cache Content-Length: 15 Error occurred!针对404响应的处理
var app = WebApplication.Create(); app.MapGet("/foo", BuildHandler(app, false)); app.MapGet("/bar", BuildHandler(app, true)); app.Run(); static RequestDelegate BuildHandler( IEndpointRouteBuilder endpoints, bool allowStatusCode404Response) { var options = new ExceptionHandlerOptions { ExceptionHandler = httpContext => { httpContext.Response.StatusCode = 404; return Task.CompletedTask; }, AllowStatusCode404Response = allowStatusCode404Response }; var app = endpoints.CreateApplicationBuilder(); app .UseExceptionHandler(options) .Run(httpContext => Task.FromException( new InvalidOperationException( "Manually thrown exception."))); return app.Build(); }该演示程序启动之后,针对两个路由分支的路径的请求会得到不同的输出结果。如图5所示,针对路径“/foo”的请求返回依然是状态码为500的响应,异常处理器返回的404响应在针对路径“/bar”的请求中被正常返回了。图片
利用IStatusCodePagesFeature特性忽略异常处理
如果某些内容已经被写入响应的主体部分,或者响应的媒体类型已经被预先设置,StatusCodePagesMiddleware中间件就不会再执行任何错误处理操作。但是应用程序往往具有自身的异常处理策略,也许在某些情况下就应该回复一个状态码在400~599区间内的响应,该中间件就不应该对当前响应做任何干预的。为了解决这种情况,我们必须赋予后续中间件能够阻止StatusCodePagesMiddleware中间件进行错误处理的能力。这项能力是借助IStatusCodePagesFeature特性来实现的。如下面的代码片段所示,该接口定义了唯一的Enabled属性表示是否希望StatusCodePagesMiddleware中间件参与当前的异常处理。StatusCodePagesFeature类型是对该接口的默认实现,它的Enabled属性默认返回True。public interface IStatusCodePagesFeature { bool Enabled { get; set; } } public class StatusCodePagesFeature : IStatusCodePagesFeature { public bool Enabled { get; set; } = true ; }如下面的代码片段所示,StatusCodePagesMiddleware中间件在将请求交付给后续管道处理之前,它会创建一个StatusCodePagesFeature特性并附着到当前HttpContext上下文上。后面的中间件如果希望StatusCodePagesMiddleware中间件能够“放行”,只需要将此特性的Enabled属性设置为False就可以了。
public class StatusCodePagesMiddleware { ... public async Task Invoke(HttpContext context) { var feature = new StatusCodePagesFeature(); context.Features.Set<IStatusCodePagesFeature>(feature); await _next(context); var response = context.Response; if ((response.StatusCode >= 400 && response.StatusCode <= 599) &&!response.ContentLength.HasValue && string.IsNullOrEmpty(response.ContentType) && feature.Enabled) { await _options.HandleAsync( new StatusCodeContext(context, _options, _next)); } } }下面的演示程序将针对根路径“/”请求的处理实现在Process方法中,该方法会将响应状态码为“401 Unauthorized”。我们通过随机数让这个方法在50%的概率下将StatusCodePagesFeature特性的Enabled属性设置为False。注册的StatusCodePagesMiddleware中间件会直接将“Error occurred!”文本作为响应内容。
using Microsoft.AspNetCore.Diagnostics; var random = new Random(); var app = WebApplication.Create(); app.UseStatusCodePages(HandleAsync); app.MapGet("/", Process); app.Run(); static Task HandleAsync(StatusCodeContext context) => context.HttpContext.Response .WriteAsync("Error occurred!"); void Process(HttpContext context) { context.Response.StatusCode = 401; if (random.Next() % 2 == 0) { context.Features.Get<IStatusCodePagesFeature>()! .Enabled = false; } }针对根路径的请求会得到如下两种不同的响应。没有主体内容的响应是通过Process方法产生的,这种情况发生在StatusCodePagesMiddleware中间件通过StatusCodePagesFeature特性被屏蔽的时候。有主体内容的响应则是Process方法和StatusCodePagesMiddleware中间件共同作用的结果。
HTTP/1.1 401 Unauthorized Date: Sat, 11 Sep 2021 03:07:20 GMT Server: Kestrel Content-Length: 15 Error occurred! HTTP/1.1 401 Unauthorized Date: Sat, 11 Sep 2021 03:07:34 GMT Server: Kestrel Content-Length: 0