標籤:spn 目標 develop cookie 工作 ram 一行代碼 封裝 web應用
三種呈現錯誤頁面的方式
由於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”)之外,它並沒有提供任何有益於差錯錯誤修正的錯誤資訊。
那麼有人可能會覺得雖然瀏覽器上沒有顯示出任何詳細的錯誤資訊,也許它會隱藏在接收到的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應用在處理請求出現的異常資訊就會以的形式直接出現在瀏覽器上,我們可以在這個頁面中看到幾乎所有的錯誤資訊,包括異常的類型、訊息和堆棧資訊等。
開發人員異常頁面除了顯示與拋出的異常相關的資訊之外,還會以如所示的形式顯示與當前請求上下文相關的資訊,其中包括當前請求URL攜帶的所有查詢字串、所有請求前序以及Cookie的內容。如此詳盡的資訊無疑會極大地協助開發人員儘快地找出錯誤的根源。
通過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所示的形式直接呈現在瀏覽器上。
最終作為異常處理器的是一個類型為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中介軟體最終會採用當前響應狀態代碼來替換它。如果我們利用瀏覽器來訪問這個應用,將會得到如所示的錯誤頁面。
如果我們希望針對不同的錯誤狀態代碼顯示不同的錯誤頁面,那麼我們就需要將具體的請求處理邏輯實現在一個的狀態代碼錯誤處理器中,並最終提供給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: }
我們指定的狀態代碼錯誤處理器在處理請求的時候,根據響應狀態代碼將錯誤分成用戶端錯誤和服務端錯誤兩種類型,並選擇針對性的錯誤訊息作為響應內容。當我們利用瀏覽器訪問這個應用的時候,顯示的錯誤訊息將由響應狀態代碼來決定。
在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中介軟體
三種呈現錯誤頁面的方式