ASP.NET Core 6框架揭祕實例演示[32]:錯誤頁面的集中呈現方式

語言: CN / TW / HK

由於ASP.NET是一個同時處理多個請求的Web應用框架,所以在處理某個請求過程中出現異常並不會導致整個應用的中止。出於安全方面的考量,為了避免敏感信息外泄,客户端在默認情況下並不會得到詳細的出錯信息,這無疑會在開發過程中增加查錯和糾錯的難度。對於生產環境來説,我們也希望最終用户能夠根據具體的錯誤類型得到具有針對性並且友好的錯誤消息。ASP.NET提供的相應的中間件可以幫助我們將定製化的錯誤信息呈現出來。本文提供的示例演示已經同步到《 ASP.NET Core 6框架揭祕-實例演示版 》)

目錄

[2101]開發者異常頁面的呈現( 源代碼

[2102]定製異常頁面的呈現( 源代碼

[2103]利用註冊的中間件處理異常( 源代碼

[2104]針對異常頁面的重定向( 源代碼

[2105]基於響應狀態碼錯誤頁面的呈現(設置響應內容模板)( 源代碼

[2106]基於響應狀態碼錯誤頁面的呈現(提供異常處理器)( 源代碼

[2107]基於響應狀態碼錯誤頁面的呈現(利用中間件創建異常處理器)( 源代碼

[2101]開發者異常頁面的呈現

如果ASP.NET應用在處理某個請求時出現異常,它一般會返回一個狀態碼為“500 Internal Server Error”的響應。為了避免一些敏感信息的外泄,客户端只會得到一個很泛化的錯誤消息。以如下所示的程序為例,處理根路徑的請求時都會拋出一個InvalidOperationException類型的異常。

var app = WebApplication.Create();
app.MapGet("/",
void () => throw new InvalidOperationException("Manually thrown exception"));
app.Run();

利用瀏覽器訪問這個應用總是會得到圖1所示的錯誤頁面。可以看出這個頁面僅僅告訴我們目標應用當前無法正常處理本次請求,除了提供的響應狀態碼(“HTTP ERROR 500”),它並沒有提供任何有益於糾錯的輔助信息。

圖1默認的錯誤頁面

有人認為瀏覽器上雖然沒有顯示任何詳細的錯誤信息,但這並不意味着HTTP響應報文中也沒有攜帶任何詳細的出錯信息。如下所示的服務端會返回的HTTP響應報文,該響應沒有主體內容,有限的幾個報頭也並沒有承載任何與錯誤有關的信息。

HTTP/1.1 500 Internal Server Error
Content-Length: 0
Date: Sun, 07 Nov 2021 08:34:18 GMT
Server: Kestrel

由於應用並沒有中斷,瀏覽器上也並沒有顯示任何具有針對性的錯誤信息,我們無法知道背後究竟出現了什麼錯誤。這個問題有兩種解決方案:一種是利用日誌,ASP.NET在處理請求過程中出現異常時,會發出相應的日誌事件,我們可以註冊相應的ILoggerProvider對象將日誌輸出到指定的渠道。另一種解決方案就是利用註冊的DeveloperExceptionPageMiddleware中間件顯示一個“開發者異常頁面(Developer Exception Page)”。

如下的演示程序調用IApplicationBuilder接口的UseDeveloperExceptionPage擴展方法來註冊了這個中間件。該程序註冊了一個路由模板為“{foo}/{bar}”的終結點,後者在處理請求時直接拋出異常。

var app = WebApplication.Create();
app.UseDeveloperExceptionPage();
app.MapGet("{foo}/{bar}",
void () => throw new InvalidOperationException("Manually thrown exception"));
app.Run();

一旦註冊了DeveloperExceptionPageMiddleware中間件,ASP.NET應用在處理請求過程中出現的異常信息就會以圖2所示的形式直接出現在瀏覽器上,我們可以在這個頁面中看到幾乎所有的錯誤信息,包括異常的類型、消息和堆棧信息等。

圖2開發者異常頁面(基本信息)

開發者異常頁面除了顯示與拋出的異常相關的信息,還會以圖3所示的形式顯示與當前請求上下文相關的信息,包括當前請求URL攜帶的所有查詢字符串、所有請求報頭、Cookie的內容和路由信息(終結點和路由參數)。如此詳盡的信息無疑會極大地幫助開發人員儘快找出錯誤的根源。由於此頁面上往往會攜帶一些敏感的信息,所以只有在開發環境才能註冊這個中間件。實際上Minimal API在開發環境會默認註冊這個中間件。

圖3開發者異常頁面(詳細信息)

[2102]定製異常頁面的呈現

由於ExceptionHandlerMiddleware中間件直接利用提供的RequestDelegate委託來處理出現異常的請求,我們可以利用它呈現一個定製化的錯誤頁面。如下的演示程序通過調用IApplicationBuilder接口的UseExceptionHandler擴展方法註冊了這個中間件,提供的的ExceptionHandlerOptions配置選項指定了一個指向HandleErrorAsync方法的RequestDelegate委託作為異常處理器。

var options = new ExceptionHandlerOptions { ExceptionHandler = HandleErrorAsync };
var app = WebApplication.Create();
app.UseExceptionHandler(options);
app.MapGet("/",
void () => throw new InvalidOperationException("Manually thrown exception"));
app.Run();

static Task HandleErrorAsync(HttpContext context) => context.Response.WriteAsync("Unhandled exception occurred!");

如上面的代碼片段所示,HandleErrorAsync方法僅僅是將一個簡單的錯誤消息(Unhandled exception occurred!)作為響應的內容。演示程序註冊了一個針對根路徑(“/”)的並且直接拋出異常的終結點,當我們利用瀏覽器訪問該終結點時,這個定製的錯誤消息會以圖4所示的形式直接呈現在瀏覽器上。

圖4定製的錯誤頁面

[2103]利用註冊的中間件處理異常

由於ExceptionHandlerMiddleware中間件的異常處理器的是一個RequestDelegate委託,而IApplicationBuilder對象具有利用註冊的中間件來創建這個委託對象的能力,所以用於註冊該中間件的UseExceptionHandler擴展方法提供了一個參數類型為Action<IApplicationBuilder>重載。如下的演示程序調用了這個方法,在提供的作為參數的Action<IApplicationBuilder>委託中,我們調用了IApplicationBuilder接口的Run方法註冊了一箇中間件來處理異常,訪問啟動後的程序同樣會得到如圖21-4的錯誤信息(S2103)。

var app = WebApplication.Create();
app.UseExceptionHandler(app2 => app2.Run(HandleErrorAsync))
app.MapGet("/",
void () => throw new InvalidOperationException("Manually thrown exception"));
app.Run();

static Task HandleErrorAsync(HttpContext context)  => context.Response.WriteAsync("Unhandled exception occurred!");

[2104]針對異常頁面的重定向

如果應用已經提供了一個錯誤頁面,ExceptionHandlerMiddleware中間件在進行異常處理時可以直接重定向到該頁面就可以了。如下的演示程序採用這種方式調用了另一個UseExceptionHandler擴展方法重載,作為參數的字符串(“/error”)指定的就是錯誤頁面的路徑,訪問啟動後的程序同樣會得到如圖4的錯誤信息。

var app = WebApplication.Create();
app.UseExceptionHandler("/error");
app.MapGet("/",
void () => throw new InvalidOperationException("Manually thrown exception"));
app.MapGet("/error", HandleErrorAsync);
app.Run();

static Task HandleErrorAsync(HttpContext context)  => context.Response.WriteAsync("Unhandled exception occurred!");

[2105]基於響應狀態碼錯誤頁面的呈現(設置響應內容模板)

我們知道HTTP語義中的錯誤是由響應的狀態碼來表達的,涉及的錯誤大體劃分為如下兩種類型:

  • 客户端錯誤:表示因客户端提供不正確的請求信息而導致服務器不能正常處理請求,響應狀態碼的範圍為400~499。
  • 服務端錯誤:表示服務器在處理請求過程中因自身的問題而發生錯誤,響應狀態碼的範圍為500~599。

StatusCodePagesMiddleware中間件幫助我們針對響應狀態碼對錯誤頁面進行定製。該中間件只有在後續管道產生一個錯誤響應狀態碼(範圍為400~599)才會將錯誤頁面呈現出來。如下的演示程序通過調用UseStatusCodePages擴展方法註冊了這個中間件,作為參數的兩個字符串分別是響應的媒體類型和作為主體內容的模板,佔位符“{0}”將被狀態碼進行填充。

var app = WebApplication.Create();
app.UseStatusCodePages("text/plain", "Error occurred ({0})");
app.MapGet("/", void (HttpResponse response) => response.StatusCode = 500);
app.Run();

我們針對根路徑(“/”)註冊了一個終結點,後者在處理請求時直接返回狀態碼為500的響應。應用啟動後,針對該路徑請求將會得到如圖5所示的錯誤頁面。

圖5針對錯誤響應狀態碼定製的錯誤頁面

[2106]基於響應狀態碼錯誤頁面的呈現(提供異常處理器)

StatusCodePagesMiddleware中間件的錯誤處理器體現為一個Func<StatusCodeContext, Task>委託,作為輸入的StatusCodeContext是對當前HttpContext上下文的封裝。如下的演示程序定義了一個與此委託具有一致聲明的HandleErrorAsync來呈現錯誤頁面,UseStatusCodePages擴展方法指定的Func<StatusCodeContext, Task>委託指向這個方法。

using Microsoft.AspNetCore.Diagnostics;
var random = new Random();
var app = WebApplication.Create();
app.UseStatusCodePages(HandleErrorAsync);
app.MapGet("/", void (HttpResponse response) => response.StatusCode = random.Next(400,599));
app.Run();

static  Task HandleErrorAsync(StatusCodeContext context)
{
    var response = context.HttpContext.Response;
    return response.StatusCode < 500
    ? response.WriteAsync($"Client error ({response.StatusCode})")
    : response.WriteAsync($"Server error ({response.StatusCode})");
}

我們針對根路徑(“/”)註冊的終結點會隨機返回一個狀態碼在(400,599)區間內的響應。用來處理錯誤的HandleErrorAsync方法會根據狀態碼所在的區間(400~499, 500~599)分別顯式“Client error”和“Server error”。應用啟動後,針對根路徑的請求會得到如圖6所示錯誤頁面。

圖6針對錯誤響應狀態碼定製的錯誤頁面

[2107]基於響應狀態碼錯誤頁面的呈現(利用中間件創建異常處理器)

在ASP.NET的世界裏,針對請求的處理總是體現為一個RequestDelegate委託,而IApplicationBuilder對象具有根據註冊的中間件構建這個委託的能力,所以 UseStatusCodePages方法還具有另一個將Action<IApplicationBuilder>委託作為參數的重載。如下的演示程序調用了這個重載,我們利用提供的委託調用了IApplicationBuilder對象的Run擴展方法註冊了一箇中間件來處理異常(S2107)。

var random = new Random();
var app = WebApplication.Create();
app.UseStatusCodePages(app2 => app2.Run(HandleErrorAsync));
app.MapGet("/", void (HttpResponse response) => response.StatusCode = random.Next(400,599));
app.Run();

static  Task HandleErrorAsync(HttpContext context)
{
    var response = context.Response;
    return response.StatusCode < 500
    ? response.WriteAsync($"Client error ({response.StatusCode})")
    : response.WriteAsync($"Server error ({response.StatusCode})");
}