ASP.NET Core应用错误处理之三种呈现错误页面的方式

前言

由于ASP.NET Core应用是一个同时处理多个请求的服务器应用,所以在处理某个请求过程中抛出的异常并不会导致整个应用的终止。出于安全方面的考量,为了避免敏感信息的外泄,客户端在默认的情况下并不会得到详细的出错信息,这无疑会在开发环境下增加查错纠错的难度。对于生产环境来说,我们也希望最终用户能够根据具体的错误类型得到具有针对性并且友好的错误消息。ASP.NET Core提供了相应的中间件帮助我们将定制化的错误信息呈现出来,这些中间件都定义在“Microsoft.AspNetCore.Diagnostics”这个NuGet包中。在着重介绍这些中间件之前,我们照理演示几个简单的实例让读者朋友们对这些中间件的作用有一个大概的了解。

一、显示开发者异常页面

一般情况下,如果ASP.NET Core在处理某个请求时出现异常,它一般会返回一个状态码为“500 Internal Server Error”的响应。为了避免一些敏感信息的外泄,详细的错误信息并不会随着响应发送给客户端,所以客户端只会得到一个很一般化的错误消息。以如下这个程序为例,服务端在处理每个请求时都会抛出一个类型为InvalidOperationException的异常。

public class Program
 {
 public static void Main()
 {
 new WebHostBuilder()
 .UseKestrel()
 .Configure(app => app.Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception..."))))
 .Build()
 .Run();
 }
 }

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

那么有人可能会觉得虽然浏览器上没有显示出任何详细的错误信息,也许它会隐藏在接收到的HTTP响应报文中。针对通过浏览器放出的这个请求,得到的响应内容如下所示,我们会发现响应报文根本没有主体部分,有限的几个报头也并没有承载任何与错误有关的信息。

 HTTP/1.1 500 Internal Server Error
 Date: Fri, 09 Dec 2016 23:42:18 GMT
 Content-Length: 0
 Server: Kestrel

由于应用并没有中断,浏览器上也并没有显示任何具有针对性的错误信息,开发人员在进行查错纠错的时候如何准确定位到作为错误根源的那一行代码呢?具体来说,我们又两种解决方案,一种就是利用日志,因为ASP.NET Core在进行请求处理时出现的任何错误都会被写入日志,所以我们可以通过注册相应的LoggerProvider(比如注册一个ConsoleLoggerProvider将日志直接写入宿主应用的控制台)到来获取写入的错误日志。

至于另一种解决方案,就是直接显示一个包含错误相应信息的错误页面,由于这个页面是在开发环境给开发者看的,所以我们将这个页面称为“开发者异常页面(Developer Exception Page)”。针对页面的自动呈现是利用一个名为DeveloperExceptionPageMiddleware的中间件来完成的,我们可以调用ApplicationBuilder的扩展方法UseDeveloperExceptionPage来注册这个中间件。

 public class Program
 {
 public static void Main()
 {
 new WebHostBuilder()
 .UseKestrel()
 .Configure(app => app
 .UseDeveloperExceptionPage()
 .Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception..."))))
 .Build()
 .Run();
 }
 }

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

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

通过DeveloperExceptionPageMiddleware中间件呈现的错误页面仅仅是供开发人员使用的,详细的错误信息往往会携带一些敏感的信息,所以务必记住只有在开发环境才能注册这个中间件,如下所示的代码片段体现了针对DeveloperExceptionPageMiddleware中间件正确的注册方式。

 new WebHostBuilder()
 .UseStartup<Startup>()
 …

 public class Startup
 {
 public void Configure(IApplicationBuilder app, IHostingEnvironment env)
 {
 if (env.IsDevelopment())
 {
 app.UseDeveloperExceptionPage();
 }
 }
 }

二、显示定制异常页面

DeveloperExceptionPageMiddleware中间件通过将异常详细信息和基于当前请求的内容直接呈现在错误页面中,这为开发人员的纠错诊断提供了极大的便利。但是在生产环境下,我们倾向于为最终的用户呈现一个定制的错误页面,而这可以通过注册另一个名为ExceptionHandlerMiddleware的中间件来实现。顾名思义,这个中间件旨在提供一个异常处理器(Exception Handler)来处理抛出的异常。实际上这个所谓的异常处理器就是一个类型为RequestDelegate的委托对象,ExceptionHandlerMiddleware中间件捕捉到抛出的异常后利用它来响应当前的请求。

还是以上面创建的这个总是会抛出一个 InvalidOperationException异常的应用为例。我们按照如下的形式调用ApplicationBuilder的扩展方法UseExceptionHandler注册了上述的这个ExceptionHandlerMiddleware中间件。这个扩展方法具有一个ExceptionHandlerOptions类型的参数,它的ExceptionHandler属性返回的就是这个作为异常处理器的RequestDelegate对象。

 public class Program
 {
 public static void Main()
 {
 RequestDelegate handler = async context => await context.Response.WriteAsync("Unhandled exception occurred!");

 new WebHostBuilder()
 .UseKestrel()
 .Configure(app => app.UseExceptionHandler(new ExceptionHandlerOptions { ExceptionHandler = handler})
 .Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception..."))))
 .Build()
 .Run();
 }
 }

如上面的代码片段所示,这个作为异常处理器的RequestDelegate仅仅是将一个简单的错误消息(“Unhandled exception occurred!”)作为响应的内容。当我们利用浏览器访问该应用的时候,这个定制的错误消息将会以如图4所示的形式直接呈现在浏览器上。

最终作为异常处理器的是一个类型为RequestDelegate的委托对象,而ApplicationBuilder具有创建这个委托对象的能力。具体来说,我们可以根据异常处理的需要将相应的中间件注册到某个ApplicationBuilder对象上,并最终利用这个ApplicationBuilder根据注册的中间件创建出作为异常处理器的RequestDelegate对象。 如果异常处理需要通过一个或者多个中间件来完成,我们可以按照如下的形式调用另一个UseExceptionHandler方法重载。这个方法的参数类型为Action<IApplicationBuilder>,我们调用它的Run方法注册了一个中间件来响应一个简单的错误消息。

 public class Program
 {
 public static void Main()
 {
 new WebHostBuilder()
 .UseKestrel()
 .Configure(app => app.UseExceptionHandler(builder=>builder.Run(async context => await context.Response.WriteAsync("Unhandled exception occurred!")))
 .Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception..."))))
 .Build()
 .Run();
 }
 }

上面这两种异常处理的形式都体现在提供一个RequestDelegate的委托对象来处理抛出的异常并完成最终的响应。如果应用已经设置了一个错误页面,并且这个错误页面具有一个固定的路径,那么我们在进行异常处理的时候就没有必要提供这个RequestDelegate对象,而只需要重定向到错误页面指向的路径即可。这种采用服务端重定向的异常处理方式可以采用如下的形式调用另一个UseExceptionHandler方法重载来完成,这个方法的参数表示的就是重定向的目标路径(“/error”),我们针对这个路径注册了一个路由来响应定制的错误消息。

 public class Program
 {
 public static void Main()
 {
 new WebHostBuilder()
 .UseKestrel()
 .ConfigureServices(svcs=>svcs.AddRouting())
 .Configure(app => app
  .UseExceptionHandler("/error")
  .UseRouter(builder=>builder.MapRoute("error", async context => await context.Response.WriteAsync("Unhandled exception occurred!")))
  .Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception..."))))
 .Build()
 .Run();
 }
 }

三、针对响应状态码定制错误页面

由于Web应用采用HTTP通信协议,所以我们应该尽可能低迎合HTTP标准并将定义在协议规范中的语义应用到应用中。对于异常或者错误的语义表达在HTTP协议层面主要体现在响应报文的状态码上,具体来说HTTP通信的错误大体分为如下两种类型:

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

正是因为响应状态码是对错误或者异常语义最重要的表达,所以在很多情况下我们需要针对不同的响应状态码来定制显示的错误信息。针对响应状态码对错误页面的定制可以借助一个类型为StatusCodePagesMiddleware的中间件来实现,我们可以调用ApplicationBuilder相应的扩展方法来注册这个中间件。

DeveloperExceptionPageMiddleware和ExceptionHandlerMiddleware中间件都是在后续请求处理过程中抛出异常的情况下才会被调用,而StatusCodePagesMiddleware被调用的前提是后续请求助理过程中产生一个错误响应状态码(范围在400~599之间)。如果仅仅希望显示一个统一的错误页面,我们可以按照如下的形式调用扩展方法UseStatusCodePages注册这个中间件,传入该方法的两个参数分别表示响应采用的媒体类型和主体内容。

 public class Program
 {
 public static void Main()
 {
 new WebHostBuilder()
 .UseKestrel()
 .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中间件最终会采用当前响应状态码来替换它。如果我们利用浏览器来访问这个应用,将会得到如下图所示的错误页面。

如果我们希望针对不同的错误状态码显示不同的错误页面,那么我们就需要将具体的请求处理逻辑实现在一个的状态码错误处理器中,并最终提供给StatusCodePagesMiddleware中间件。这个所谓的状态码错误处理器体现为一个类型为Func<StatusCodeContext, Task>的委托对象,作为输入的StatusCodeContext对象是对当前HttpContext的封装,同时承载着其他一些与错误处理相关的选项设置,我们将在本系列后续部分对这个类型进行详细介绍。

对于如下这个应用来说,它在处理任意一个请求是总是会随机地选择一个400~599之间的整数作为响应的状态码,所以客户端返回的响应内容总是通过注册的StatusCodePagesMiddleware中间件来提供。我们在调用另一个UseStatusCodePages方法重载的时候,为注册的中间件指定了一个Func<StatusCodeContext, Task>对象作为状态码错误处理器。

 public class Program
 {
 private static Random _random = new Random();

 public static void Main()
 {
 Func<StatusCodeContext, Task> handler = async 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})");
 }
 };
 new WebHostBuilder()
 .UseKestrel()
 .Configure(app => app
  .UseStatusCodePages(handler)
  .Run(context => Task.Run(() => context.Response.StatusCode = _random.Next(400,599))))
 .Build()
 .Run();
 }
 }

我们指定的状态码错误处理器在处理请求的时候,根据响应状态码将错误分成客户端错误和服务端错误两种类型,并选择针对性的错误消息作为响应内容。当我们利用浏览器访问这个应用的时候,显示的错误消息将由响应状态码来决定。

在ASP.NET Core的世界里,针对请求的处理总是体现为一个RequestDelegate对象。如果请求的处理需要借助一个或者多个中间件来完成,我们可以将它们注册到ApplicationBuilder对象上并利用它将中间件管道转换成一个RequestDelegate对象。用于注册StatusCodePagesMiddleware中间件的UseStatusCodePage方法还具有另一个重载,它允许我们采用这种方式来创建一个RequestDelegate对象来完成最终的请求处理工作,所以上面演示的这个应用完全可以改写成如下的形式。

 public class Program
 {
 private static Random _random = new Random();
 public static void Main()
 {
 RequestDelegate handler = async context =>
 {
 var response = context.Response;
 if (response.StatusCode < 500)
 {
  await response.WriteAsync($"Client error ({response.StatusCode})");
 }
 else
 {
  await response.WriteAsync($"Server error ({response.StatusCode})");
 }
 };
 new WebHostBuilder()
 .UseKestrel()
 .Configure(app => app
  .UseStatusCodePages(builder=>builder.Run(handler))
  .Run(context => Task.Run(() => context.Response.StatusCode = _random.Next(400, 599))))
 .Build()
 .Run();
 }
 }

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • ASP.NET Core应用错误处理之DeveloperExceptionPageMiddleware中间件呈现“开发者异常页面”

    前言 在<ASP.NET Core应用的错误处理[1]:三种呈现错误页面的方式>中,我们通过几个简单的实例演示了如何呈现一个错误页面,这些错误页面的呈现分别由三个对应的中间件来完成,接下来我们将对这三个中间件进行详细介绍.在开发环境呈现的异常页面是通过一个类型为DeveloperExceptionPageMiddleware中间件实现的. public class DeveloperExceptionPageMiddleware { public DeveloperExceptionPageM

  • 在ASP.NET Core中显示自定义的错误页面

    前言 相信每位程序员们应该都知道在 ASP.NET Core 中,默认情况下当发生500或404错误时,只返回http状态码,不返回任何内容,页面一片空白. 如果在 Startup.cs 的 Configure() 中加上 app.UseStatusCodePages(); ,500错误时依然是一片空白(不知为何对500错误不起作用),404错误时有所改观,页面会显示下面的文字: Status Code: 404; Not Found 如果我们想实现不管500还是404错误都显示自己定制的友好错

  • ASP.NET Core应用错误处理之StatusCodePagesMiddleware中间件针对响应码呈现错误页面

    前言 StatusCodePagesMiddleware中间件与ExceptionHandlerMiddleware中间件比较类似,它们都是在后续请求处理过程中"出错"的情况下利用一个错误处理器来完成最终的请求处理与响应的任务.它们之间的差异在于对"错误"的界定上,对于ExceptionHandlerMiddleware中间件来说,它所谓的错误就是抛出异常,但是对于StatusCodePagesMiddleware中间件来说,则将介于400~599之间的响应状态码视

  • ASP.NET Core应用错误处理之ExceptionHandlerMiddleware中间件呈现“定制化错误页面”

    前言 DeveloperExceptionPageMiddleware中间件利用呈现出来的错误页面实现抛出异常和当前请求的详细信息以辅助开发人员更好地进行纠错诊断工作,而ExceptionHandlerMiddleware中间件则是面向最终用户的,我们可以利用它来显示一个友好的定制化的错误页面.按照惯例,我们还是先来看看ExceptionHandlerMiddleware的类型定义. public class ExceptionHandlerMiddleware { public Excepti

  • ASP.NET Core异常和错误处理(8)

    在这一章,我们将讨论异常和错误处理.当 ASP.NET Core应用程序中发生错误时,您可以以各种不同的方式来处理.让我们来看看通过添加一个中间件来处理异常情况,这个中间件将帮助我们处理错误. 要模拟出错,让我们转到应用程序,运行,如果我们只是抛出异常的话,看看程序是如何运转转的. using Microsoft.AspNet.Builder; using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Http; using Microsoft

  • ASP.NET Core应用错误处理之三种呈现错误页面的方式

    前言 由于ASP.NET Core应用是一个同时处理多个请求的服务器应用,所以在处理某个请求过程中抛出的异常并不会导致整个应用的终止.出于安全方面的考量,为了避免敏感信息的外泄,客户端在默认的情况下并不会得到详细的出错信息,这无疑会在开发环境下增加查错纠错的难度.对于生产环境来说,我们也希望最终用户能够根据具体的错误类型得到具有针对性并且友好的错误消息.ASP.NET Core提供了相应的中间件帮助我们将定制化的错误信息呈现出来,这些中间件都定义在"Microsoft.AspNetCore.Di

  • ASP.NET Core设置URLs的五种方法

    目录 前言 URL格式 前提条件 方法1 使用环境变量 方法2 使用命令行参数 方法3 使用配置文件 方法4 使用UseUrls 方法5 使用Kestrel 优先级 总结 前言 在使用ASP.NET Core 3.1开发时,需要配置服务器监听的端口和协议,官方帮助文档进行简单说明,文档中提到了4种指定URL的方法 设置ASPNETCORE_URLS 环境变量: 使用dotnet --urls 命令行参数: 使用urls作为键进行配置: 使用UseUrls扩展方法: 为便于讲清楚URLs设置方法,

  • ASP.NET Core文件上传与下载实例(多种上传方式)

    前言 前段时间项目上线,实在太忙,最近终于开始可以研究研究ASP.NET Core了. 打算写个系列,但是还没想好目录,今天先来一篇,后面在整理吧. ASP.NET Core 2.0 发展到现在,已经很成熟了.下个项目争取使用吧. 正文 1.使用模型绑定上传文件(官方例子) 官方机器翻译的地址:https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads 这里吐槽一下 - -,这TM的机器翻译..还不如自己看E文的..

  • ASP.NET Core实现中间件的几种方式

    前言 ASP.NET Core 中 HTTP 管道使用中间件组合处理的方式, 换句人话来说, 对于写代码的人而言,一切皆中间件. 业务逻辑/数据访问/等等一切都需要以中间件的方式来呈现. 那么我们必须学会如何实现自定义中间件 这里划重点,必考 这里我们介绍下中间件的几种实现方式... 匿名函数 通常新建一个空的 ASP.NET Core Web Application,项目名字无所谓啦 在启动类里可以看到这么一句: // Startup.cs // ... app.Run(async (cont

  • 详解如何在ASP.NET Core Web API中以三种方式返回数据

    在 ASP.NET Core 中有三种返回 数据 和 HTTP状态码 的方式,最简单的就是直接返回指定的类型实例,如下代码所示: [ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { [HttpGet] public IEnumerable<WeatherForecast> Get() { var rng = new Random()

  • ASP.NET Core 2.2中的Endpoint路由详解

    Endpoint路由 在ASP.NET Core 2.2中,新增了一种路由,叫做 Endpoint (终结点)路由.本文将以往的路由系统称为 传统路由 . 本文通过源码的方式介绍传统路由和 Endpoint 路由部分核心功能和实现方法,具体功能上的差异见 官方文档 . 在升级到ASP.NET Core 2.2后,会自动启用 Endpoint 路由.如果要恢复以往的实现逻辑,需要加入以下代码: services.AddMvc(options => options.EnableEndpointRou

  • ASP.NET Core Mvc中空返回值的处理方法详解

    前言 如果你是一个初学者开始学习 ASP.NET 或 ASP.NET MVC, 你可能并不知道什么是. net Framework和. net ore.不用担心!我建议您看下官方文档https://docs.microsoft.com/zh-cn/aspnet/index , 您可以轻松地看到比较和差异. .NET Core MVC在如何返回操作结果方面非常灵活的. 你可以返回一个实现IActionResult接口的对象, 比如我们熟知的ViewResult, FileResult, Conte

  • 如何在Asp.Net Core MVC中处理null值的实现

    译文链接:https://www.infoworld.com/article/3434624/how-to-handle-null-values-in-aspnet-core-mvc.html 传统的 asp.net mvc 对应着 .netcore 中的 asp.net core mvc,可以利用 asp.net core mvc 去构建跨平台,可扩展,高性能的web应用和 api 接口. 程序员都有一些洁癖,很多时候我们都想很完美的包装一些错误信息,如一些返回空response的reques

  • ASP.NET Core自定义中间件如何读取Request.Body与Response.Body的内容详解

    背景# 最近在徒手造轮子,编写一个ASP.NET Core的日志监控器,其中用到了自定义中间件读取Request.Body和Response.Body的内容,但是编写过程,并不像想象中的一帆风顺,ASP.NET Core针对Request.Body和Response.Body的几个特殊设计,导致了完成以上功能需要绕一些弯路. 原始代码# 为了读取Request.Body和Response.Body的内容,我的实现思路如下: 创建一个LoggerMiddleware的中间件,将它放置在项目中间件管

随机推荐