DeveloperExceptionPageMiddleware中间件错误页面可以呈现抛出的异常和当前请求上下文的详细信息,以辅助开发人员更好地进行纠错诊断工作。ExceptionHandlerMiddleware中间件则主要面向最终用户,我们可以利用它来显示一个友好的定制化错误页面。更多关于ASP.NET Core的文章请点这里]

一、ExceptionHandlerMiddleware

由于ExceptionHandlerMiddleware中间件可以使用指定的RequestDelegate对象来作为异常处理器,所以我们可以将它视为一个“万能”的异常处理方案。按照惯例,下面先介绍ExceptionHandlerMiddleware类型的定义。

public class ExceptionHandlerMiddleware
{
public ExceptionHandlerMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions<ExceptionHandlerOptions> options, DiagnosticListener diagnosticListener);
public Task Invoke(HttpContext context);
} public class ExceptionHandlerOptions
{
public RequestDelegate ExceptionHandler { get; set; }
public PathString ExceptionHandlingPath { get; set; }
}

与DeveloperExceptionPageMiddleware类似,在创建一个ExceptionHandlerMiddleware对象时同样需要提供一个携带配置选项的对象,从上面的代码片段可以看出,配置选项由一个ExceptionHandlerOptions对象承载。一个ExceptionHandlerOptions对象通过其ExceptionHandler属性提供了一个作为异常处理器的RequestDelegate对象。如果希望应用在发生异常后自动重定向到某个指定的路径,该路径就可以利用ExceptionHandlingPath属性来指定。我们一般调用IApplicationBuilder接口的UseExceptionHandler扩展方法来注册ExceptionHandlerMiddleware中间件,这些重载的UseExceptionHandler扩展方法会采用如下方式完成中间件的注册工作。

public static class ExceptionHandlerExtensions
{
public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app)
=> app.UseMiddleware<ExceptionHandlerMiddleware>(); public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, ExceptionHandlerOptions options)
=> app.UseMiddleware<ExceptionHandlerMiddleware>(Options.Create(options)); public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, string errorHandlingPath)
=>app.UseExceptionHandler(new ExceptionHandlerOptions
{
ExceptionHandlingPath = new PathString(errorHandlingPath)
}); public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, Action<IApplicationBuilder> configure)
{
IApplicationBuilder newBuilder = app.New();
configure(newBuilder); return app.UseExceptionHandler(new ExceptionHandlerOptions
{
ExceptionHandler = newBuilder.Build()
});
}
}

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

public class ExceptionHandlerMiddleware
{
private RequestDelegate _next;
private ExceptionHandlerOptions _options; public ExceptionHandlerMiddleware(RequestDelegate next, IOptions<ExceptionHandlerOptions> options,...)
{
_next = next;
_options = options.Value;
...
} public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch
{
context.Response.StatusCode = 500;
context.Response.Clear();
if (_options.ExceptionHandlingPath.HasValue)
{
context.Request.Path = _options.ExceptionHandlingPath;
}
var handler = _options.ExceptionHandler ?? _next;
await handler(context);
}
}
}

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

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

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

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

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

public interface IExceptionHandlerFeature
{
Exception Error { get; }
} public interface IExceptionHandlerPathFeature : IExceptionHandlerFeature
{
string Path { get; }
} public class ExceptionHandlerFeature : IExceptionHandlerPathFeature,
{
public Exception Error { get; set; }
public string Path { get; set; }
}

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

public class ExceptionHandlerMiddleware
{
...
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch(Exception ex)
{
context.Response.StatusCode = 500; var feature = new ExceptionHandlerFeature()
{
Error = ex,
Path = context.Request.Path,
};
context.Features.Set<IExceptionHandlerFeature>(feature);
context.Features.Set<IExceptionHandlerPathFeature>(feature); if (_options.ExceptionHandlingPath.HasValue)
{
context.Request.Path = _options.ExceptionHandlingPath;
}
RequestDelegate handler = _options.ExceptionHandler ?? _next; try
{
await handler(context);
}
finally
{
context.Request.Path = originalPath;
}
}
}
}

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

