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 自定义异常处理中间件

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

  2. ASP.NET Core 中的中间件

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

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

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

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

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

  5. 给 asp.net core 写个中间件来记录接口耗时

    给 asp.net core 写个中间件来记录接口耗时 Intro 写接口的难免会遇到别人说接口比较慢,到底慢多少,一个接口服务器处理究竟花了多长时间,如果能有具体的数字来记录每个接口耗时多少,别人再 ...

  6. ASP.NET Core系列:中间件

    1. 概述 ASP.NET Core中的中间件是嵌入到应用管道中用于处理请求和响应的一段代码. 2. 使用 IApplicationBuilder 创建中间件管道 2.1 匿名函数 使用Run, Ma ...

  7. asp.net core 系列 15 中间件

    一.概述 中间件(也叫中间件组件)是一种装配到应用管道以处理请求和响应的软件. 每个组件:(1)选择是否将请求传递到管道中的下一个组件;(2)可以在管道中的下一个组件之前和之后执行工作. 请求委托用于 ...

  8. 在Asp.Net Core中使用中间件保护非公开文件

    在企业开发中,我们经常会遇到由用户上传文件的场景,比如某OA系统中,由用户填写某表单并上传身份证,由身份管理员审查,超级管理员可以查看. 就这样一个场景,用户上传的文件只能有三种人看得见(能够访问) ...

  9. [译]ASP.NET Core 2.0 中间件

    问题 如何创建一个最简单的ASP.NET Core中间件? 答案 使用VS创建一个ASP.NET Core 2.0的空项目,注意Startup.cs中的Configure()方法: public vo ...

  10. 【ASP.NET Core】给中间件传参数的方法

    最近博客更新频率慢了些,原因有三: 其一,最近老周每星期六都录 ASP.NET Core 的直播,有些内容在视频里讲过,就不太想在博客里面重复.有兴趣的话可以去老周的微博看,或者去一直播,直播帐号与微 ...

随机推荐

  1. ftl 问题

    取不到后台数据,这里是因为 page 已经是关键字

  2. JSP中文乱码总结

    大家在JSP的开发过程中,经常出现中文乱码的问题,可能一至困扰着大家,现把JSP开发中遇到的中文乱码的问题及解决办法写出来供大家参考.首先了解一下Java中文问题的由来: Java的内核和class文 ...

  3. js020-JSON

    js020-JSON 20.1 语法 JSON的语法可以表示为一下三种类型的值. 简单值 使用与JS相同的语法,可以在JSON中表示字符串.数值.布尔值和null,但是JSON不支持JS中的特殊性Un ...

  4. 数组遍历map和each使用

    <body> <input type="/> <input type="/> <input type="/> </b ...

  5. AppcompatActivity闪退问题解决方案

    apply plugin: 'com.android.application' android { compileSdkVersion 23 buildToolsVersion "23.0. ...

  6. JavaScript中的位置坐标

    参考来源 http://www.cnblogs.com/tugenhua0707/p/4501843.html screenX.screenY:浏览器屏幕水平.垂直坐标(相对于浏览器内整个屏幕,包括地 ...

  7. Java ftp断点续传

    FtpTransFile类 import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundExcept ...

  8. EF中使用Contains方法

    第一种情况 var db=new ECEntities(); var list=new []{"8","9"}; var result=from a in db ...

  9. Fastify 系列教程三 (验证、序列化和生命周期)

    Fastify 系列教程: Fastify 系列教程一 (路由和日志) Fastify 系列教程二 (中间件.钩子函数和装饰器) Fastify 系列教程三 (验证.序列化和生命周期) 验证 Fast ...

  10. C#代码总结03---通过获取类型,分类对前台页面的控件进行赋值操作

    该方法: 一般用于将数据库中的基本信息字段显示到前台页面对应的字段控件中 private void InitViewZc(XxEntity model) { foreach (var info in ...