ASP.NET Core应用的错误处理[3]:ExceptionHandlerMiddleware中间件如何呈现“定制化错误页面”

DeveloperExceptionPageMiddleware中间件利用呈现出来的错误页面实现抛出异常和当前请求的详细信息以辅助开发人员更好地进行纠错诊断工作,而ExceptionHandlerMiddleware中间件则是面向最终用户的,我们可以利用它来显示一个友好的定制化的错误页面。按照惯例,我们还是先来看看ExceptionHandlerMiddleware的类型定义。 [本文已经同步到《ASP.NET Core框架揭秘》之中]

   1: public class ExceptionHandlerMiddleware
   2: {    
   3:     public ExceptionHandlerMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions<ExceptionHandlerOptions> options, DiagnosticSource diagnosticSource);  
   4:     public Task Invoke(HttpContext context);
   5: }
   6:  
   7: public class ExceptionHandlerOptions
   8: {
   9:     public RequestDelegate     ExceptionHandler { get; set; }
  10:     public PathString          ExceptionHandlingPath { get; set; }
  11: }

与DeveloperExceptionPageMiddleware类似,我们在创建一个ExceptionHandlerMiddleware对象的时候同样需要提供一个携带配置选项的对象,从上面的代码可以看出这是一个ExceptionHandlerOptions。具体来说,一个ExceptionHandlerOptions对象通过其ExceptionHandler属性提供了一个最终用来处理请求的RequestDelegate对象。如果希望发生异常后自动重定向到某个指定的路径,我们可以利用ExceptionHandlerOptions对象的ExceptionHandlingPath属性来指定这个路径。我们一般会调用ApplicationBuilder的扩展方法UseExceptionHandler来注册ExceptionHandlerMiddleware中间件,这些重载的UseExceptionHandler方法会采用如下的方式完整中间件的注册工作。

   1: public static class ExceptionHandlerExtensions
   2: {
   3:     public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app)=> app.UseMiddleware<ExceptionHandlerMiddleware>();
   4:  
   5:     public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, ExceptionHandlerOptions options) 
   6:        => app.UseMiddleware<ExceptionHandlerMiddleware>(Options.Create(options));
   7:  
   8:     public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, string errorHandlingPath)
   9:     { 
  10:         return app.UseExceptionHandler(new ExceptionHandlerOptions
  11:         {
  12:             ExceptionHandlingPath = new PathString(errorHandlingPath)
  13:         });
  14:     }
  15:  
  16:     public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, Action<IApplicationBuilder> configure)
  17:     {
  18:         IApplicationBuilder newBuilder = app.New();
  19:         configure(newBuilder);
  20:  
  21:         return app.UseExceptionHandler(new ExceptionHandlerOptions
  22:         {
  23:             ExceptionHandler = newBuilder.Build()
  24:         });
  25:     }     
  26: }

一、异常处理器

ExceptionHandlerMiddleware中间件处理请求的本质就是在后续请求处理过程中出现异常的情况下采用注册的异常处理器来处理并响应请求,这个异常处理器就是我们再熟悉不过的RequestDelegate对象。该中间件采用的请求处理逻辑大体上可以通过如下所示的这段代码来体现。

   1: public class ExceptionHandlerMiddleware
   2: {
   3:     private RequestDelegate             _next;
   4:     private ExceptionHandlerOptions     _options;
   5:  
   6:     public ExceptionHandlerMiddleware(RequestDelegate next, IOptions<ExceptionHandlerOptions> options,…)
   7:     {
   8:         _next         = next;
   9:         _options      = options.Value;
  10:         …
  11:     }
  12:  
  13:     public async Task Invoke(HttpContext context)
  14:     {
  15:         try
  16:         {
  17:             await _next(context);
  18:         }
  19:         catch 
  20:         {
  21:             context.Response.StatusCode = 500;
  22:             context.Response.Clear();
  23:             if (_options.ExceptionHandlingPath.HasValue)
  24:             {
  25:                 context.Request.Path = _options.ExceptionHandlingPath;
  26:             }
  27:             RequestDelegate handler = _options.ExceptionHandler ?? _next;
  28:             await handler(context);
  29:         }
  30:     }
  31: }

