1. 程式人生 > >ASP.NET Core應用的錯誤處理[1]:三種呈現錯誤頁面的方式

ASP.NET Core應用的錯誤處理[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中介軟體來提供。我們在呼叫另