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. 关于几个主流语音SDK的接入问题

    这两周都在忙着游戏上线还有接入游戏语音,两周分别接了腾讯语音和百度语音!!! 关于腾讯语音的一些问题 由于发现腾讯语音的在录完音频后的数据是编过码的所以出现了一些问题: *不能解码(腾讯方不提供解码算 ...

  2. obj.style.z-index的正确写法

    obj.style.z-index的正确写法 今天发现obj.style.z-index在js里面报错,后来才知道在js里应该把含"-"的字符写成驼峰式,例如obj.style.z ...

  3. CSS 3 学习——transform 3D转换渲染

    以下内容根据官方规范翻译,没有翻译关于SVG变换的内容和关于矩阵计算的内容. 一般情况下,元素在一个无景深无立体感的平面(flat plane)上渲染,这个平面就是其包含块所处的平面.同时,页面上的其 ...

  4. 介绍一款原创的四则运算算式生成器:CalculateIt2

    家里小朋友读一年级了,最近每天都有一些10以内的加减法口算练习,作为程序员爸爸,自然也是想办法能够偷懒,让电脑出题,给小朋友做些练习.于是,自己在业余时间开发了一个四则运算算式生成器,名为:Calcu ...

  5. JavaScript实现DOM对象选择器

    目的: 根据传入的选择器类型选出第一个符合的DOM对象. ①可以通过id获取DOM对象,例如 $("#adom"); ②可以通过tagName获取DOM对象,例如 $(" ...

  6. MFC单文档程序添加HTML帮助支持

    1.在App类 构造函数中添加 EnableHtmlHelp(); 2.在Frame类中,添加消息影射: ON_COMMAND(ID_HELP_FINDER, CFrameWnd::OnHelpFin ...

  7. SAP CRM 用户界面对象类型和设计对象

    在CRM中的用户界面对象类型的帮助下,我们可以做这些工作: 进行不同的视图配置 创建动态导航 从设计层控制字段标签.值帮助 控制BOL对象的属性的可视性 从导航栏访问自定义组件 一个用户界面对象类型之 ...

  8. xss和sql注入原理学习

    8.4 Web跨站脚本攻击 8.4.1  跨站脚本攻击的原理(1) 跨站脚本在英文中称为Cross-Site Scripting,缩写为CSS.但是,由于层叠样式表 (Cascading Style ...

  9. App 审核由于 IPv6 网络问题被拒

    昨天 提交App Store 的时候被拒了 We discovered one or more bugs in your app when reviewed on iPhone running iOS ...

  10. 转:MSSQL还原单mdf文件报1813错误

    原文地址:http://www.cnblogs.com/clownkings/p/4950865.html 解决办法: 1.首先要备份好mdf文件,如果他没了经理非吃了你不可.都不吐骨头的. 2.在数 ...