ASP.NET Core錯誤處理中介軟體[1]: 呈現錯誤資訊
NuGet包“Microsoft.AspNetCore.Diagnostics”中提供了幾個與異常處理相關的中介軟體。當ASP.NET Core應用在處理請求過程中出現錯誤時,我們可以利用它們將原生的或者定製的錯誤資訊作為響應內容傳送給客戶端。在著重介紹這些中介軟體之前,下面先演示幾個簡單的例項,從而使讀者大致瞭解這些中介軟體的作用。[更多關於ASP.NET Core的文章請點這裡]
一、顯示開發者異常頁面
如果ASP.NET Core應用在處理某個請求時出現異常,它一般會返回一個狀態碼為“500 Internal Server Error”的響應。為了避免一些敏感資訊的外洩,詳細的錯誤資訊並不會隨著響應傳送給客戶端,所以客戶端只會得到一個很泛化的錯誤訊息。以如下所示的程式為例,它處理每個請求時都會丟擲一個InvalidOperationException型別的異常。
public class Program { public static void Main() { Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(builder => builder.Configure(app => app.Run( context=> Task.FromException(new InvalidOperationException("Manually thrown exception..."))))) .Build() .Run(); } }
利用瀏覽器訪問這個應用總是會得到下圖所示的錯誤頁面。可以看出,這個頁面僅僅告訴我們目標應用當前無法正常處理本次請求,除了提供的響應狀態碼(“HTTP ERROR 500”),它並沒有提供任何有益於糾錯的輔助資訊。
有人認為瀏覽器上雖然沒有顯示任何詳細的錯誤資訊,但這並不意味著HTTP響應報文中也沒有攜帶任何詳細的出錯資訊。實際上,針對通過瀏覽器發出的這個請求,服務端會返回如下這段HTTP響應報文。我們會發現響應報文根本沒有主體部分,有限的幾個報頭也並沒有承載任何與錯誤有關的資訊。
HTTP/1.1 500 Internal Server Error Date: Wed, 18 Sep 2019 23:38:59 GMT Content-Length: 0 Server: Kestrel
由於應用並沒有中斷,瀏覽器上也並沒有顯示任何具有針對性的錯誤資訊,開發人員在進行查錯和糾錯時如何準確定位到作為錯誤根源的那一行程式碼?這個問題有兩種解決方案:一種是利用日誌,因為ASP.NET Core應用在進行請求處理時出現的任何錯誤都會被寫入日誌,所以可以通過註冊相應的ILoggerProvider物件來獲取寫入的錯誤日誌,如可以註冊一個ConsoleLoggerProvider物件將日誌直接輸出到宿主應用的控制檯上。
另一種解決方案就是直接顯示一個錯誤頁面,由於這個頁面只是在開發環境給開發人員看的,所以可以將這個頁面稱為開發者異常頁面(Developer Exception Page)。開發者異常頁面的呈現是利用一個名為DeveloperExceptionPageMiddleware的中介軟體完成的,我們可以採用如下所示的方式呼叫IApplicationBuilder介面的UseDeveloperExceptionPage擴充套件方法來註冊這個中介軟體。
public class Program { public static void Main() { Host.CreateDefaultBuilder() .ConfigureServices(svcs => svcs.AddRouting()) .ConfigureWebHostDefaults(builder => builder.Configure(app => app .UseDeveloperExceptionPage() .UseRouting() .UseEndpoints(endpoints => endpoints.MapGet("{foo}/{bar}", HandleAsync)))) .Build() .Run(); static Task HandleAsync(HttpContext httpContext) => Task.FromException(new InvalidOperationException("Manually thrown exception...")); } }
一旦註冊了DeveloperExceptionPageMiddleware中介軟體,ASP.NET Core應用在處理請求過程中出現的異常資訊就會以下圖所示的形式直接出現在瀏覽器上,我們可以在這個頁面中看到幾乎所有的錯誤資訊,包括異常的型別、訊息和堆疊資訊等。
開發者異常頁面除了顯示與丟擲的異常相關的資訊,還會以圖16-3所示的形式顯示與當前請求上下文相關的資訊,其中包括當前請求URL攜帶的所有查詢字串、所有請求報頭、Cookie的內容和路由資訊(終結點和路由引數)。如此詳盡的資訊無疑會極大地幫助開發人員儘快找出錯誤的根源。
通過DeveloperExceptionPageMiddleware中介軟體呈現的錯誤頁面僅僅是供開發人員使用的,頁面上往往會攜帶一些敏感的資訊,所以只有在開發環境才能註冊這個中介軟體,如下所示的程式碼片段體現了Startup型別中針對DeveloperExceptionPageMiddleware中介軟體正確的註冊方式。
public class Startup { public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } } }
二、顯示定製異常頁面
DeveloperExceptionPageMiddleware中介軟體會將異常詳細資訊和基於當前請求的上下文直接呈現在錯誤頁面中,這為開發人員的糾錯診斷提供了極大的便利。但是在生產環境下,我們傾向於為最終的使用者呈現一個定製的錯誤頁面,這可以通過註冊另一個名為ExceptionHandlerMiddleware的中介軟體來實現。顧名思義,這個中介軟體旨在提供一個異常處理器(ExceptionHandler)來處理丟擲的異常。實際上,這個所謂的異常處理器就是一個RequestDelegate物件,ExceptionHandlerMiddleware中介軟體捕捉到丟擲的異常後利用它來處理當前的請求。
下面以上面建立的這個總是會丟擲一個 InvalidOperationException異常的應用為例進行介紹。我們按照如下形式呼叫IApplicationBuilder介面的UseExceptionHandler擴充套件方法註冊了ExceptionHandlerMiddleware中介軟體。這個擴充套件方法具有一個ExceptionHandlerOptions型別的引數,它的ExceptionHandler屬性返回的就是這個作為異常處理器的RequestDelegate物件。
public class Program { public static void Main() { var options = new ExceptionHandlerOptions { ExceptionHandler = HandleAsync }; Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(builder => builder.Configure(app => app .UseExceptionHandler(options) .Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception..."))))) .Build() .Run(); static Task HandleAsync(HttpContext context) => context.Response.WriteAsync("Unhandled exception occurred!"); }
如上面的程式碼片段所示,這個作為異常處理器的RequestDelegate物件僅僅是將一個簡單的錯誤訊息(Unhandled exception occurred!)作為響應的內容。當我們利用瀏覽器訪問該應用時,這個定製的錯誤訊息會以下圖所示的形式直接呈現在瀏覽器上。
由於最終作為異常處理器的是一個RequestDelegate物件,而IApplicationBuilder物件具有根據註冊的中介軟體來建立這個委託物件的能力,所以我們可以根據異常處理的需求將相應的中介軟體註冊到某個IApplicationBuilder物件上,並最終利用它來建立作為異常處理器的RequestDelegate物件。如果異常處理需要通過一個或者多箇中間件來完成,我們可以按照如下所示的形式呼叫另一個UseExceptionHandler方法過載。這個方法的引數型別為Action<IApplicationBuilder>,我們呼叫它的Run方法註冊了一箇中間件來響應一個簡單的錯誤訊息。
public class Program { public static void Main() { Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(builder => builder.Configure(app => app .UseExceptionHandler(app2 => app2.Run(HandleAsync)) .Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception..."))))) .Build() .Run(); static Task HandleAsync(HttpContext context) => context.Response.WriteAsync("Unhandled exception occurred!"); } }
上面這兩種異常處理的形式都體現在提供一個RequestDelegate的委託物件來處理丟擲的異常並完成最終的響應。如果應用已經設定了一個錯誤頁面,並且這個錯誤頁面有一個固定的路徑,那麼我們在進行異常處理的時候就沒有必要提供這個RequestDelegate物件,只需要重定向到錯誤頁面指向的路徑即可。這種採用服務端重定向的異常處理方式可以採用如下所示的形式呼叫另一個UseExceptionHandler方法過載來完成,這個方法的引數表示的就是重定向的目標路徑(“/error”),我們針對這個路徑註冊了一個路由來響應定製的錯誤訊息。
public class Program { public static void Main() { Host.CreateDefaultBuilder() .ConfigureServices(svcs => svcs.AddRouting()) .ConfigureWebHostDefaults(builder => builder.Configure(app => app .UseExceptionHandler("/error") .UseRouting() .UseEndpoints(endpoints => endpoints.MapGet("error", HandleAsync)) .Run(context => Task.FromException(new InvalidOperationException("Manually thrown exception..."))))) .Build() .Run(); static Task HandleAsync(HttpContext context) => context.Response.WriteAsync("Unhandled exception occurred!"); } }
三、針對響應狀態碼定製錯誤頁面
由於Web應用採用HTTP通訊協議,所以我們應該儘可能迎合HTTP標準,並將定義在協議規範中的語義應用到程式中。異常或者錯誤的語義表達在HTTP協議層面主要體現在響應報文的狀態碼上,具體來說,HTTP通訊的錯誤大體分為如下兩種型別。
- 客戶端錯誤:表示因客戶端提供不正確的請求資訊而導致伺服器不能正常處理請求,響應狀態碼的範圍為400~499。
- 服務端錯誤:表示伺服器在處理請求過程中因自身的問題而發生錯誤,響應狀態碼的範圍為500~599。
正是因為響應狀態碼是對錯誤或者異常語義最重要的表達,所以在很多情況下我們需要針對不同的響應狀態碼來定製顯示的錯誤資訊。針對響應狀態碼對錯誤頁面的定製可以藉助一個StatusCodePagesMiddleware型別的中介軟體來實現,我們可以呼叫IApplicationBuilder介面相應的擴充套件方法來註冊這個中介軟體。
DeveloperExceptionPageMiddleware中介軟體和ExceptionHandlerMiddleware中介軟體都是在後續請求處理過程中丟擲異常的情況下才會被呼叫的,而StatusCodePagesMiddleware中介軟體被呼叫的前提是後續請求處理過程中產生一個錯誤的響應狀態碼(範圍為400~599)。如果僅僅希望顯示一個統一的錯誤頁面,我們可以按照如下所示的形式呼叫IApplicationBuilder介面的UseStatusCodePages擴充套件方法註冊這個中介軟體,傳入該方法的兩個引數分別表示響應採用的媒體型別和主體內容。
public class Program { public static void Main() { Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(webBuilder => webBuilder.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中介軟體最終會採用當前響應狀態碼來替換它。如果我們利用瀏覽器來訪問這個應用,得到的錯誤頁面如下圖16-5所示。
如果我們希望針對不同的錯誤狀態碼顯示不同的錯誤頁面,那麼就需要將具體的請求處理邏輯實現在一個狀態碼錯誤處理器中,並最終提供給StatusCodePagesMiddleware中介軟體。這個所謂的狀態碼錯誤處理器體現為一個Func<StatusCodeContext, Task>型別的委託物件,作為輸入的StatusCodeContext物件是對HttpContext上下文的封裝,它同時承載著其他一些與錯誤處理相關的選項設定,我們將在本章後續部分對這個型別進行詳細介紹。
對於如下所示的應用來說,它在處理任意一個請求時總是隨機選擇400~599的一個整數來作為響應的狀態碼,所以客戶端返回的響應內容總是通過註冊的StatusCodePagesMiddleware中介軟體來提供。在呼叫另一個UseStatusCodePages方法過載時,我們為註冊的中介軟體指定一個Func<StatusCodeContext, Task>物件作為狀態碼錯誤處理器。
public class Program { private static readonly Random _random = new Random(); public static void Main() { Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(webBuilder => webBuilder.Configure(app => app .UseStatusCodePages(HandleAsync) .Run(context => Task.Run(() => context.Response.StatusCode = _random.Next(400, 599))))) .Build() .Run(); static async Task HandleAsync(StatusCodeContext 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})"); } } } }
我們指定的狀態碼錯誤處理器在處理請求時,根據響應狀態碼將錯誤分為客戶端錯誤和服務端錯誤兩種型別,並選擇針對性的錯誤訊息作為響應內容。當我們利用瀏覽器訪問這個應用的時候,顯示的錯誤訊息將以下圖所示的形式由響應狀態碼來決定。
在ASP.NET Core的世界裡,針對請求的處理總是體現為一個RequestDelegate物件。如果請求的處理需要藉助一個或者多箇中間件來完成,就可以將它們註冊到IApplicationBuilder物件上,並利用該物件將中介軟體管道轉換成一個RequestDelegate物件。用於註冊StatusCodePagesMiddleware中介軟體的UseStatusCodePages方法還有另一個過載,它允許我們採用這種方式來建立一個RequestDelegate物件來完成錯誤請求處理工作,所以上面演示的這個應用完全可以改寫成如下形式。
public class Program { private static readonly Random _random = new Random(); public static void Main() { Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(webBuilder => webBuilder.Configure(app => app .UseStatusCodePages(app2 => app2.Run(HandleAsync)) .Run(context => Task.Run(() => context.Response.StatusCode = _random.Next(400, 599))))) .Build() .Run(); static async Task HandleAsync(HttpContext context) { var response = context.Response; if (response.StatusCode < 500) { await response.WriteAsync($"Client error ({response.StatusCode})"); } else { await response.WriteAsync($"Server error ({response.StatusCode})"); } } } }
ASP.NET Core錯誤處理中介軟體[1]: 呈現錯誤資訊
ASP.NET Core錯誤處理中介軟體[2]: 開發者異常頁面
ASP.NET Core錯誤處理中介軟體[3]: 異常處理器
ASP.NET Core錯誤處理中介軟體[4]: 響應狀態