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:             context.Features.Set<IExceptionHandlerFeature>(feature);

  20:             context.Features.Set<IExceptionHandlerPathFeature>(feature);

  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:                 context.Request.Path = originalPath;

  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: Cache-Control: max-age=3600

   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: Cache-Control: no-cache

  14: Pragma: no-cache

  15: Expires: -1

  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中间件

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

  1. ExceptionHandlerMiddleware中间件如何呈现“定制化错误页面”

    ExceptionHandlerMiddleware中间件如何呈现“定制化错误页面” DeveloperExceptionPageMiddleware中间件利用呈现出来的错误页面实现抛出异常和当前请求 ...

  2. Asp.Net Core 2.0 项目实战(11) 基于OnActionExecuting全局过滤器,页面操作权限过滤控制到按钮级

    1.权限管理 权限管理的基本定义:百度百科. 基于<Asp.Net Core 2.0 项目实战(10) 基于cookie登录授权认证并实现前台会员.后台管理员同时登录>我们做过了登录认证, ...

  3. asp.net core系列 37 WebAPI 使用OpenAPI (swagger)中间件

    一.概述 在使用Web API时,对于开发人员来说,了解其各种方法可能是一项挑战.在ASP.NET Core上,Web api 辅助工具介绍二个中间件,包括:Swashbuckle和NSwag .NE ...

  4. 15.ASP.NET Core 应用程序中的静态文件中间件

    在这篇文章中,我将向大家介绍,如何使用中间件组件来处理静态文件.这篇文章中,我们讨论下面几个问题: 在ASP.NET Core中,我们需要把静态文件存放在哪里? 在ASP.NET Core中 wwwr ...

  5. Asp.Net Core子应用由于配置中重复添加模块会引起IIS错误500.19

    ASP.NET Core已经从IIS中解耦,可以作为自宿主程序运行,不再依赖IIS. 但我们还是需要强大的IIS作为前置服务器,IIS利用httpPlatformHandler模块来对后台的一些web ...

  6. 【ASP.NET Core】解决“The required antiforgery cookie "xxx" is not present”的错误

    当你在页面上用 form post 内容时,可能会遇到以下异常: The required antiforgery cookie "????????" is not present ...

  7. ASP.NET Core 核心特性--宿主、启动、中间件

    宿主 public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().R ...

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

    ASP.NET Core管道虽然在结构组成上显得非常简单,但是在具体实现上却涉及到太多的对象,所以我们在 <ASP.NET Core管道深度剖析[共4篇]> 中围绕着一个经过极度简化的模拟 ...

  9. [译]ASP.NET Core 2.0 带初始参数的中间件

    问题 如何在ASP.NET Core 2.0向中间件传入初始参数? 答案 在一个空项目中,创建一个POCO(Plain Old CLR Object)来保存中间件所需的参数: public class ...

随机推荐

  1. Javascript - Promise学习笔记

    最近工作轻松了点,想起了以前总是看到的一个单词promise,于是耐心下来学习了一下.   一:Promise是什么?为什么会有这个东西? 首先说明,Promise是为了解决javascript异步编 ...

  2. 数据库优化案例——————某市中心医院HIS系统

    记得在自己学习数据库知识的时候特别喜欢看案例,因为优化的手段是容易掌握的,但是整体的优化思想是很难学会的.这也是为什么自己特别喜欢看案例,今天也开始分享自己做的优化案例. 最近一直很忙,博客产出也少的 ...

  3. 解决vs创建或打开C++浏览数据库文件*.sdf时发生错误的问题

    VS2012,  创建或打开C++浏览数据库文件*.sdf时发生错误. IntelliSense 和浏览信息将不能用于C++项目. 请确保已安装 Microsoft SQL Server Compac ...

  4. Javascript中的valueOf与toString

    基本上,javascript中所有数据类型都拥有valueOf和toString这两个方法,null除外.它们俩解决javascript值运算与显示的问题,本文将详细介绍,有需要的朋友可以参考下. t ...

  5. 【一起学OpenFoam】01 OpenFoam的优势

    CFD技术发展到今天,已经超过了大半个世纪了,已经涌现出非常多的CFD软件可供人们使用.通用商业CFD软件譬如Fluent.CFX.Star CCM+等在工业上得到了广泛的应用,另外一些专用的软件(如 ...

  6. python_单元测试unittest

    Python自带一个单元测试框架是unittest模块,用它来做单元测试,它里面封装好了一些校验返回的结果方法和一些用例执行前的初始化操作. 步骤1:首先引入unittest模块--import un ...

  7. test

    http://img.ivsky.com/img/bizhi/pic/201009/07/fangaoyouhua-015.jpghttp://desk.fd.zol-img.com.cn/t_s16 ...

  8. 分享阿里云推荐码 IC1L2A,购买服务器可以直接打9折,另附阿里云服务器部署ASP.NET MVC5关键教程

    阿里云推荐码为:IC1L2A 阿里云还是不错滴. 以windows server 2008 R2为例,介绍如何从全新的服务器部署MVC5 站点. 新购买的阿里云服务器是没有IIS的,要安装IIS: 控 ...

  9. 浅析Java 泛型

    泛型是JavaSE5引入的一个新概念,但是这个概念在编程语言中却是很普遍的一个概念.下面,根据以下内容,我们总结下在Java中使用泛型. 泛型使用的意义 什么是泛型 泛型类 泛型方法 泛型接口 泛型擦 ...

  10. ASP.net 页面生命周期

    ASP.NET 页面生命周期 Page_Preinit(); 在页初始化开始时发生 Page_Init(); 在所有控件初始化且应用外观设置后引发 Page_InitComplete(); 在页初始化 ...