ASP.NET Core應(yīng)用錯(cuò)誤處理之三種呈現(xiàn)錯(cuò)誤頁(yè)面的方式
前言
由于ASP.NET Core應(yīng)用是一個(gè)同時(shí)處理多個(gè)請(qǐng)求的服務(wù)器應(yīng)用,所以在處理某個(gè)請(qǐng)求過(guò)程中拋出的異常并不會(huì)導(dǎo)致整個(gè)應(yīng)用的終止。出于安全方面的考量,為了避免敏感信息的外泄,客戶端在默認(rèn)的情況下并不會(huì)得到詳細(xì)的出錯(cuò)信息,這無(wú)疑會(huì)在開發(fā)環(huán)境下增加查錯(cuò)糾錯(cuò)的難度。對(duì)于生產(chǎn)環(huán)境來(lái)說(shuō),我們也希望最終用戶能夠根據(jù)具體的錯(cuò)誤類型得到具有針對(duì)性并且友好的錯(cuò)誤消息。ASP.NET Core提供了相應(yīng)的中間件幫助我們將定制化的錯(cuò)誤信息呈現(xiàn)出來(lái),這些中間件都定義在“Microsoft.AspNetCore.Diagnostics”這個(gè)NuGet包中。在著重介紹這些中間件之前,我們照理演示幾個(gè)簡(jiǎn)單的實(shí)例讓讀者朋友們對(duì)這些中間件的作用有一個(gè)大概的了解。
一、顯示開發(fā)者異常頁(yè)面
一般情況下,如果ASP.NET Core在處理某個(gè)請(qǐng)求時(shí)出現(xiàn)異常,它一般會(huì)返回一個(gè)狀態(tài)碼為“500 Internal Server Error”的響應(yīng)。為了避免一些敏感信息的外泄,詳細(xì)的錯(cuò)誤信息并不會(huì)隨著響應(yīng)發(fā)送給客戶端,所以客戶端只會(huì)得到一個(gè)很一般化的錯(cuò)誤消息。以如下這個(gè)程序?yàn)槔?,服?wù)端在處理每個(gè)請(qǐng)求時(shí)都會(huì)拋出一個(gè)類型為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();
}
}
當(dāng)我們利用瀏覽器訪問(wèn)這個(gè)應(yīng)用的時(shí)候,總是會(huì)得到如下圖所示的這個(gè)錯(cuò)誤頁(yè)面??梢钥闯鲞@個(gè)頁(yè)面僅僅告訴我們目標(biāo)應(yīng)用當(dāng)前無(wú)法正常處理本次請(qǐng)求,除了提供的響應(yīng)狀態(tài)碼(“HTTP ERROR 500”)之外,它并沒(méi)有提供任何有益于差錯(cuò)糾錯(cuò)的錯(cuò)誤信息。
那么有人可能會(huì)覺(jué)得雖然瀏覽器上沒(méi)有顯示出任何詳細(xì)的錯(cuò)誤信息,也許它會(huì)隱藏在接收到的HTTP響應(yīng)報(bào)文中。針對(duì)通過(guò)瀏覽器放出的這個(gè)請(qǐng)求,得到的響應(yīng)內(nèi)容如下所示,我們會(huì)發(fā)現(xiàn)響應(yīng)報(bào)文根本沒(méi)有主體部分,有限的幾個(gè)報(bào)頭也并沒(méi)有承載任何與錯(cuò)誤有關(guān)的信息。
HTTP/1.1 500 Internal Server Error Date: Fri, 09 Dec 2016 23:42:18 GMT Content-Length: 0 Server: Kestrel
由于應(yīng)用并沒(méi)有中斷,瀏覽器上也并沒(méi)有顯示任何具有針對(duì)性的錯(cuò)誤信息,開發(fā)人員在進(jìn)行查錯(cuò)糾錯(cuò)的時(shí)候如何準(zhǔn)確定位到作為錯(cuò)誤根源的那一行代碼呢?具體來(lái)說(shuō),我們又兩種解決方案,一種就是利用日志,因?yàn)锳SP.NET Core在進(jìn)行請(qǐng)求處理時(shí)出現(xiàn)的任何錯(cuò)誤都會(huì)被寫入日志,所以我們可以通過(guò)注冊(cè)相應(yīng)的LoggerProvider(比如注冊(cè)一個(gè)ConsoleLoggerProvider將日志直接寫入宿主應(yīng)用的控制臺(tái))到來(lái)獲取寫入的錯(cuò)誤日志。
至于另一種解決方案,就是直接顯示一個(gè)包含錯(cuò)誤相應(yīng)信息的錯(cuò)誤頁(yè)面,由于這個(gè)頁(yè)面是在開發(fā)環(huán)境給開發(fā)者看的,所以我們將這個(gè)頁(yè)面稱為“開發(fā)者異常頁(yè)面(Developer Exception Page)”。針對(duì)頁(yè)面的自動(dòng)呈現(xiàn)是利用一個(gè)名為DeveloperExceptionPageMiddleware的中間件來(lái)完成的,我們可以調(diào)用ApplicationBuilder的擴(kuò)展方法UseDeveloperExceptionPage來(lái)注冊(cè)這個(gè)中間件。
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();
}
}
一旦注冊(cè)了這個(gè)DeveloperExceptionPageMiddleware中間件,ASP.NET Core應(yīng)用在處理請(qǐng)求出現(xiàn)的異常信息就會(huì)以下圖的形式直接出現(xiàn)在瀏覽器上,我們可以在這個(gè)頁(yè)面中看到幾乎所有的錯(cuò)誤信息,包括異常的類型、消息和堆棧信息等。
開發(fā)者異常頁(yè)面除了顯示與拋出的異常相關(guān)的信息之外,還會(huì)以如下圖所示的形式顯示與當(dāng)前請(qǐng)求上下文相關(guān)的信息,其中包括當(dāng)前請(qǐng)求URL攜帶的所有查詢字符串、所有請(qǐng)求報(bào)頭以及Cookie的內(nèi)容。如此詳盡的信息無(wú)疑會(huì)極大地幫助開發(fā)人員盡快地找出錯(cuò)誤的根源。
通過(guò)DeveloperExceptionPageMiddleware中間件呈現(xiàn)的錯(cuò)誤頁(yè)面僅僅是供開發(fā)人員使用的,詳細(xì)的錯(cuò)誤信息往往會(huì)攜帶一些敏感的信息,所以務(wù)必記住只有在開發(fā)環(huán)境才能注冊(cè)這個(gè)中間件,如下所示的代碼片段體現(xiàn)了針對(duì)DeveloperExceptionPageMiddleware中間件正確的注冊(cè)方式。
new WebHostBuilder()
.UseStartup<Startup>()
…
public class Startup
{
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
}
}
二、顯示定制異常頁(yè)面
DeveloperExceptionPageMiddleware中間件通過(guò)將異常詳細(xì)信息和基于當(dāng)前請(qǐng)求的內(nèi)容直接呈現(xiàn)在錯(cuò)誤頁(yè)面中,這為開發(fā)人員的糾錯(cuò)診斷提供了極大的便利。但是在生產(chǎn)環(huán)境下,我們傾向于為最終的用戶呈現(xiàn)一個(gè)定制的錯(cuò)誤頁(yè)面,而這可以通過(guò)注冊(cè)另一個(gè)名為ExceptionHandlerMiddleware的中間件來(lái)實(shí)現(xiàn)。顧名思義,這個(gè)中間件旨在提供一個(gè)異常處理器(Exception Handler)來(lái)處理拋出的異常。實(shí)際上這個(gè)所謂的異常處理器就是一個(gè)類型為RequestDelegate的委托對(duì)象,ExceptionHandlerMiddleware中間件捕捉到拋出的異常后利用它來(lái)響應(yīng)當(dāng)前的請(qǐng)求。
還是以上面創(chuàng)建的這個(gè)總是會(huì)拋出一個(gè) InvalidOperationException異常的應(yīng)用為例。我們按照如下的形式調(diào)用ApplicationBuilder的擴(kuò)展方法UseExceptionHandler注冊(cè)了上述的這個(gè)ExceptionHandlerMiddleware中間件。這個(gè)擴(kuò)展方法具有一個(gè)ExceptionHandlerOptions類型的參數(shù),它的ExceptionHandler屬性返回的就是這個(gè)作為異常處理器的RequestDelegate對(duì)象。
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();
}
}
如上面的代碼片段所示,這個(gè)作為異常處理器的RequestDelegate僅僅是將一個(gè)簡(jiǎn)單的錯(cuò)誤消息(“Unhandled exception occurred!”)作為響應(yīng)的內(nèi)容。當(dāng)我們利用瀏覽器訪問(wèn)該應(yīng)用的時(shí)候,這個(gè)定制的錯(cuò)誤消息將會(huì)以如圖4所示的形式直接呈現(xiàn)在瀏覽器上。
最終作為異常處理器的是一個(gè)類型為RequestDelegate的委托對(duì)象,而ApplicationBuilder具有創(chuàng)建這個(gè)委托對(duì)象的能力。具體來(lái)說(shuō),我們可以根據(jù)異常處理的需要將相應(yīng)的中間件注冊(cè)到某個(gè)ApplicationBuilder對(duì)象上,并最終利用這個(gè)ApplicationBuilder根據(jù)注冊(cè)的中間件創(chuàng)建出作為異常處理器的RequestDelegate對(duì)象。 如果異常處理需要通過(guò)一個(gè)或者多個(gè)中間件來(lái)完成,我們可以按照如下的形式調(diào)用另一個(gè)UseExceptionHandler方法重載。這個(gè)方法的參數(shù)類型為Action<IApplicationBuilder>,我們調(diào)用它的Run方法注冊(cè)了一個(gè)中間件來(lái)響應(yīng)一個(gè)簡(jiǎn)單的錯(cuò)誤消息。
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();
}
}
上面這兩種異常處理的形式都體現(xiàn)在提供一個(gè)RequestDelegate的委托對(duì)象來(lái)處理拋出的異常并完成最終的響應(yīng)。如果應(yīng)用已經(jīng)設(shè)置了一個(gè)錯(cuò)誤頁(yè)面,并且這個(gè)錯(cuò)誤頁(yè)面具有一個(gè)固定的路徑,那么我們?cè)谶M(jìn)行異常處理的時(shí)候就沒(méi)有必要提供這個(gè)RequestDelegate對(duì)象,而只需要重定向到錯(cuò)誤頁(yè)面指向的路徑即可。這種采用服務(wù)端重定向的異常處理方式可以采用如下的形式調(diào)用另一個(gè)UseExceptionHandler方法重載來(lái)完成,這個(gè)方法的參數(shù)表示的就是重定向的目標(biāo)路徑(“/error”),我們針對(duì)這個(gè)路徑注冊(cè)了一個(gè)路由來(lái)響應(yīng)定制的錯(cuò)誤消息。
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();
}
}
三、針對(duì)響應(yīng)狀態(tài)碼定制錯(cuò)誤頁(yè)面
由于Web應(yīng)用采用HTTP通信協(xié)議,所以我們應(yīng)該盡可能低迎合HTTP標(biāo)準(zhǔn)并將定義在協(xié)議規(guī)范中的語(yǔ)義應(yīng)用到應(yīng)用中。對(duì)于異?;蛘咤e(cuò)誤的語(yǔ)義表達(dá)在HTTP協(xié)議層面主要體現(xiàn)在響應(yīng)報(bào)文的狀態(tài)碼上,具體來(lái)說(shuō)HTTP通信的錯(cuò)誤大體分為如下兩種類型:
- 客戶端錯(cuò)誤:表示因客戶端提供不正確的請(qǐng)求信息而導(dǎo)致服務(wù)器不能正常處理請(qǐng)求,響應(yīng)狀態(tài)碼范圍在400~499之間。
- 服務(wù)端錯(cuò)誤:表示服務(wù)器在處理請(qǐng)求過(guò)程中因自身的問(wèn)題而發(fā)生錯(cuò)誤,響應(yīng)狀態(tài)碼在500~509之間。
正是因?yàn)轫憫?yīng)狀態(tài)碼是對(duì)錯(cuò)誤或者異常語(yǔ)義最重要的表達(dá),所以在很多情況下我們需要針對(duì)不同的響應(yīng)狀態(tài)碼來(lái)定制顯示的錯(cuò)誤信息。針對(duì)響應(yīng)狀態(tài)碼對(duì)錯(cuò)誤頁(yè)面的定制可以借助一個(gè)類型為StatusCodePagesMiddleware的中間件來(lái)實(shí)現(xiàn),我們可以調(diào)用ApplicationBuilder相應(yīng)的擴(kuò)展方法來(lái)注冊(cè)這個(gè)中間件。
DeveloperExceptionPageMiddleware和ExceptionHandlerMiddleware中間件都是在后續(xù)請(qǐng)求處理過(guò)程中拋出異常的情況下才會(huì)被調(diào)用,而StatusCodePagesMiddleware被調(diào)用的前提是后續(xù)請(qǐng)求助理過(guò)程中產(chǎn)生一個(gè)錯(cuò)誤響應(yīng)狀態(tài)碼(范圍在400~599之間)。如果僅僅希望顯示一個(gè)統(tǒng)一的錯(cuò)誤頁(yè)面,我們可以按照如下的形式調(diào)用擴(kuò)展方法UseStatusCodePages注冊(cè)這個(gè)中間件,傳入該方法的兩個(gè)參數(shù)分別表示響應(yīng)采用的媒體類型和主體內(nèi)容。
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();
}
}
如上面的代碼片段所示,應(yīng)用在處理請(qǐng)求的時(shí)候總是會(huì)將響應(yīng)狀態(tài)碼設(shè)置為500,所以最終的響應(yīng)內(nèi)容將由注冊(cè)的StatusCodePagesMiddleware中間件來(lái)提供。我們調(diào)用UseStatusCodePages方法的時(shí)候?qū)㈨憫?yīng)的媒體類型設(shè)置為“text/plain”,并將一段簡(jiǎn)單的錯(cuò)誤消息作為了響應(yīng)的主體內(nèi)容。值得一提的時(shí)候,作為響應(yīng)內(nèi)容的字符串可以包含一個(gè)占位符({0}),StatusCodePagesMiddleware中間件最終會(huì)采用當(dāng)前響應(yīng)狀態(tài)碼來(lái)替換它。如果我們利用瀏覽器來(lái)訪問(wèn)這個(gè)應(yīng)用,將會(huì)得到如下圖所示的錯(cuò)誤頁(yè)面。
如果我們希望針對(duì)不同的錯(cuò)誤狀態(tài)碼顯示不同的錯(cuò)誤頁(yè)面,那么我們就需要將具體的請(qǐng)求處理邏輯實(shí)現(xiàn)在一個(gè)的狀態(tài)碼錯(cuò)誤處理器中,并最終提供給StatusCodePagesMiddleware中間件。這個(gè)所謂的狀態(tài)碼錯(cuò)誤處理器體現(xiàn)為一個(gè)類型為Func<StatusCodeContext, Task>的委托對(duì)象,作為輸入的StatusCodeContext對(duì)象是對(duì)當(dāng)前HttpContext的封裝,同時(shí)承載著其他一些與錯(cuò)誤處理相關(guān)的選項(xiàng)設(shè)置,我們將在本系列后續(xù)部分對(duì)這個(gè)類型進(jìn)行詳細(xì)介紹。
對(duì)于如下這個(gè)應(yīng)用來(lái)說(shuō),它在處理任意一個(gè)請(qǐng)求是總是會(huì)隨機(jī)地選擇一個(gè)400~599之間的整數(shù)作為響應(yīng)的狀態(tài)碼,所以客戶端返回的響應(yīng)內(nèi)容總是通過(guò)注冊(cè)的StatusCodePagesMiddleware中間件來(lái)提供。我們?cè)谡{(diào)用另一個(gè)UseStatusCodePages方法重載的時(shí)候,為注冊(cè)的中間件指定了一個(gè)Func<StatusCodeContext, Task>對(duì)象作為狀態(tài)碼錯(cuò)誤處理器。
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();
}
}
我們指定的狀態(tài)碼錯(cuò)誤處理器在處理請(qǐng)求的時(shí)候,根據(jù)響應(yīng)狀態(tài)碼將錯(cuò)誤分成客戶端錯(cuò)誤和服務(wù)端錯(cuò)誤兩種類型,并選擇針對(duì)性的錯(cuò)誤消息作為響應(yīng)內(nèi)容。當(dāng)我們利用瀏覽器訪問(wèn)這個(gè)應(yīng)用的時(shí)候,顯示的錯(cuò)誤消息將由響應(yīng)狀態(tài)碼來(lái)決定。
在ASP.NET Core的世界里,針對(duì)請(qǐng)求的處理總是體現(xiàn)為一個(gè)RequestDelegate對(duì)象。如果請(qǐng)求的處理需要借助一個(gè)或者多個(gè)中間件來(lái)完成,我們可以將它們注冊(cè)到ApplicationBuilder對(duì)象上并利用它將中間件管道轉(zhuǎn)換成一個(gè)RequestDelegate對(duì)象。用于注冊(cè)StatusCodePagesMiddleware中間件的UseStatusCodePage方法還具有另一個(gè)重載,它允許我們采用這種方式來(lái)創(chuàng)建一個(gè)RequestDelegate對(duì)象來(lái)完成最終的請(qǐng)求處理工作,所以上面演示的這個(gè)應(yīng)用完全可以改寫成如下的形式。
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();
}
}
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Asp.net SignalR應(yīng)用并實(shí)現(xiàn)群聊功能
這篇文章主要為大家分享了Asp.net SignalR應(yīng)用并實(shí)現(xiàn)群聊功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04
防SQL注入 生成參數(shù)化的通用分頁(yè)查詢語(yǔ)句
前些時(shí)間看了玉開兄的“如此高效通用的分頁(yè)存儲(chǔ)過(guò)程是帶有sql注入漏洞的”這篇文章,才突然想起某個(gè)項(xiàng)目也是使用了累似的通用分頁(yè)存儲(chǔ)過(guò)程。2010-07-07
asp.net動(dòng)態(tài)生成HTML表單的方法
這篇文章主要介紹了asp.net動(dòng)態(tài)生成HTML表單的方法,結(jié)合實(shí)例形式分析了asp.net動(dòng)態(tài)生成HTML表單的相關(guān)控件使用技巧與注意事項(xiàng),需要的朋友可以參考下2017-03-03
silverlight用webclient大文件上傳的實(shí)例代碼
這篇文章介紹了silverlight用webclient大文件上傳的實(shí)例代碼,有需要的朋友可以參考一下2013-10-10
擴(kuò)展了Repeater控件的EmptyDataTemplate模板功能
Repeater控件是一個(gè)數(shù)據(jù)顯示控件,該控件允許通過(guò)為列表中顯示的每一項(xiàng)重復(fù)使用指定的模板來(lái)自定義布局2013-01-01
asp.net簡(jiǎn)單頁(yè)面控件賦值實(shí)現(xiàn)方法
這篇文章主要介紹了asp.net簡(jiǎn)單頁(yè)面控件賦值實(shí)現(xiàn)方法,涉及數(shù)據(jù)庫(kù)的查詢及頁(yè)面控件元素賦值操作相關(guān)技巧,需要的朋友可以參考下2016-07-07
DropDownList綁定選擇數(shù)據(jù)報(bào)錯(cuò)提示異常解決方案
DropDownList控件在綁定選擇數(shù)據(jù)時(shí)提示報(bào)錯(cuò)異常詳細(xì)信息為:有一個(gè)無(wú)效 SelectedValue,因?yàn)樗辉陧?xiàng)目列表中,應(yīng)該有很多新手朋友們遇到過(guò)吧,本文將給予解決方法,感興趣的朋友可以了解下,希望本對(duì)你有所幫助2013-01-01
.net core在服務(wù)器端獲取api傳遞的參數(shù)過(guò)程
這篇文章主要介紹了.net core在服務(wù)器端獲取api傳遞的參數(shù)過(guò)程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10