如上面的代码片段所示,如果后续的请求处理过程中出现异常,ExceptionHandlerMiddleware中间件会利用一个作为异常处理器的RequestDelegate对象来完成最终的请求处理工作。如果在创建ExceptionHandlerMiddleware时提供的ExceptionHandlerOptions携带着这么一个RequestDelegate对象,那么它将作为最终使用的异常处理器,否则作为异常处理器的实际上就是后续的中间件。换句话说,如果我们没有通过ExceptionHandlerOptions显式指定一个异常处理器,ExceptionHandlerMiddleware中间件会在后续管道处理请求抛出异常的情况下将请求再次传递给后续管道。

当ExceptionHandlerMiddleware最终利用异常处理器来处理请求之前,它会对请求做一些前置处理工作,比如它会将响应状态码设置为500,比如清空当前所有响应内容等。如果我们利用ExceptionHandlerOptions的ExceptionHandlingPath属性设置了一个重定向路径,它会将该路径设置为当前请求的路径。除了这些,ExceptionHandlerMiddleware中间件实际上做了一些没有反应在上面这段代码片段中的工作。

二、异常的传递与请求路径的恢复

由于ExceptionHandlerMiddleware中间件总会利用一个作为异常处理器的RequestDelegate对象来完成最终的异常处理工作,为了让后者能够得到抛出的异常,该中间件应该采用某种方式将异常传递给它。除此之外,由于ExceptionHandlerMiddleware中间件会改变当前请求的路径,当整个请求处理完成之后,它必须将请求路径恢复成原始的状态,否则前置的中间件就无法获取到正确的请求路径。

请求处理过程中抛出的异常和原始请求路径的恢复是通过相应的特性完成的。具体来说,传递这两者的特性分别叫做ExceptionHandlerFeature和ExceptionHandlerPathFeature,对应的接口分别为IExceptionHandlerFeature和IExceptionHandlerPathFeature,如下面的代码片段所示,后者继承前者。默认使用的ExceptionHandlerFeature实现了这两个接口。

   1: public interface IExceptionHandlerFeature
   2: {
   3:     Exception Error { get; }
   4: }
   5:  
   6: public interface IExceptionHandlerPathFeature : IExceptionHandlerFeature
   7: {
   8:     string Path { get; }
   9: }
  10:  
  11: public class ExceptionHandlerFeature : IExceptionHandlerPathFeature, 
  12: {
  13:     public Exception  Error { get; set; }
  14:     public string     Path { get; set; }
  15: }

当ExceptionHandlerMiddleware中间件将代码当前请求的HttpContext传递给请求处理器之前,它会按照如下所示的方式根据抛出的异常的原始的请求路径创建一个ExceptionHandlerFeature对象,该对象最终被添加到HttpContext之上。当整个请求处理流程完全结束之后,ExceptionHandlerMiddleware中间件会借助这个特性得到原始的请求路径,并将其重新应用到当前请求上下文上。

   1: public class ExceptionHandlerMiddleware
   2: {
   3:     ...
   4:     public async Task Invoke(HttpContext context)
   5:     {
   6:         try
   7:         {
   8:             await _next(context);
   9:         }
  10:         catch(Exception ex)
  11:         {
  12:             context.Response.StatusCode = 500;
  13:  
  14:             var feature = new ExceptionHandlerFeature()
  15:             {
  16:                 Error = ex,
  17:                 Path = context.Request.Path,
  18:             };
  19:             
  20:             
  21:  
  22:             if (_options.ExceptionHandlingPath.HasValue)
  23:             {
  24:                 context.Request.Path = _options.ExceptionHandlingPath;
  25:             }
  26:             RequestDelegate handler = _options.ExceptionHandler ?? _next;
  27:  
  28:             try
  29:             {
  30:                 await handler(context);
  31:             }
  32:             finally
  33:             {
  34:                 
  35:             }
  36:         }
  37:     }
  38: }

