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: 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中间件如何呈现“定制化错误页面”的更多相关文章
- ExceptionHandlerMiddleware中间件如何呈现“定制化错误页面”
ExceptionHandlerMiddleware中间件如何呈现“定制化错误页面” DeveloperExceptionPageMiddleware中间件利用呈现出来的错误页面实现抛出异常和当前请求 ...
- Asp.Net Core 2.0 项目实战(11) 基于OnActionExecuting全局过滤器,页面操作权限过滤控制到按钮级
1.权限管理 权限管理的基本定义:百度百科. 基于<Asp.Net Core 2.0 项目实战(10) 基于cookie登录授权认证并实现前台会员.后台管理员同时登录>我们做过了登录认证, ...
- asp.net core系列 37 WebAPI 使用OpenAPI (swagger)中间件
一.概述 在使用Web API时,对于开发人员来说,了解其各种方法可能是一项挑战.在ASP.NET Core上,Web api 辅助工具介绍二个中间件,包括:Swashbuckle和NSwag .NE ...
- 15.ASP.NET Core 应用程序中的静态文件中间件
在这篇文章中,我将向大家介绍,如何使用中间件组件来处理静态文件.这篇文章中,我们讨论下面几个问题: 在ASP.NET Core中,我们需要把静态文件存放在哪里? 在ASP.NET Core中 wwwr ...
- Asp.Net Core子应用由于配置中重复添加模块会引起IIS错误500.19
ASP.NET Core已经从IIS中解耦,可以作为自宿主程序运行,不再依赖IIS. 但我们还是需要强大的IIS作为前置服务器,IIS利用httpPlatformHandler模块来对后台的一些web ...
- 【ASP.NET Core】解决“The required antiforgery cookie "xxx" is not present”的错误
当你在页面上用 form post 内容时,可能会遇到以下异常: The required antiforgery cookie "????????" is not present ...
- ASP.NET Core 核心特性--宿主、启动、中间件
宿主 public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().R ...
- ASP.NET Core真实管道详解[1]:中间件是个什么东西?
ASP.NET Core管道虽然在结构组成上显得非常简单,但是在具体实现上却涉及到太多的对象,所以我们在 <ASP.NET Core管道深度剖析[共4篇]> 中围绕着一个经过极度简化的模拟 ...
- [译]ASP.NET Core 2.0 带初始参数的中间件
问题 如何在ASP.NET Core 2.0向中间件传入初始参数? 答案 在一个空项目中,创建一个POCO(Plain Old CLR Object)来保存中间件所需的参数: public class ...
随机推荐
- 说说Golang的使用心得
13年上半年接触了Golang,对Golang十分喜爱.现在是2015年,离春节还有几天,从开始学习到现在的一年半时间里,前前后后也用Golang写了些代码,其中包括业余时间的,也有产品项目中的.一直 ...
- ABP框架 - OData 集成
文档目录 本节内容: 简介 安装 安装Nuget包 设置模块依赖 配置你的实体 创建控制器 示例 获取实体列表 请求 响应 获取单个实体 请求 响应 获取单个实体及导航属性 请求 响应 查询 请求 响 ...
- junit4进行单元测试
一.前言 提供服务的时候,为了保证服务的正确性,有时候需要编写测试类验证其正确性和可用性.以前的做法都是自己简单写一个控制层,然后在控制层里调用服务并测试,这样做虽然能够达到测试的目的,但是太不专业了 ...
- HDU1671——前缀树的一点感触
题目http://acm.hdu.edu.cn/showproblem.php?pid=1671 题目本身不难,一棵前缀树OK,但是前两次提交都没有成功. 第一次Memory Limit Exceed ...
- Linux学习之探索文件系统
Linux,一起学习进步- ls With it, we can see directory contents and determine a variety of important file ...
- MySQL 系列(四)主从复制、备份恢复方案生产环境实战
第一篇:MySQL 系列(一) 生产标准线上环境安装配置案例及棘手问题解决 第二篇:MySQL 系列(二) 你不知道的数据库操作 第三篇:MySQL 系列(三)你不知道的 视图.触发器.存储过程.函数 ...
- 代码的坏味道(22)——不完美的库类(Incomplete Library Class)
坏味道--不完美的库类(Incomplete Library Class) 特征 当一个类库已经不能满足实际需要时,你就不得不改变这个库(如果这个库是只读的,那就没辙了). 问题原因 许多编程技术都建 ...
- PHP设计模式(五)建造者模式(Builder For PHP)
建造者模式:将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示的设计模式. 设计场景: 有一个用户的UserInfo类,创建这个类,需要创建用户的姓名,年龄,爱好等信息,才能获得用 ...
- Android开发学习—— Fragment
#Fragment* 用途:在一个Activity里切换界面,切换界面时只切换Fragment里面的内容* 生命周期方法跟Activity一致,可以理解把其为就是一个Activity* 定义布局文件作 ...
- [高性能MYSQL 读后随笔] 关于事务的隔离级别(一)
一.锁的种类 MySQL中锁的种类很多,有常见的表锁和行锁,也有新加入的Metadata Lock等等,表锁是对一整张表加锁,虽然可分为读锁和写锁,但毕竟是锁住整张表,会导致并发能力下降,一般是做dd ...