ASP.NET Core错误处理中间件[1]: 呈现错误信息
NuGet包“Microsoft.AspNetCore.Diagnostics”中提供了几个与异常处理相关的中间件。当ASP.NET Core应用在处理请求过程中出现错误时,我们可以利用它们将原生的或者定制的错误信息作为响应内容发送给客户端。在着重介绍这些中间件之前,下面先演示几个简单的实例,从而使读者大致了解这些中间件的作用。
一、显示开发者异常页面
如果ASP.NET Core应用在处理某个请求时出现异常,它一般会返回一个状态码为“500 Internal Server Error”的响应。为了避免一些敏感信息的外泄,详细的错误信息并不会随着响应发送给客户端,所以客户端只会得到一个很泛化的错误消息。以如下所示的程序为例,它处理每个请求时都会抛出一个InvalidOperationException类型的异常。
public class Program
{
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder.Configure(app => app.Run(
context=> Task.FromException(new InvalidOperationException("Manually thrown exception...")))))
.Build()
.Run();
}
}
利用浏览器访问这个应用总是会得到下图所示的错误页面。可以看出,这个页面仅仅告诉我们目标应用当前无法正常处理本次请求,除了提供的响应状态码(“HTTP ERROR 500”),它并没有提供任何有益于纠错的辅助信息。

有人认为浏览器上虽然没有显示任何详细的错误信息,但这并不意味着HTTP响应报文中也没有携带任何详细的出错信息。实际上,针对通过浏览器发出的这个请求,服务端会返回如下这段HTTP响应报文。我们会发现响应报文根本没有主体部分,有限的几个报头也并没有承载任何与错误有关的信息。
HTTP/1.1 500 Internal Server Error
Date: Wed, 18 Sep 2019 23:38:59 GMT
Content-Length: 0
Server: Kestrel
由于应用并没有中断,浏览器上也并没有显示任何具有针对性的错误信息,开发人员在进行查错和纠错时如何准确定位到作为错误根源的那一行代码?这个问题有两种解决方案:一种是利用日志,因为ASP.NET Core应用在进行请求处理时出现的任何错误都会被写入日志,所以可以通过注册相应的ILoggerProvider对象来获取写入的错误日志,如可以注册一个ConsoleLoggerProvider对象将日志直接输出到宿主应用的控制台上。
另一种解决方案就是直接显示一个错误页面,由于这个页面只是在开发环境给开发人员看的,所以可以将这个页面称为开发者异常页面(Developer Exception Page)。开发者异常页面的呈现是利用一个名为DeveloperExceptionPageMiddleware的中间件完成的,我们可以采用如下所示的方式调用IApplicationBuilder接口的UseDeveloperExceptionPage扩展方法来注册这个中间件。
public class Program
{
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureServices(svcs => svcs.AddRouting())
.ConfigureWebHostDefaults(builder => builder.Configure(app => app
.UseDeveloperExceptionPage()
.UseRouting()
.UseEndpoints(endpoints => endpoints.MapGet("{foo}/{bar}", HandleAsync))))
.Build()
.Run(); static Task HandleAsync(HttpContext httpContext)
=> Task.FromException(new InvalidOperationException("Manually thrown exception..."));
}
}
一旦注册了DeveloperExceptionPageMiddleware中间件,ASP.NET Core应用在处理请求过程中出现的异常信息就会以下图所示的形式直接出现在浏览器上,我们可以在这个页面中看到几乎所有的错误信息,包括异常的类型、消息和堆栈信息等。

开发者异常页面除了显示与抛出的异常相关的信息,还会以图16-3所示的形式显示与当前请求上下文相关的信息,其中包括当前请求URL携带的所有查询字符串、所有请求报头、Cookie的内容和路由信息(终结点和路由参数)。如此详尽的信息无疑会极大地帮助开发人员尽快找出错误的根源。