在具体进行异常处理的时候,我们可以从当前HttpContext中提取这个ExceptionHandlerFeature对象,进而获取抛出的异常和原始的请求路径。如下面的代码所示,我们利用HandleError方法来呈现一个定制的错误页面。在这个方法中,我们正式借助于这个ExceptionHandlerFeature特性得到抛出的异常,并将它的类型、消息以及堆栈追踪显示出来。

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .ConfigureServices(svcs=>svcs.AddRouting())
   8:             .Configure(app => app
   9:                 .UseExceptionHandler("/error")
  10:                 .UseRouter(builder=>builder.MapRoute("error", HandleError))
  11:                 .Run(context=> Task.FromException(new InvalidOperationException("Manually thrown exception"))))
  12:             .Build()
  13:             .Run();
  14:     }
  15:  
  16:     private async static Task HandleError(HttpContext context)
  17:     {
  18:         context.Response.ContentType = "text/html";
  19:         Exception ex = context.Features.Get<IExceptionHandlerPathFeature>().Error;
  20:  
  21:         await context.Response.WriteAsync("<html><head><title>Error</title></head><body>");
  22:         await context.Response.WriteAsync($"<h3>{ex.Message}</h3>");
  23:         await context.Response.WriteAsync($"<p>Type: {ex.GetType().FullName}");
  24:         await context.Response.WriteAsync($"<p>StackTrace: {ex.StackTrace}");
  25:         await context.Response.WriteAsync("</body></html>");
  26:     }

在上面这个应用中,我们注册了一个模板为“error”的路由指向这个HandleError方法。对于通过调用扩展方法UseExceptionHandler注册的ExceptionHandlerMiddleware来说,我们将该路径设置为异常处理路径。那么对于任意从浏览器发出的请求,都会得到如下图所示的错误页面。

三、清除缓存

对于一个用于获取资源的GET请求来说,如果请求目标是一个相对稳定的资源,我们可以采用客户端缓存的方式避免相同资源的频繁获取和传输。对于作为资源提供者的Web应用来说,当它在处理请求的时候,除了将目标资源作为响应的主体内容之外,它还需要设置用于控制缓存的相关响应报头。由于缓存在大部分情况下只适用于成功的响应,如果服务端在处理请求过程中出现异常,之前设置的缓存报头是不应该出现在响应报文中。对于ExceptionHandlerMiddleware中间件来说,清楚缓存报头也是它负责的一项重要工作。

我们同样可以通过一个简单的实例来演示ExceptionHandlerMiddleware中间件针对缓存响应报头的清除。在如下这个应用中,我们将针对请求的处理实现在Invoke方法中,它有50%的可能会抛出异常。不论是返回正常的响应内容还是抛出异常,这个方法都会先设置一个“Cache-Control”的响应报头,并将缓存时间设置为1个小时(“Cache-Control:
max-age=3600”)。

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .ConfigureServices(svcs => svcs.AddRouting())
   8:             .Configure(app => app
   9:                 .UseExceptionHandler(builder => builder.Run(async context => await context.Response.WriteAsync("Error occurred!")))
  10:                 .Run(Invoke))
  11:             .Build()
  12:             .Run();
  13:     }
  14:  
  15:     private static Random _random = new Random();
  16:     private async  static Task Invoke(HttpContext context)
  17:     {
  18:         context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue
  19:         {
  20:             MaxAge = TimeSpan.FromHours(1)
  21:         };
  22:  
  23:         if (_random.Next() % 2 == 0)
  24:         {
  25:             throw new InvalidOperationException("Manually thrown exception...");
  26:         }
  27:         await context.Response.WriteAsync("Succeed...");
  28:     }
  29: }

