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最终利用异常处理器来处理请求之前,它会对请求做一些前置处理工作,比如它会将响应状态码设置为,比如清空当前所有响应内容等。如果我们利用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中间件

作者:蒋金楠 
微信公众账号:大内老A
微博:www.weibo.com/artech

ExceptionHandlerMiddleware中间件如何呈现“定制化错误页面”的更多相关文章

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

    DeveloperExceptionPageMiddleware中间件利用呈现出来的错误页面实现抛出异常和当前请求的详细信息以辅助开发人员更好地进行纠错诊断工作,而ExceptionHandlerMi ...

  2. Java Web应用定制404错误页面

    Http响应状态码404的含义是服务器端没有找到客户端请求的资源,定制404错误页面至少有以下两个好处: 1.向客户端隐藏服务器信息,服务器提供的默认404错误页面上一般都包含当前应用使用的是什么服务 ...

  3. 关于ProjectServer定制化项目中心页面

    ProjectServer界面很多客户接受不了,随便用户可以根据自己需要展示页面,但大多数国内用户喜欢确定的样式,我就是要这样的页面,不要个人定制. 那只有自己再做一个项目中心的webpart嵌入,对 ...

  4. [SoapUI] 通过JSONAssert比较两个环境的JSON Response,定制化错误信息到Excel

    package ScriptLibrary; import org.json.JSONArray; import org.json.JSONException; import org.json.JSO ...

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

    StatusCodePagesMiddleware中间件与ExceptionHandlerMiddleware中间件比较类似,它们都是在后续请求处理过程中“出错”的情况下利用一个错误处理器来完成最终的 ...

  6. StatusCodePagesMiddleware中间件如何针对响应码呈现错误页面

    StatusCodePagesMiddleware中间件如何针对响应码呈现错误页面 StatusCodePagesMiddleware中间件与ExceptionHandlerMiddleware中间件 ...

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

    在<ASP.NET Core应用的错误处理[1]:三种呈现错误页面的方式>中,我们通过几个简单的实例演示了如何呈现一个错误页面,这些错误页面的呈现分别由三个对应的中间件来完成,接下来我们将 ...

  8. ASP.NET Core应用的错误处理[1]:三种呈现错误页面的方式

    由于ASP.NET Core应用是一个同时处理多个请求的服务器应用,所以在处理某个请求过程中抛出的异常并不会导致整个应用的终止.出于安全方面的考量,为了避免敏感信息的外泄,客户端在默认的情况下并不会得 ...

  9. ASP.NET Core 6框架揭秘实例演示[32]:错误页面的集中呈现方式

    由于ASP.NET是一个同时处理多个请求的Web应用框架,所以在处理某个请求过程中出现异常并不会导致整个应用的中止.出于安全方面的考量,为了避免敏感信息外泄,客户端在默认情况下并不会得到详细的出错信息 ...

随机推荐

  1. 搭建自己的AppRTCDemo服务器

    http://www.jianshu.com/p/c55ecf5a3fcf http://io.diveinedu.com/2015/02/05/%E7%AC%AC%E5%85%AD%E7%AB%A0 ...

  2. listen 59

    Different Brain Regions Handle Different Music Types (Vivaldi) versus (the Beatles) . Both great. Bu ...

  3. vim 模式下的几个快捷用法

    1.ctrl + v  (-- VISUAL BLOCK --) 选中块模式,y 复制,d 剪切,p 粘贴,Esc退出模式 2.Shift + v  (-- VISUAL LINE -- ) 快速行选 ...

  4. BZOJ2329:[HNOI2011]括号修复

    浅谈\(splay\):https://www.cnblogs.com/AKMer/p/9979592.html 浅谈\(fhq\)_\(treap\):https://www.cnblogs.com ...

  5. CF285 E Positions in Permutations——“恰好->大于”的容斥和允许“随意放”的dp

    题目:http://codeforces.com/contest/285/problem/E 是2018.7.31的一场考试的题,当时没做出来. 题解:http://www.cnblogs.com/y ...

  6. 重学JAVA基础(一):PATH和CLASSPATH

    我想大多数Java初学者都会遇到的问题,那就是怎么配置环境,执行java -jar xxx.jar  都会报NoClassDefFindError,我在最开始学习的时候,也遇到了这些问题. 1.PAT ...

  7. 如何让Surface RT支持网站的flash

    Go to the desktop version of IE10, hit the click ALT button on your keyboard, click on and then Comp ...

  8. centos7 install pip

    1. 安装过程 yum -y install epel-release yum install python-pip pip install --upgrade pip

  9. In-App Purchase Configuration Guide for iTunes Connect---(一)----Introduction

    Introduction In-App Purchase is an Apple technology that allows your users to purchase content and s ...

  10. mysql连接错误解决

    EB101IWSWD-eyJsaWNlbnNlSWQiOiJFQjEwMUlXU1dEIiwibGljZW5zZWVOYW1lIjoibGFuIHl1IiwiYXNzaWduZWVOYW1lIjoiI ...