通过DeveloperExceptionPageMiddleware中间件呈现的错误页面仅仅是供开发人员使用的,页面上往往会携带一些敏感的信息,所以只有在开发环境才能注册这个中间件,如下所示的代码片段体现了Startup类型中针对DeveloperExceptionPageMiddleware中间件正确的注册方式。
public class Startup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
}
}
二、显示定制异常页面
DeveloperExceptionPageMiddleware中间件会将异常详细信息和基于当前请求的上下文直接呈现在错误页面中,这为开发人员的纠错诊断提供了极大的便利。但是在生产环境下,我们倾向于为最终的用户呈现一个定制的错误页面,这可以通过注册另一个名为ExceptionHandlerMiddleware的中间件来实现。顾名思义,这个中间件旨在提供一个异常处理器(ExceptionHandler)来处理抛出的异常。实际上,这个所谓的异常处理器就是一个RequestDelegate对象,ExceptionHandlerMiddleware中间件捕捉到抛出的异常后利用它来处理当前的请求。
下面以上面创建的这个总是会抛出一个 InvalidOperationException异常的应用为例进行介绍。我们按照如下形式调用IApplicationBuilder接口的UseExceptionHandler扩展方法注册了ExceptionHandlerMiddleware中间件。这个扩展方法具有一个ExceptionHandlerOptions类型的参数,它的ExceptionHandler属性返回的就是这个作为异常处理器的RequestDelegate对象。
public class Program
{
public static void Main()
{
var options = new ExceptionHandlerOptions { ExceptionHandler = HandleAsync };
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder.Configure(app => app
.UseExceptionHandler(options)
.Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception...")))))
.Build()
.Run(); static Task HandleAsync(HttpContext context) => context.Response.WriteAsync("Unhandled exception occurred!");
}
如上面的代码片段所示,这个作为异常处理器的RequestDelegate对象仅仅是将一个简单的错误消息(Unhandled exception occurred!)作为响应的内容。当我们利用浏览器访问该应用时,这个定制的错误消息会以下图所示的形式直接呈现在浏览器上。

由于最终作为异常处理器的是一个RequestDelegate对象,而IApplicationBuilder对象具有根据注册的中间件来创建这个委托对象的能力,所以我们可以根据异常处理的需求将相应的中间件注册到某个IApplicationBuilder对象上,并最终利用它来创建作为异常处理器的RequestDelegate对象。如果异常处理需要通过一个或者多个中间件来完成,我们可以按照如下所示的形式调用另一个UseExceptionHandler方法重载。这个方法的参数类型为Action<IApplicationBuilder>,我们调用它的Run方法注册了一个中间件来响应一个简单的错误消息。
public class Program
{
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(builder => builder.Configure(app => app
.UseExceptionHandler(app2 => app2.Run(HandleAsync))
.Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception...")))))
.Build()
.Run(); static Task HandleAsync(HttpContext context) => context.Response.WriteAsync("Unhandled exception occurred!");
}
}
上面这两种异常处理的形式都体现在提供一个RequestDelegate的委托对象来处理抛出的异常并完成最终的响应。如果应用已经设置了一个错误页面,并且这个错误页面有一个固定的路径,那么我们在进行异常处理的时候就没有必要提供这个RequestDelegate对象,只需要重定向到错误页面指向的路径即可。这种采用服务端重定向的异常处理方式可以采用如下所示的形式调用另一个UseExceptionHandler方法重载来完成,这个方法的参数表示的就是重定向的目标路径(“/error”),我们针对这个路径注册了一个路由来响应定制的错误消息。
public class Program
{
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureServices(svcs => svcs.AddRouting())
.ConfigureWebHostDefaults(builder => builder.Configure(app => app
.UseExceptionHandler("/error")
.UseRouting()
.UseEndpoints(endpoints => endpoints.MapGet("error", HandleAsync))
.Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception...")))))
.Build()
.Run(); static Task HandleAsync(HttpContext context) => context.Response.WriteAsync("Unhandled exception occurred!");
}
}
三、针对响应状态码定制错误页面
由于Web应用采用HTTP通信协议,所以我们应该尽可能迎合HTTP标准,并将定义在协议规范中的语义应用到程序中。异常或者错误的语义表达在HTTP协议层面主要体现在响应报文的状态码上,具体来说,HTTP通信的错误大体分为如下两种类型。
- 客户端错误:表示因客户端提供不正确的请求信息而导致服务器不能正常处理请求,响应状态码的范围为400~499。
- 服务端错误:表示服务器在处理请求过程中因自身的问题而发生错误,响应状态码的范围为500~599。
正是因为响应状态码是对错误或者异常语义最重要的表达,所以在很多情况下我们需要针对不同的响应状态码来定制显示的错误信息。针对响应状态码对错误页面的定制可以借助一个StatusCodePagesMiddleware类型的中间件来实现,我们可以调用IApplicationBuilder接口相应的扩展方法来注册这个中间件。
DeveloperExceptionPageMiddleware中间件和ExceptionHandlerMiddleware中间件都是在后续请求处理过程中抛出异常的情况下才会被调用的,而StatusCodePagesMiddleware中间件被调用的前提是后续请求处理过程中产生一个错误的响应状态码(范围为400~599)。如果仅仅希望显示一个统一的错误页面,我们可以按照如下所示的形式调用IApplicationBuilder接口的UseStatusCodePages扩展方法注册这个中间件,传入该方法的两个参数分别表示响应采用的媒体类型和主体内容。
public class Program
{
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(webBuilder => webBuilder.Configure(app => app
.UseStatusCodePages("text/plain", "Error occurred ({0})")
.Run(context => Task.Run(() => context.Response.StatusCode = 500))))
.Build()
.Run();
}
}
如上面的代码片段所示,应用程序在处理请求时总是将响应状态码设置为“500”,所以最终的响应内容将由注册的StatusCodePagesMiddleware中间件来提供。我们调用UseStatusCodePages方法时将响应的媒体类型设置为text/plain,并将一段简单的错误消息作为响应的主体内容。值得注意的是,作为响应内容的字符串可以包含一个占位符({0}),StatusCodePagesMiddleware中间件最终会采用当前响应状态码来替换它。如果我们利用浏览器来访问这个应用,得到的错误页面如下图16-5所示。

如果我们希望针对不同的错误状态码显示不同的错误页面,那么就需要将具体的请求处理逻辑实现在一个状态码错误处理器中,并最终提供给StatusCodePagesMiddleware中间件。这个所谓的状态码错误处理器体现为一个Func<StatusCodeContext, Task>类型的委托对象,作为输入的StatusCodeContext对象是对HttpContext上下文的封装,它同时承载着其他一些与错误处理相关的选项设置,我们将在本章后续部分对这个类型进行详细介绍。
对于如下所示的应用来说,它在处理任意一个请求时总是随机选择400~599的一个整数来作为响应的状态码,所以客户端返回的响应内容总是通过注册的StatusCodePagesMiddleware中间件来提供。在调用另一个UseStatusCodePages方法重载时,我们为注册的中间件指定一个Func<StatusCodeContext, Task>对象作为状态码错误处理器。
public class Program
{
private static readonly Random _random = new Random();
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(webBuilder => webBuilder.Configure(app => app
.UseStatusCodePages(HandleAsync)
.Run(context => Task.Run(() => context.Response.StatusCode = _random.Next(400, 599)))))
.Build()
.Run(); static async Task HandleAsync(StatusCodeContext context)
{
var response = context.HttpContext.Response;
if (response.StatusCode < 500)
{
await response.WriteAsync($"Client error ({response.StatusCode})");
}
else
{
await response.WriteAsync($"Server error ({response.StatusCode})");
}
}
}
}
我们指定的状态码错误处理器在处理请求时,根据响应状态码将错误分为客户端错误和服务端错误两种类型,并选择针对性的错误消息作为响应内容。当我们利用浏览器访问这个应用的时候,显示的错误消息将以下图所示的形式由响应状态码来决定。

在ASP.NET Core的世界里,针对请求的处理总是体现为一个RequestDelegate对象。如果请求的处理需要借助一个或者多个中间件来完成,就可以将它们注册到IApplicationBuilder对象上,并利用该对象将中间件管道转换成一个RequestDelegate对象。用于注册StatusCodePagesMiddleware中间件的UseStatusCodePages方法还有另一个重载,它允许我们采用这种方式来创建一个RequestDelegate对象来完成错误请求处理工作,所以上面演示的这个应用完全可以改写成如下形式。
public class Program
{
private static readonly Random _random = new Random();
public static void Main()
{
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(webBuilder => webBuilder.Configure(app => app
.UseStatusCodePages(app2 => app2.Run(HandleAsync))
.Run(context => Task.Run(() => context.Response.StatusCode = _random.Next(400, 599)))))
.Build()
.Run(); static async Task HandleAsync(HttpContext context)
{
var response = context.Response;
if (response.StatusCode < 500)
{
await response.WriteAsync($"Client error ({response.StatusCode})");
}
else
{
await response.WriteAsync($"Server error ({response.StatusCode})");
}
}
}
}
ASP.NET Core错误处理中间件[1]: 呈现错误信息
ASP.NET Core错误处理中间件[2]: 开发者异常页面
ASP.NET Core错误处理中间件[3]: 异常处理器
ASP.NET Core错误处理中间件[4]: 响应状态码页面
ASP.NET Core错误处理中间件[1]: 呈现错误信息的更多相关文章
- ASP.NET Core 中的中间件
前言 由于是第一次写博客,如果您看到此文章,希望大家抱着找错误.批判的心态来看. sky! 何为中间件? 在 ASP.NET Framework 中应该都知道请求管道.可参考:浅谈 ASP.NET ...
- asp.net core 自定义异常处理中间件
asp.net core 自定义异常处理中间件 Intro 在 asp.net core 中全局异常处理,有时候可能不能满足我们的需要,可能就需要自己自定义一个中间件处理了,最近遇到一个问题,有一些异 ...
- ASP.NET Core静态文件中间件[1]: 搭建文件服务器
虽然ASP.NET Core是一款"动态"的Web服务端框架,但是由它接收并处理的大部分是针对静态文件的请求,最常见的是开发Web站点使用的3种静态文件(JavaScript脚本. ...
- ASP.NET Core 3.1 中间件
参考微软官方文档 : https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1 ...
- Asp.Net Core 通过自定义中间件防止图片盗链的实例(转)
一.原理 要实现防盗链,我们就必须先理解盗链的实现原理,提到防盗链的实现原理就不得不从HTTP协议说起,在HTTP协议中,有一个表头字段叫referer,采用URL的格式来表示从哪儿链接到当前的网页或 ...
- 在Asp.net Core中使用中间件来管理websocket
介绍 ASP.NET Core SignalR是一个有用的库,可以简化Web应用程序中实时通信的管理.但是,我宁愿使用WebSockets,因为我想要更灵活,并且与任何WebSocket客户端兼容. ...
- 给 asp.net core 写个中间件来记录接口耗时
给 asp.net core 写个中间件来记录接口耗时 Intro 写接口的难免会遇到别人说接口比较慢,到底慢多少,一个接口服务器处理究竟花了多长时间,如果能有具体的数字来记录每个接口耗时多少,别人再 ...
- ASP.NET Core系列:中间件
1. 概述 ASP.NET Core中的中间件是嵌入到应用管道中用于处理请求和响应的一段代码. 2. 使用 IApplicationBuilder 创建中间件管道 2.1 匿名函数 使用Run, Ma ...
- ASP.NET Core 实战:使用 NLog 将日志信息记录到 MongoDB
一.前言 在项目开发中,日志系统是系统的一个重要组成模块,通过在程序中记录运行日志.错误日志,可以让我们对于系统的运行情况做到很好的掌控.同时,收集日志不仅仅可以用于诊断排查错误,由于日志同样也是大量 ...
随机推荐
- es6删除指定元素
原数组: let arr =[{id:1},{id:2},{id:3},{id:8}] 待删除数据 obj = {id:1} 删除原数组指定元素 arr.splice(arr.findIndex(it ...
- 前端js部署
1 执行命令 cnpm run build 2.2 提取dist静态资源 将静态资源放置后端static下 /static文件是django后端的部署文件夹 3 Nginx写入配置文件 写入etc ...
- 一、eclipse配置TestNG
eclipse配置TestNG可以通过eclipse直接下载,但我没有vpn,所以使用线下配置. 1-下载TestNG的配置文件,有两个文件 features 和 plugins 2-eclipse配 ...
- mysql批量刷新用户密码
不知道用户密码,并且不改变用户密码的情况下,批量刷新MySQL数据库用户的密码 select concat('alter user \'',user,'\'@\'',host,'\' identifi ...
- oracle DG搭建
Oracle DG 搭建1. 环境 OS IP hostname db_name DB_UNIQUE_NAME主库 RHEL 5.4 192.168.12.20 edgzrip1.oracle.com ...
- vue插值 v-cloak
vue插值 v-cloak 使用VUE时,页面刷新时会出现闪动的现象(即在插值时会显示两侧的 {}) 先定义一个VUE 通过选择器在style中定义v-cloak的display值为none 再在元素 ...
- jenkins 配置任务
新建筑任务 ""imuke 建一个自由风格的 要执行python .py程序,我们需要把.py所在的目录设置进去 如果保存的是在svn,需要把他的地址放进去 如图: 设置自动构建时 ...
- Flink 反压 浅入浅出
前言 微信搜[Java3y]关注这个朴实无华的男人,点赞关注是对我最大的支持! 文本已收录至我的GitHub:https://github.com/ZhongFuCheng3y/3y,有300多篇原创 ...
- oracle rm -fr datafile 数据文件被误删的场景恢复(没有rman备份)
环境: Linux release 7.5 oracle19c (无pdb,从11.2.0.4升级上去的) 一:单个非系统表空间的数据文件被删除 我先备份一下,虽然是测试环境. [oracle@19c ...
- Python简单的验证码生成
用python生成简单的四位数验证码: 1 import random 2 3 if __name__ == "__main__": #这句话简单的理解就是,只有在本文件下以下的代 ...