通过调用扩展方法
UseExceptionHandler注册的ExceptionHandlerMiddleware中间件在处理异常时会响应一个内容为“Error
occurred!”的字符串。如下所示的两个响应报文分别对应于正常响应和抛出异常的情况,我们会发现程序中设置的缓存报头“Cache-Control:
max-age=3600”只会出现在状态码为“200 OK”的响应中。至于状态码为“500 Internal Server
Error”的响应中,则会出现三个与缓存相关的报头,它们的目的都会为了禁止缓存(或者指示缓存过期)。

   1: HTTP/1.1 200 OK
   2: Date: Sat, 17 Dec 2016 14:39:02 GMT
   3: Server: Kestrel
   4: 
   5: Content-Length: 10
   6:  
   7: Succeed...
   8:  
   9:  
  10: HTTP/1.1 500 Internal Server Error
  11: Date: Sat, 17 Dec 2016 14:38:39 GMT
  12: Server: Kestrel
  13: 
  14: 
  15: 
  16: Content-Length: 15
  17:  
  18: Error occurred!

ExceptionHandlerMiddleware中间件针对缓存响应报头的清除体现在如下所示的代码片段中。我们可以看出它通过调用HttpResponse的OnStarting方法注册了一个回调(ClearCacheHeaders),上述的这三个缓存报头在这个回调中设置的。除此之外,我们还看到这个回调方法还会清除ETag报头,这也很好理解:由于目标资源没有得到正常的响应,表示资源“签名”的ETag报头自然不应该出现在响应报文中。

   1: public class ExceptionHandlerMiddleware
   2: {
   3:     ...
   4:     public async Task Invoke(HttpContext context)
   5:     {
   6:         try
   7:         {
   8:             await _next(context);
   9:         }
  10:         catch (Exception ex)
  11:         {
  12:             …
  13:             context.Response.OnStarting(ClearCacheHeaders, context.Response);
  14:             RequestDelegate handler = _options.ExceptionHandler ?? _next;
  15:             await handler(context);
  16:         }
  17:     }
  18:  
  19:     private Task ClearCacheHeaders(object state)
  20:     {
  21:         var response = (HttpResponse)state;
  22:         response.Headers[HeaderNames.CacheControl]     = "no-cache";
  23:         response.Headers[HeaderNames.Pragma]           = "no-cache";
  24:         response.Headers[HeaderNames.Expires]          = "-1";
  25:         response.Headers.Remove(HeaderNames.ETag);
  26:         return Task.CompletedTask;
  27:     }
  28: }

 



ASP.NET Core应用的错误处理[1]:三种呈现错误页面的方式
ASP.NET Core应用的错误处理[2]:DeveloperExceptionPageMiddleware中间件
ASP.NET Core应用的错误处理[3]:ExceptionHandlerMiddleware中间件
ASP.NET Core应用的错误处理[4]:StatusCodePagesMiddleware中间件

作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文链接

时间: 2024-10-30 18:25:40

ASP.NET Core应用的错误处理[3]:ExceptionHandlerMiddleware中间件如何呈现“定制化错误页面”的相关文章

ASP.NET Core真实管道详解[1]:中间件是个什么东西?

ASP.NET Core管道虽然在结构组成上显得非常简单,但是在具体实现上却涉及到太多的对象,所以我们在 <ASP.NET Core管道深度剖析[共4篇]> 中围绕着一个经过极度简化的模拟管道讲述了真实管道构建的方式以及处理HTTP请求的流程.在这个系列 中,我们会还原构建模拟管道时刻意舍弃和改写的部分,想读者朋友们呈现一个真是的HTTP请求处理管道. ASP.NET Core 的请求处理管道由一个Server和一组有序排列的中间件构成,前者仅仅完成基本的请求监听.接收和响应的工作,请求接收之

ASP.NET Core应用的错误处理[4]:StatusCodePagesMiddleware中间件如何针对响应码呈现错误页面

StatusCodePagesMiddleware中间件与ExceptionHandlerMiddleware中间件比较类似,它们都是在后续请求处理过程中"出错"的情况下利用一个错误处理器来完成最终的请求处理与响应的任务.它们之间的差异在于对"错误"的界定上,对于ExceptionHandlerMiddleware中间件来说,它所谓的错误就是抛出异常,但是对于StatusCodePagesMiddleware中间件来说,则将介于400~599之间的响应状态码视为错误

如何远程关闭一个ASP.NET Core应用?

