ASP.NET Core應用錯誤處理之三種呈現錯誤頁面的方式

發布時間:2019-05-29    來源:腳本之家    瀏覽:1276次

前言

由于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();
 }
 }

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。

上一篇:ASP.NET Core應用錯誤處理之三種呈現錯誤頁面的方式 下一篇:ASP.NET Core應用錯誤處理之三種呈現錯誤頁面的方式
天津快乐十分走势