天天看点

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

注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录

开发人员异常页用于显示未处理的请求异常的详细信息。当我们通过ASP.NET Core模板创建一个项目时,<code>Startup.Configure</code>方法中会自动生成以下代码:

需要注意的是,与“异常处理”有关的中间件,一定要尽早添加,这样,它可以最大限度的捕获后续中间件抛出的未处理异常。

可以看到,当程序运行在开发环境中时,才会启用开发人员异常页,这很好理解,因为在生产环境中,我们不能将异常的详细信息暴露给用户,否则,这将会导致一系列安全问题。

现在我们在下方添加如下代码抛出一个异常:

当开发人员异常页中间件捕获了该未处理异常时,会展示类似如下的相关信息:

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

该异常页面展示了如下信息:

异常消息

异常堆栈追踪(Stack)

HTTP请求查询参数(Query)

Cookies

HTTP请求标头(Headers)

路由(Routing),包含了终结点和路由信息

当你查看<code>DeveloperExceptionPageMiddleware</code>的源码时,你会在构造函数中发现一个入参,类型为<code>IEnumerable&lt;IDeveloperPageExceptionFilter&gt;</code>。通过这个Filter集合,组成一个错误处理器管道,按照先注册先执行的原则,顺序进行错误处理。

下面是<code>DeveloperExceptionPageMiddleware</code>的核心源码:

这也就说明,如果我们想要自定义开发者异常页,那我们可以通过实现<code>IDeveloperPageExceptionFilter</code>接口来达到目的。

先看一下<code>IDeveloperPageExceptionFilter</code>接口定义:

<code>HandleExceptionAsync</code>方法除了错误上下文信息外,还包含了一个<code>Func&lt;ErrorContext, Task&gt; next</code>,这是干嘛的呢?其实,前面我们已经提到了,<code>IDeveloperPageExceptionFilter</code>的所有实现,会组成一个管道,当错误需要在管道中的后续处理器作进一步处理时,就是通过这个<code>next</code>传递错误的,所以,当需要传递错误时,一定要记得调用<code>next</code>!

不废话了,赶紧实现一个看看效果吧:

当抛出一个异常,你会看到类似如下的页面:

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

上面介绍了开发环境中的异常处理,现在我们来看一下生产环境中的异常处理,通过调用<code>UseExceptionHandler</code>扩展方法注册中间件<code>ExceptionHandlerMiddleware</code>。

该异常处理程序:

可以捕获后续中间件未处理的异常

若无异常或HTTP响应已经启动(<code>Response.HasStarted == true</code>),则不做任何处理

不会改变URL中的路径

默认情况下,会生成类似如下的模板:

我们可以通过lambda向<code>UseExceptionHandler</code>中提供一个异常处理逻辑:

可以看到,当捕获到异常时,可以通过<code>HttpContext.Features</code>,并指定类型<code>IExceptionHandlerPathFeature</code>或<code>IExceptionHandlerFeature</code>(前者继承自后者),来获取到异常信息。

再提醒一遍,千万不要将敏感的错误信息暴露给客户端。

除了使用lambda外,我们还可以指定一个路径,指向一个备用管道进行异常处理,这个备用管道对于MVC来说,一般是Controller中的Action,例如MVC模板默认的<code>/Home/Error</code>:

当捕获到异常时,你会看到类似如下的页面:

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

你可以在Action<code>Error</code>中自定义错误处理逻辑,就像lambda一样。

需要注意的是,不要随意对<code>Error</code>添加<code>[HttpGet]</code>、<code>[HttpPost]</code>等限定Http请求方法的特性。一旦你加上了<code>[HttpGet]</code>,那么该方法只能处理<code>Get</code>请求的异常。

不过,如果你就是打算将不同方法的Http请求分别进行处理,你可以类似如下进行处理:

另外,还需要提醒一下,如果在请求备用管道(如示例中的<code>Error</code>)时也报错了,无论是Http请求管道中的中间件报错,还是<code>Error</code>里面报错,此时<code>ExceptionHandlerMiddleware</code>均会重新引发原始异常,而不是向外抛出备用管道的异常。

一般异常处理程序页是面向所有用户的,所以请保证它可以匿名访问。

下面一块看一下<code>ExceptionHandlerMiddleware</code>吧:

默认情况下,当ASP.NET Core遇到没有正文的400-599Http错误状态码时,不会为其提供页面,而是返回状态码和空响应正文。可是,为了良好的用户体验,一般我们会对常见的错误状态码(404)提供友好的页面,如gitee404

请注意,本节所涉及到的中间件与上两节所讲解的错误异常处理中间件不冲突,可以同时使用。确切的说,本节并不是处理异常,只是为了提升用户体验。

