由于ASP.NET是一个同时处理多个请求的Web应用框架,所以在处理某个请求过程中出现异常并不会导致整个应用的中止。出于安全方面的考量,为了避免敏感信息外泄,客户端在默认情况下并不会得到详细的出错信息,这无疑会在开发过程中增加查错和纠错的难度。对于生产环境来说,我们也希望最终用户能够根据具体的错误类型得到具有针对性并且友好的错误消息。ASP.NET提供的相应的中间件可以帮助我们将定制化的错误信息呈现出来。本文提供的示例演示已经同步到《ASP.NET Core 6框架揭秘-实例演示版》)

目录
[2101]开发者异常页面的呈现(源代码

[2102]定制异常页面的呈现(源代码

[2103]利用注册的中间件处理异常(源代码

[2104]针对异常页面的重定向(源代码

[2105]基于响应状态码错误页面的呈现(设置响应内容模板)(源代码

[2106]基于响应状态码错误页面的呈现(提供异常处理器)(源代码

[2107]基于响应状态码错误页面的呈现(利用中间件创建异常处理器)(源代码

[2101]开发者异常页面的呈现

如果ASP.NET应用在处理某个请求时出现异常,它一般会返回一个状态码为“500 Internal Server Error”的响应。为了避免一些敏感信息的外泄,客户端只会得到一个很泛化的错误消息。以如下所示的程序为例,处理根路径的请求时都会抛出一个InvalidOperationException类型的异常。

var app = WebApplication.Create();
app.MapGet("/",
void () => throw new InvalidOperationException("Manually thrown exception"));
app.Run();

利用浏览器访问这个应用总是会得到图1所示的错误页面。可以看出这个页面仅仅告诉我们目标应用当前无法正常处理本次请求,除了提供的响应状态码(“HTTP ERROR 500”),它并没有提供任何有益于纠错的辅助信息。

图1 默认的错误页面

有人认为浏览器上虽然没有显示任何详细的错误信息,但这并不意味着HTTP响应报文中也没有携带任何详细的出错信息。如下所示的服务端会返回的HTTP响应报文,该响应没有主体内容,有限的几个报头也并没有承载任何与错误有关的信息。

HTTP/1.1 500 Internal Server Error
Content-Length: 0
Date: Sun, 07 Nov 2021 08:34:18 GMT
Server: Kestrel

由于应用并没有中断,浏览器上也并没有显示任何具有针对性的错误信息,我们无法知道背后究竟出现了什么错误。这个问题有两种解决方案:一种是利用日志,ASP.NET在处理请求过程中出现异常时,会发出相应的日志事件,我们可以注册相应的ILoggerProvider对象将日志输出到指定的渠道。另一种解决方案就是利用注册的DeveloperExceptionPageMiddleware中间件显示一个“开发者异常页面(Developer Exception Page)”。

如下的演示程序调用IApplicationBuilder接口的UseDeveloperExceptionPage扩展方法来注册了这个中间件。该程序注册了一个路由模板为“{foo}/{bar}”的终结点,后者在处理请求时直接抛出异常。

var app = WebApplication.Create();
app.UseDeveloperExceptionPage();
app.MapGet("{foo}/{bar}",
void () => throw new InvalidOperationException("Manually thrown exception"));
app.Run();

一旦注册了DeveloperExceptionPageMiddleware中间件,ASP.NET应用在处理请求过程中出现的异常信息就会以图2所示的形式直接出现在浏览器上,我们可以在这个页面中看到几乎所有的错误信息,包括异常的类型、消息和堆栈信息等。

图2 开发者异常页面(基本信息)

开发者异常页面除了显示与抛出的异常相关的信息,还会以图3所示的形式显示与当前请求上下文相关的信息,包括当前请求URL携带的所有查询字符串、所有请求报头、Cookie的内容和路由信息(终结点和路由参数)。如此详尽的信息无疑会极大地帮助开发人员尽快找出错误的根源。由于此页面上往往会携带一些敏感的信息,所以只有在开发环境才能注册这个中间件。实际上Minimal API在开发环境会默认注册这个中间件。

图3 开发者异常页面(详细信息)

[2102]定制异常页面的呈现

由于ExceptionHandlerMiddleware中间件直接利用提供的RequestDelegate委托来处理出现异常的请求,我们可以利用它呈现一个定制化的错误页面。如下的演示程序通过调用IApplicationBuilder接口的UseExceptionHandler扩展方法注册了这个中间件,提供的的ExceptionHandlerOptions配置选项指定了一个指向HandleErrorAsync方法的RequestDelegate委托作为异常处理器。

var options = new ExceptionHandlerOptions { ExceptionHandler = HandleErrorAsync };
var app = WebApplication.Create();
app.UseExceptionHandler(options);
app.MapGet("/",
void () => throw new InvalidOperationException("Manually thrown exception"));
app.Run(); static Task HandleErrorAsync(HttpContext context) => context.Response.WriteAsync("Unhandled exception occurred!");

如上面的代码片段所示,HandleErrorAsync方法仅仅是将一个简单的错误消息(Unhandled exception occurred!)作为响应的内容。演示程序注册了一个针对根路径(“/”)的并且直接抛出异常的终结点,当我们利用浏览器访问该终结点时,这个定制的错误消息会以图4所示的形式直接呈现在浏览器上。

图4 定制的错误页面

[2103]利用注册的中间件处理异常

由于ExceptionHandlerMiddleware中间件的异常处理器的是一个RequestDelegate委托,而IApplicationBuilder对象具有利用注册的中间件来创建这个委托对象的能力,所以用于注册该中间件的UseExceptionHandler扩展方法提供了一个参数类型为Action<IApplicationBuilder>重载。如下的演示程序调用了这个方法,在提供的作为参数的Action<IApplicationBuilder>委托中,我们调用了IApplicationBuilder接口的Run方法注册了一个中间件来处理异常,访问启动后的程序同样会得到如图21-4的错误信息(S2103)。

var app = WebApplication.Create();
app.UseExceptionHandler(app2 => app2.Run(HandleErrorAsync))
app.MapGet("/",
void () => throw new InvalidOperationException("Manually thrown exception"));
app.Run(); static Task HandleErrorAsync(HttpContext context) => context.Response.WriteAsync("Unhandled exception occurred!");

[2104]针对异常页面的重定向

如果应用已经提供了一个错误页面,ExceptionHandlerMiddleware中间件在进行异常处理时可以直接重定向到该页面就可以了。如下的演示程序采用这种方式调用了另一个UseExceptionHandler扩展方法重载,作为参数的字符串(“/error”)指定的就是错误页面的路径,访问启动后的程序同样会得到如图4的错误信息。

var app = WebApplication.Create();
app.UseExceptionHandler("/error");
app.MapGet("/",
void () => throw new InvalidOperationException("Manually thrown exception"));
app.MapGet("/error", HandleErrorAsync);
app.Run(); static Task HandleErrorAsync(HttpContext context) => context.Response.WriteAsync("Unhandled exception occurred!");

[2105]基于响应状态码错误页面的呈现(设置响应内容模板)

我们知道HTTP语义中的错误是由响应的状态码来表达的,涉及的错误大体划分为如下两种类型:

  • 客户端错误:表示因客户端提供不正确的请求信息而导致服务器不能正常处理请求,响应状态码的范围为400~499。
  • 服务端错误:表示服务器在处理请求过程中因自身的问题而发生错误,响应状态码的范围为500~599。

StatusCodePagesMiddleware中间件帮助我们针对响应状态码对错误页面进行定制。该中间件只有在后续管道产生一个错误响应状态码(范围为400~599)才会将错误页面呈现出来。如下的演示程序通过调用UseStatusCodePages扩展方法注册了这个中间件,作为参数的两个字符串分别是响应的媒体类型和作为主体内容的模板,占位符“{0}”将被状态码进行填充。

var app = WebApplication.Create();
app.UseStatusCodePages("text/plain", "Error occurred ({0})");
app.MapGet("/", void (HttpResponse response) => response.StatusCode = 500);
app.Run();

我们针对根路径(“/”)注册了一个终结点,后者在处理请求时直接返回状态码为500的响应。应用启动后,针对该路径请求将会得到如图5所示的错误页面。

图5 针对错误响应状态码定制的错误页面

[2106]基于响应状态码错误页面的呈现(提供异常处理器)

StatusCodePagesMiddleware中间件的错误处理器体现为一个Func<StatusCodeContext, Task>委托,作为输入的StatusCodeContext是对当前HttpContext上下文的封装。如下的演示程序定义了一个与此委托具有一致声明的HandleErrorAsync来呈现错误页面,UseStatusCodePages扩展方法指定的Func<StatusCodeContext, Task>委托指向这个方法。

using Microsoft.AspNetCore.Diagnostics;
var random = new Random();
var app = WebApplication.Create();
app.UseStatusCodePages(HandleErrorAsync);
app.MapGet("/", void (HttpResponse response) => response.StatusCode = random.Next(400,599));
app.Run(); static Task HandleErrorAsync(StatusCodeContext context)
{
var response = context.HttpContext.Response;
return response.StatusCode < 500
? response.WriteAsync($"Client error ({response.StatusCode})")
: response.WriteAsync($"Server error ({response.StatusCode})");
}

我们针对根路径(“/”)注册的终结点会随机返回一个状态码在(400,599)区间内的响应。用来处理错误的HandleErrorAsync方法会根据状态码所在的区间(400~499, 500~599)分别显式“Client error”和“Server error”。应用启动后,针对根路径的请求会得到如图6所示错误页面。

图6 针对错误响应状态码定制的错误页面

[2107]基于响应状态码错误页面的呈现(利用中间件创建异常处理器)

在ASP.NET的世界里,针对请求的处理总是体现为一个RequestDelegate委托,而IApplicationBuilder对象具有根据注册的中间件构建这个委托的能力,所以 UseStatusCodePages方法还具有另一个将Action<IApplicationBuilder>委托作为参数的重载。如下的演示程序调用了这个重载,我们利用提供的委托调用了IApplicationBuilder对象的Run扩展方法注册了一个中间件来处理异常(S2107)。

var random = new Random();
var app = WebApplication.Create();
app.UseStatusCodePages(app2 => app2.Run(HandleErrorAsync));
app.MapGet("/", void (HttpResponse response) => response.StatusCode = random.Next(400,599));
app.Run(); static Task HandleErrorAsync(HttpContext context)
{
var response = context.Response;
return response.StatusCode < 500
? response.WriteAsync($"Client error ({response.StatusCode})")
: response.WriteAsync($"Server error ({response.StatusCode})");
}

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

  1. ASP.NET Core 6框架揭秘实例演示[24]:中间件的多种定义方式

    ASP.NET Core的请求处理管道由一个服务器和一组中间件组成,位于 "龙头" 的服务器负责请求的监听.接收.分发和最终的响应,针对请求的处理由后续的中间件来完成.中间件最终体 ...

  2. ASP.NET Core 6框架揭秘实例演示[07]:文件系统

    ASP.NET Core应用具有很多读取文件的场景,如读取配置文件.静态Web资源文件(如CSS.JavaScript和图片文件等).MVC应用的视图文件,以及直接编译到程序集中的内嵌资源文件.这些文 ...

  3. ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式

    .NET的配置支持多样化的数据源,我们可以采用内存的变量.环境变量.命令行参数.以及各种格式的配置文件作为配置的数据来源.在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源 ...

  4. ASP.NET Core 6框架揭秘实例演示[09]:配置绑定

    我们倾向于将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定.除了将配置树叶子节点配置节的绑定为某种标量对象外,我们还可以直接将一个配置 ...

  5. ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式

    依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...

  6. ASP.NET Core 6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式

    在整个软件开发维护生命周期内,最难的不是如何将软件系统开发出来,而是在系统上线之后及时解决遇到的问题.一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根 ...

  7. ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法

    一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根据当前的运行状态预知未来可能发生的问题,并将问题扼杀在摇篮中.诊断跟踪能够帮助我们有效地纠错和排错&l ...

  8. ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]

    <诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...

  9. ASP.NET Core 6框架揭秘实例演示[14]:日志的进阶用法

    为了对各种日志框架进行整合,微软创建了一个用来提供统一的日志编程模式的日志框架.<日志的基本编程模式>以实例演示的方式介绍了日志的基本编程模式,现在我们来补充几种"进阶" ...

随机推荐

  1. IntelliJ IDEA中如何优雅的调试Java Stream操作

    Stream操作是Java 8推出的一大亮点!虽然java.util.stream很强大,但依然还是有很多开发者在实际工作中很少使用,其中吐槽最多的一个原因就是不好调试,一开始确实是这样,因为stre ...

  2. Calico网络插件

    以下大部分是本人参考各种资料{官方文档.书籍}对知识的汇总和整理,其中有理解错误的地方请大神留言和指正,嘿嘿~~ 1.概述 参考文档:https://projectcalico.docs.tigera ...

  3. SpringBoot 错误(2)

    springBoot整合redis时,报错如下: org.springframework.data.redis.serializer.SerializationException: Cannot de ...

  4. 关于我学git这档子事(2)

    将本地main分支push到远程dev分支(不同名分支间的push) 远程dev分支还未创建 (在push同时创建远程dev分支,并将本地main分支内容上传) git push -u --set-u ...

  5. 功耗优化之Sensor功耗分析

    功耗优化之Sensor功耗分析 一.Sensor功耗问题分类 二.Sensor功耗问题分析方法 SSC子系统引起系统无法进入AOSD问题分析: SSC子系统频繁唤醒AP问题分析方法 SSC子系统的GP ...

  6. 支付宝开放平台--网页&移动应用(一)

    前提是先在支付宝上签约自己需要的支付宝功能,然后支付宝开放平台才能设置你需要的功能 一:支付宝开放平台登录 登录进入支付宝开放平台 二:根据自己的需求创建应用(我是用的网页&移动应用) 三:点 ...

  7. B 树的简单认识

    理解 B 树的概念 B 树是一种自平衡的查找树,能够保持数据有序.这种数据结构能够让查找数据.顺序访问.插入数据及删除数据的动作,都能在对数时间内完成. 同一般的二叉查找树不同,B 树是一棵多路平衡查 ...

  8. 技术分享 | Appium环境安装与架构介绍

    原文链接 Appium架构 Appium 设计哲学 不需要为了自动化而重新编译或修改被测应用 不应该让移动端自动化测试限定在某种语言或者某个具体的框架 不要为了移动端的自动化测试而重新造轮子 移动端自 ...

  9. mac M1 php扩展 xlswriter 编译安装爬坑记录

    电脑配置 MacBook Pro(14英寸,2021年) 系统版本 macOS Monterey 12.4 芯片 Apple M1 Pro PHP环境 MAMP Pro Version 6.6.1 ( ...

  10. 使用nodejs的wxmnode模块,开发一个微信自动监控提醒功能,做个天气预报。

    这个模块是一个公众号的模块,名字叫"帮你看着". 原本这个公众号是做股票监控提醒的,我也没炒股.因为接口支持写入任何内容,所以可以有其他的用处.比如做成天气预报定时提醒. 我们去n ...