public class Program
{
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder
.ConfigureServices(svcs => svcs.AddRouting())
.Configure(app => app
.UseExceptionHandler("/error")
.UseRouting()
.UseEndpoints(endpoints => endpoints.MapGet("error", HandleErrorAsync))
.Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception")))))
.Build()
.Run(); static async Task HandleErrorAsync(HttpContext context)
{
context.Response.ContentType = "text/html";
var ex = context.Features.Get<IExceptionHandlerPathFeature>().Error; await context.Response.WriteAsync("<html><head><title>Error</title></head><body>");
await context.Response.WriteAsync($"<h3>{ex.Message}</h3>");
await context.Response.WriteAsync($"<p>Type: {ex.GetType().FullName}");
await context.Response.WriteAsync($"<p>StackTrace: {ex.StackTrace}");
await context.Response.WriteAsync("</body></html>");
}
}
}

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

三、清除缓存

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

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

public class Program
{
private static readonly Random _random = new Random();
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder.Configure(app => app
.UseExceptionHandler(app2 => app2.Run(HandleAsync))
.Run(ProcessAsync)))
.Build()
.Run(); static Task HandleAsync(HttpContext context) => context.Response.WriteAsync("Error occurred!"); static async Task ProcessAsync(HttpContext context)
{
context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue
{
MaxAge = TimeSpan.FromHours(1)
}; if (_random.Next() % 2 == 0)
{
throw new InvalidOperationException("Manually thrown exception...");
}
await context.Response.WriteAsync("Succeed...");
}
}
}

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

HTTP/1.1 200 OK
Date: Sat, 21 Sep 2019 11:25:27 GMT
Server: Kestrel
Cache-Control: max-age=3600
Content-Length: 10 Succeed...
HTTP/1.1 500 Internal Server Error
Date: Sat, 21 Sep 2019 11:26:11 GMT
Server: Kestrel
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Content-Length: 15 Error occurred!

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

public class ExceptionHandlerMiddleware
{
...
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
...
context.Response.OnStarting(ClearCacheHeaders, context.Response);
var handler = _options.ExceptionHandler ?? _next;
await handler(context);
}
} private Task ClearCacheHeaders(object state)
{
var response = (HttpResponse)state;
response.Headers[HeaderNames.CacheControl] = "no-cache";
response.Headers[HeaderNames.Pragma] = "no-cache";
response.Headers[HeaderNames.Expires] = "-1";
response.Headers.Remove(HeaderNames.ETag);
return Task.CompletedTask;
}
}

