ASP.NET Core应用的错误处理[1]:三种呈现错误页面的方式

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介:

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

目录
一、显示开发者异常页面
二、显示定制异常页面
三、针对响应状态码定制错误页面

一、显示开发者异常页面

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

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .Configure(app => app.Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception..."))))
   8:             .Build()
   9:             .Run();
  10:     }
  11: }

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

1

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

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

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

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

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .Configure(app => app
   8:                 .UseDeveloperExceptionPage()
   9:                 .Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception..."))))
  10:             .Build()
  11:             .Run();
  12: }
  13: }

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

2

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

3

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

   1: new WebHostBuilder()
   2:     .UseStartup<Startup>()
   3:
   4:  
   5: public class Startup
   6: {
   7:     public void Configure(IApplicationBuilder app, IHostingEnvironment env)
   8:     {
   9:         if (env.IsDevelopment())
  10:         {
  11:             app.UseDeveloperExceptionPage();
  12:         }
  13:     }
  14: }

二、显示定制异常页面

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

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

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         RequestDelegate handler = async context => await context.Response.WriteAsync("Unhandled exception occurred!");
   6:  
   7:         new WebHostBuilder()
   8:             .UseKestrel()
   9:             .Configure(app => app.UseExceptionHandler(new ExceptionHandlerOptions { ExceptionHandler = handler})
  10:             .Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception..."))))
  11:             .Build()
  12:             .Run();
  13:     }
  14: }

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

4

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

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {        
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .Configure(app => app.UseExceptionHandler(builder=>builder.Run(async context => await context.Response.WriteAsync("Unhandled exception occurred!")))
   8:             .Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception..."))))
   9:         .Build()
  10:         .Run();
  11:     }
  12: }

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

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .ConfigureServices(svcs=>svcs.AddRouting())
   8:             .Configure(app => app
   9:                 .UseExceptionHandler("/error")
  10:                 .UseRouter(builder=>builder.MapRoute("error", async context => await context.Response.WriteAsync("Unhandled exception occurred!")))
  11:                 .Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception..."))))
  12:         .Build()
  13:         .Run();
  14:     }
  15: }

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

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

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

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

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

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {        
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .Configure(app=>app
   8:                 .UseStatusCodePages("text/plain", "Error occurred ({0})")
   9:                 .Run(context=> Task.Run(()=>context.Response.StatusCode = 500)))
  10:         .Build()
  11:         .Run();
  12:     }
  13: }

如上面的代码片段所示,应用在处理请求的时候总是会将响应状态码设置为500,所以最终的响应内容将由注册的StatusCodePagesMiddleware中间件来提供。我们调用UseStatusCodePages方法的时候将响应的媒体类型设置为“text/plain”,并将一段简单的错误消息作为了响应的主体内容。值得一提的时候,作为响应内容的字符串可以包含一个占位符({0}),StatusCodePagesMiddleware中间件最终会采用当前响应状态码来替换它。如果我们利用浏览器来访问这个应用,将会得到如下图所示的错误页面。

5

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

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

   1: public class Program
   2: {
   3: private static Random _random = new Random();
   4:  
   5:     public static void Main()
   6:     {
   7:         Func<StatusCodeContext, Task> handler = async context => {
   8:             var response = context.HttpContext.Response;
   9:             if (response.StatusCode < 500)
  10:             {
  11:                 await response.WriteAsync($"Client error ({response.StatusCode})");
  12:             }
  13:             else
  14:             {
  15:                 await response.WriteAsync($"Server error ({response.StatusCode})");
  16:             }
  17:         };
  18:         new WebHostBuilder()
  19:             .UseKestrel()
  20:             .Configure(app => app
  21:                 .UseStatusCodePages(handler)
  22:                 .Run(context => Task.Run(() => context.Response.StatusCode = _random.Next(400,599))))
  23:             .Build()
  24:             .Run();
  25:     }
  26: }

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

6

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

   1: public class Program
   2: {
   3:     private static Random _random = new Random();
   4:     public static void Main()
   5:     {
   6:         RequestDelegate handler = async context =>
   7:         {
   8:             var response = context.Response;
   9:             if (response.StatusCode < 500)
  10:             {
  11:                 await response.WriteAsync($"Client error ({response.StatusCode})");
  12:             }
  13:             else
  14:             {
  15:                 await response.WriteAsync($"Server error ({response.StatusCode})");
  16:             }
  17:         };
  18:         new WebHostBuilder()
  19:             .UseKestrel()
  20:             .Configure(app => app
  21:                 .UseStatusCodePages(builder=>builder.Run(handler))
  22:                 .Run(context => Task.Run(() => context.Response.StatusCode = _random.Next(400, 599))))
  23:             .Build()
  24:             .Run();
  25:     }
  26: }

 


ASP.NET Core应用的错误处理[1]:三种呈现错误页面的方式
ASP.NET Core应用的错误处理[2]:DeveloperExceptionPageMiddleware中间件
ASP.NET Core应用的错误处理[3]:ExceptionHandlerMiddleware中间件
ASP.NET Core应用的错误处理[4]:StatusCodePagesMiddleware中间件