我们可以通过<code>StatusCodePagesMiddleware</code>中间件实现该功能:

注意,一定要在异常处理中间件之后,请求处理中间件之前调用<code>UseStatusCodePages</code>。

现在,你可以请求一个不存在的路径,例如<code>Home/Index2</code>,你会在浏览器中看到如下输出:

<code>UseStatusCodePages</code>也提供了重载,允许我们自定义响应内容类型和正文内容,如:

浏览器输出为:

同样地,我们也可以通过向<code>UseStatusCodePages</code>传入lambda表达式进行处理:

介绍了那么多,你也看到了,事实上<code>UseStatusCodePages</code>效果并不好,所以我们在生产环境一般是不会用这玩意的,那用啥呢?请随我继续往下看。

该扩展方法,内部实际上是通过调用<code>UseStatusCodePages</code>并传入lambda进行实现的,该方法:

接收一个Http资源定位字符串。同样的,会有一个占位符<code>{0}</code>,用于填充Http状态码

向客户端发送Http状态码302-已找到

然后将客户端重定向到指定的终结点,在该终结点中,可以针对不同错误状态码分别进行处理

现在你可以自己试一下。

不知道你有没有注意:当我们请求一个不存在的路径时,它的确会跳转到404页面,但是,Url也变了,变成了<code>/Home/StatusCodeError?code=404</code>,而且,响应状态码也变了,变成了<code>200Ok</code>。可以通过源码看一下咋回事(我相信,大家看到302其实也都明白了):

如果你不想更改原始请求的Url,而且保留原始状态码,那么你应该使用接下来要介绍的<code>UseStatusCodePagesWithReExecute</code>。

同样的,该扩展方法,内部也是通过调用<code>UseStatusCodePages</code>并传入lambda进行实现的,不过该方法:

接收1个路径字符串和和1个查询字符串。同样的,会有一个占位符<code>{0}</code>,用于填充Http状态码

Url保持不变,并向客户端返回原始Http状态码

执行备用管道,用于生成响应正文

具体例子就不再列举了,用上面的就行了。现在来看看源码:

在MVC中,你可以通过给控制器或其中的Action方法添加<code>[SkipStatusCodePages]</code>特性,可以略过<code>StatusCodePagesMiddleware</code>。

除了错误处理中间件外,ASP.NET Core 还提供了异常过滤器,用于错误处理。

异常过滤器:

通过实现接口<code>IExceptionFilter</code>或<code>IAsyncExceptionFilter</code>来自定义异常过滤器

可以捕获Controller创建时(也就是只捕获构造函数中抛出的异常)、模型绑定、Action Filter和Action中抛出的未处理异常

其他地方抛出的异常不会捕获

本节仅介绍异常过滤器,有关过滤器的详细内容,后续文章将会介绍

先来看一下这两个接口:

<code>OnException</code>和<code>OnExceptionAsync</code>方法都包含一个类型为<code>ExceptionContext</code>参数,很显然,它就是与异常有关的上下文,我们的异常处理逻辑离不开它。那接着来看一下它的结构吧:

除此之外,<code>ExceptionContext</code>还继承了<code>FilterContext</code>,而<code>FilterContext</code>又继承了<code>ActionContext</code>(这也从侧面说明,过滤器是为Action服务的),也就是说我们也能够获取到一些过滤器和Action相关的信息,看看都有什么吧:

更多参数细节,我会在专门讲过滤器的文章中详细介绍。

下面,我们就来实现一个自定义的异常处理器:

接着,找到<code>/Views/Shared/Error.cshtml</code>,展示一下错误消息:

最后,将服务<code>MyExceptionFilterAttribute</code>注册到DI容器:

现在,我们将该异常处理器加在<code>/Home/Index</code>上,并抛个异常:

当请求<code>/Home/Index</code>时,你会得到如下页面:

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

现在,我们已经介绍了两种错误处理的方法——错误处理中间件和异常过滤器。现在来比较一下它们的异同,以及我们何时应该选择哪种处理方式。

错误处理中间件:

可以捕获后续中间件的所有未处理异常

拥有<code>RequestDelegate</code>,操作更加灵活

粒度较粗,仅可针对全局进行配置

错误处理中间件适合用于处理全局异常。

仅可捕获Controller创建时(也就是构造函数中抛出的异常)、模型绑定、Action Filter和Action中抛出的未处理异常,其他地方抛出的异常捕获不到

粒度更小,可以灵活针对Controller或Action配置不同的异常过滤器

异常过滤器非常适合用于捕获并处理Action中的异常。

在我们的应用中,可以同时使用错误处理中间件和异常过滤器,只有充分发挥它们各自的优势,才能处理好程序中的错误。