ASP.NET Core错误处理中间件[3]: 异常处理器的更多相关文章

  1. ASP.NET Core错误处理中间件[2]: 开发者异常页面

    <呈现错误信息>通过几个简单的实例演示了如何呈现一个错误页面,该过程由3个对应的中间件来完成.下面先介绍用来呈现开发者异常页面的DeveloperExceptionPageMiddlewa ...

  2. ASP.NET Core错误处理中间件[1]: 呈现错误信息

    NuGet包"Microsoft.AspNetCore.Diagnostics"中提供了几个与异常处理相关的中间件.当ASP.NET Core应用在处理请求过程中出现错误时,我们可 ...

  3. ASP.NET Core错误处理中间件[4]: 响应状态码页面

    StatusCodePagesMiddleware中间件与ExceptionHandlerMiddleware中间件类似,它们都是在后续请求处理过程中"出错"的情况下利用一个错误处 ...

  4. asp.net core 自定义异常处理中间件

    asp.net core 自定义异常处理中间件 Intro 在 asp.net core 中全局异常处理,有时候可能不能满足我们的需要,可能就需要自己自定义一个中间件处理了,最近遇到一个问题,有一些异 ...

  5. ASP.NET Core 3.1 中间件

    参考微软官方文档 : https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1 ...

  6. 理解ASP.NET Core - 错误处理(Handle Errors)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或[点击此处查看全文目录](https://www.cnblogs.com/xiaoxiaotank/p/151852 ...

  7. ASP.NET Core 中的中间件

    前言   由于是第一次写博客,如果您看到此文章,希望大家抱着找错误.批判的心态来看. sky! 何为中间件? 在 ASP.NET Framework 中应该都知道请求管道.可参考:浅谈 ASP.NET ...

  8. Asp.Net Core 通过自定义中间件防止图片盗链的实例(转)

    一.原理 要实现防盗链,我们就必须先理解盗链的实现原理,提到防盗链的实现原理就不得不从HTTP协议说起,在HTTP协议中,有一个表头字段叫referer,采用URL的格式来表示从哪儿链接到当前的网页或 ...

  9. 在Asp.net Core中使用中间件来管理websocket

    介绍 ASP.NET Core SignalR是一个有用的库,可以简化Web应用程序中实时通信的管理.但是,我宁愿使用WebSockets,因为我想要更灵活,并且与任何WebSocket客户端兼容. ...

随机推荐

  1. DRF框架笔记

    序列化器类的定义格式? 继承serializers.Serializer:字段 = serializers.字段类型(选项参数) 序列化器类的基本使用? 序列化器类(instance=None, da ...

  2. 为什么类只能用public修饰?

    为什么类只能使用public修饰? 首先,类只能使用public修饰是一个伪命题,应该说我们只见到过使用public修饰的类,还有一些类没有访问修饰符,此时访问权限为default.其次,类实际上分为 ...

  3. Spring Data JPA 整合Spring 第二篇

    主要是在CustomerDao中去写一些代码,在调用Query中去用SQL 例如 public interface CustomerDao extends JpaRepository<Custo ...

  4. 配合php伪协议利用文件包含漏洞

    文章来源: https://blog.csdn.net/zpy1998zpy/article/details/80598768?utm_medium=distribute.pc_relevant.no ...

  5. Excel 多/整列(多/整行)移位操作

    步骤1:创建测试数据 步骤2:把B列和C列进行移位操作(整列移位操作,多列移位操作方法一样) 选中B列,鼠标放到B列边缘地带,直到鼠标显示带有四个箭头方向为止,点击键盘shift键进行拖拽,拖拽时显示 ...

  6. 短链接服务Octopus的实现与源码开放

    前提 半年前(2020-06)左右,疫情触底反弹,公司的业务量不断提升,运营部门为了方便短信.模板消息推送等渠道的投放,提出了一个把长链接压缩为短链接的功能需求.当时为了快速推广,使用了一些比较知名的 ...

  7. 拥抱 C/C++ : Android JNI 的使用

    编译工具 CMake 以及 Android 上 JNI 的使用介绍. 编译工具 CMake 在Android Studio 2.2 之后,工具中增加了 CMake 的支持,于是我们有两种选择来编译 c ...

  8. springboot日志输出到文件

    今天来谈一谈日志,主要是说一说springboot的日志,因为最近在学习springboot.首先在写代码的时候,要养成记日志的习惯,这点真的很重要,因为之前吃了很多亏.过去我对日志很不在意,该有的日 ...

  9. 【探索之路】机器人篇(5)-Gazebo物理仿真环境搭建_让机器人运动起来

    如果完成了前两步,那么其实我们已经可以去连接我们的现实中的机器人了. 但是,做机器人所需要的材料还没有到,所以我们这里先在电脑平台上仿真一下.这里我们用到的就算gazebo物理仿真环境,他能很好的和R ...

  10. 一次MySQL死锁的排查记录

    前几天线上收到一条告警邮件,生产环境MySQL操作发生了死锁,邮件告警的提炼出来的SQL大致如下. update pe_order_product_info_test set end_time = ' ...