作者:蒋金楠
微信公众账号:大内老A
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
22天前
|
存储 Shell Linux
快速上手基于 BaGet 的脚本自动化构建 .net 应用打包
本文介绍了如何使用脚本自动化构建 `.net` 应用的 `nuget` 包并推送到指定服务仓库。首先概述了 `BaGet`——一个开源、轻量级且高性能的 `NuGet` 服务器,支持多种存储后端及配置选项。接着详细描述了 `BaGet` 的安装、配置及使用方法,并提供了 `PowerShell` 和 `Bash` 脚本实例,用于自动化推送 `.nupkg` 文件。最后总结了 `BaGet` 的优势及其在实际部署中的便捷性。
58 10
|
11天前
|
数据采集 JSON API
.NET 3.5 中 HttpWebRequest 的核心用法及应用
【9月更文挑战第7天】在.NET 3.5环境下,HttpWebRequest 类是处理HTTP请求的一个核心组件,它封装了HTTP协议的细节,使得开发者可以方便地发送HTTP请求并接收响应。本文将详细介绍HttpWebRequest的核心用法及其实战应用。
47 6
|
22天前
|
开发框架 NoSQL .NET
利用分布式锁在ASP.NET Core中实现防抖
【9月更文挑战第5天】在 ASP.NET Core 中,可通过分布式锁实现防抖功能,仅处理连续相同请求中的首个请求,其余请求返回 204 No Content,直至锁释放。具体步骤包括:安装分布式锁库如 `StackExchange.Redis`;创建分布式锁服务接口及其实现;构建防抖中间件;并在 `Startup.cs` 中注册相关服务和中间件。这一机制有效避免了短时间内重复操作的问题。
|
1月前
|
Linux iOS开发 开发者
跨平台开发不再难:.NET Core如何让你的应用在Windows、Linux、macOS上自如游走?
【8月更文挑战第28天】本文提供了一份详尽的.NET跨平台开发指南,涵盖.NET Core简介、环境配置、项目结构、代码编写、依赖管理、构建与测试、部署及容器化等多个方面,帮助开发者掌握关键技术与最佳实践,充分利用.NET Core实现高效、便捷的跨平台应用开发与部署。
60 3
|
1月前
|
前端开发 JavaScript 开发工具
跨域联姻:React.NET——.NET应用与React的完美融合,解锁前后端高效协作新姿势。
【8月更文挑战第28天】探索React.NET,这是将热门前端框架React与强大的.NET后端无缝集成的创新方案。React以其组件化和虚拟DOM技术著称,能构建高性能、可维护的用户界面;.NET则擅长企业级应用开发。React.NET作为桥梁,使.NET应用轻松采用React构建前端,并优化开发流程与性能。通过直接托管React组件,.NET应用简化了部署流程,同时支持服务器端渲染(SSR),提升首屏加载速度与SEO优化。
26 1
|
1月前
|
存储 缓存 安全
.NET 在金融行业的应用:高并发交易系统的构建与优化之路
【8月更文挑战第28天】在金融行业,交易系统需具备高并发处理、低延迟及高稳定性和安全性。利用.NET构建此类系统时,可采用异步编程提升并发能力,优化数据库访问以降低延迟,使用缓存减少数据库访问频率,借助分布式事务确保数据一致性,并加强安全性措施。通过综合优化,满足金融行业的严苛要求。
30 1
|
28天前
|
数据库 C# 开发者
WPF开发者必读:揭秘ADO.NET与Entity Framework数据库交互秘籍,轻松实现企业级应用!
【8月更文挑战第31天】在现代软件开发中,WPF 与数据库的交互对于构建企业级应用至关重要。本文介绍了如何利用 ADO.NET 和 Entity Framework 在 WPF 应用中访问和操作数据库。ADO.NET 是 .NET Framework 中用于访问各类数据库(如 SQL Server、MySQL 等)的类库;Entity Framework 则是一种 ORM 框架,支持面向对象的数据操作。文章通过示例展示了如何在 WPF 应用中集成这两种技术,提高开发效率。
39 0
|
28天前
|
开发者 API Windows
从怀旧到革新:看WinForms如何在保持向后兼容性的前提下,借助.NET新平台的力量实现自我进化与应用现代化,让经典桌面应用焕发第二春——我们的WinForms应用转型之路深度剖析
【8月更文挑战第31天】在Windows桌面应用开发中,Windows Forms(WinForms)依然是许多开发者的首选。尽管.NET Framework已演进至.NET 5 及更高版本,WinForms 仍作为核心组件保留,支持现有代码库的同时引入新特性。开发者可将项目迁移至.NET Core,享受性能提升和跨平台能力。迁移时需注意API变更,确保应用平稳过渡。通过自定义样式或第三方控件库,还可增强视觉效果。结合.NET新功能,WinForms 应用不仅能延续既有投资,还能焕发新生。 示例代码展示了如何在.NET Core中创建包含按钮和标签的基本窗口,实现简单的用户交互。
50 0
|
28天前
|
Java Spring 自然语言处理
Spring 框架里竟藏着神秘魔法?国际化与本地化的奇妙之旅等你来揭开谜底!
【8月更文挑战第31天】在软件开发中,国际化(I18N)与本地化(L10N)对于满足不同地区用户需求至关重要。Spring框架提供了强大支持,利用资源文件和`MessageSource`实现多语言文本管理。通过配置日期格式和货币符号,进一步完善本地化功能。合理应用这些特性,可显著提升应用的多地区适应性和用户体验。
31 0
|
28天前
|
开发者 C# Android开发
Xamarin 与 .NET:解锁现代化移动应用开发的超级武器——深入探讨C#与.NET框架如何赋能跨平台应用,实现高效编码与卓越性能
【8月更文挑战第31天】Xamarin 与 .NET 的结合为开发者提供了强大的平台,用于构建现代化移动应用。通过 C# 和 .NET 框架,Xamarin 可以实现一次编写、多平台运行,覆盖 iOS、Android 和 Windows。这种方式不仅节省了开发时间和成本,还保证了应用的一致性和高质量。Xamarin 是一个开源框架,专为跨平台移动应用开发设计,允许使用 C# 语言和 .NET 核心库构建原生应用,并访问各平台特定功能。微软维护的 Xamarin 是 Visual Studio 生态系统的一部分,极大地提高了开发效率。
59 0