在<历数依赖注入的N种玩法>演示系统自动注册服务的实例中,我们会发现输出的列表包含两个特殊的服务,它们的对应的服务接口分别是IApplicationLifetime和IHostingEnvironment,我们将分别实现这两个接口的服务统称在ApplicationLifetime和HostingEnvironment.我们从其命名即可以看出ApplicationLifetime与应用的声明周期有关,而HostingEnvironment则用来表示当前的执行环境,本篇文章我们着重来了解Appli

ASP.NET Core应用针对静态文件请求的处理[1]: 以Web的形式发布静态文件

虽然ASP.NET Core是一款"动态"的Web服务端框架,但是在很多情况下都需要处理针对静态文件的请求,最为常见的就是这对JavaScript脚本文件.CSS样式文件和图片文件的请求.针对不同格式的静态文件请求的处理,ASP.NET Core为我们提供了三个中间件,它们将是本系列文章论述的重点.不过在针对对它们展开介绍之前,我们照理通过一些简单的实例来体验一下如何在一个ASP.NET Core应用中发布静态文件.[本文已经同步到<ASP.NET Core框架揭秘>之中]

解析如何利用一个ASP.NET Core应用来发布静态文件_实用技巧

虽然ASP.NET Core是一款"动态"的Web服务端框架,但是在很多情况下都需要处理针对静态文件的请求,最为常见的就是这对JavaScript脚本文件.CSS样式文件和图片文件的请求.针对不同格式的静态文件请求的处理,ASP.NET Core为我们提供了三个中间件,它们将是本系列文章论述的重点.不过在针对对它们展开介绍之前,我们照理通过一些简单的实例来体验一下如何在一个ASP.NET Core应用中发布静态文件. 目录 一.以Web的形式读取文件 二.浏览目录内容 三.显示默认页面

ASP.NET Core的路由[4]:来认识一下实现路由的RouterMiddleware中间件

虽然ASP.NET Core应用的路由是通过RouterMiddleware这个中间件来完成的,但是具体的路由解析功能都落在指定的Router对象上,不过我们依然有必要以代码实现的角度来介绍一下这个中间件.在这之前,我们先来认识一个特殊的特性.[本文已经同步到<ASP.NET Core框架揭秘>之中] 让RouterMiddleware中间件委托Router完整整个路由工作之后,解析出来的路由参数会以一个RouteData对象的形式存储在RouteContext上下文中.但是RouteCont

ASP.NET Core学习之一 入门简介

一.入门简介 在学习之前,要先了解ASP.NET Core是什么?为什么?很多人学习新技术功利心很重,恨不得立马就学会了. 其实,那样做很不好,马马虎虎,联系过程中又花费非常多的时间去解决所遇到的"问题",是简单的问题,对,就是简单,就是因为觉得简单被忽略的东西,恰恰这才是最重要的. 1.学习资料 首先,介绍下哪里可以获得学习资料 英文官网,最好的文档,英语得过硬 https://docs.microsoft.com/en-us/aspnet/core/ 可惜当年英语就是马马虎虎过来的

ASP.NET Core应用的错误处理[2]:DeveloperExceptionPageMiddleware中间件如何呈现“开发者异常页面”

在<ASP.NET Core应用的错误处理[1]:三种呈现错误页面的方式>中,我们通过几个简单的实例演示了如何呈现一个错误页面,这些错误页面的呈现分别由三个对应的中间件来完成,接下来我们将对这三个中间件进行详细介绍.在开发环境呈现的异常页面是通过一个类型为DeveloperExceptionPageMiddleware中间件实现的.[本文已经同步到<ASP.NET Core框架揭秘>之中] 1: public class DeveloperExceptionPageMiddlewa

在ASP.NET Core中显示自定义的错误页面_实用技巧

前言 相信每位程序员们应该都知道在 ASP.NET Core 中,默认情况下当发生500或404错误时,只返回http状态码,不返回任何内容,页面一片空白. 如果在 Startup.cs 的 Configure() 中加上 app.UseStatusCodePages(); ,500错误时依然是一片空白(不知为何对500错误不起作用),404错误时有所改观,页面会显示下面的文字: Status Code: 404; Not Found 如果我们想实现不管500还是404错误都显示自己定制的友好错