天天看點

了解ASP.NET Core - 錯誤處理(Handle Errors)

注:本文隸屬于《了解ASP.NET Core》系列文章,請檢視置頂部落格或點選此處檢視全文目錄

開發人員異常頁用于顯示未處理的請求異常的詳細資訊。當我們通過ASP.NET Core模闆建立一個項目時,<code>Startup.Configure</code>方法中會自動生成以下代碼:

需要注意的是,與“異常處理”有關的中間件,一定要盡早添加,這樣,它可以最大限度的捕獲後續中間件抛出的未處理異常。

可以看到,當程式運作在開發環境中時,才會啟用開發人員異常頁,這很好了解,因為在生産環境中,我們不能将異常的詳細資訊暴露給使用者,否則,這将會導緻一系列安全問題。

現在我們在下方添加如下代碼抛出一個異常:

當開發人員異常頁中間件捕獲了該未處理異常時,會展示類似如下的相關資訊:

了解ASP.NET Core - 錯誤處理(Handle Errors)

該異常頁面展示了如下資訊:

異常消息

異常堆棧追蹤(Stack)

HTTP請求查詢參數(Query)

Cookies

HTTP請求标頭(Headers)

路由(Routing),包含了終結點和路由資訊

當你檢視<code>DeveloperExceptionPageMiddleware</code>的源碼時,你會在構造函數中發現一個入參,類型為<code>IEnumerable&lt;IDeveloperPageExceptionFilter&gt;</code>。通過這個Filter集合,組成一個錯誤處理器管道,按照先注冊先執行的原則,順序進行錯誤處理。

下面是<code>DeveloperExceptionPageMiddleware</code>的核心源碼:

這也就說明,如果我們想要自定義開發者異常頁,那我們可以通過實作<code>IDeveloperPageExceptionFilter</code>接口來達到目的。

先看一下<code>IDeveloperPageExceptionFilter</code>接口定義:

<code>HandleExceptionAsync</code>方法除了錯誤上下文資訊外,還包含了一個<code>Func&lt;ErrorContext, Task&gt; next</code>,這是幹嘛的呢?其實,前面我們已經提到了,<code>IDeveloperPageExceptionFilter</code>的所有實作,會組成一個管道,當錯誤需要在管道中的後續處理器作進一步處理時,就是通過這個<code>next</code>傳遞錯誤的,是以,當需要傳遞錯誤時,一定要記得調用<code>next</code>!

不廢話了,趕緊實作一個看看效果吧:

當抛出一個異常,你會看到類似如下的頁面:

了解ASP.NET Core - 錯誤處理(Handle Errors)

上面介紹了開發環境中的異常處理,現在我們來看一下生産環境中的異常處理,通過調用<code>UseExceptionHandler</code>擴充方法注冊中間件<code>ExceptionHandlerMiddleware</code>。

該異常處理程式:

可以捕獲後續中間件未處理的異常

若無異常或HTTP響應已經啟動(<code>Response.HasStarted == true</code>),則不做任何處理

不會改變URL中的路徑

預設情況下,會生成類似如下的模闆:

我們可以通過lambda向<code>UseExceptionHandler</code>中提供一個異常處理邏輯:

可以看到,當捕獲到異常時,可以通過<code>HttpContext.Features</code>,并指定類型<code>IExceptionHandlerPathFeature</code>或<code>IExceptionHandlerFeature</code>(前者繼承自後者),來擷取到異常資訊。

再提醒一遍,千萬不要将敏感的錯誤資訊暴露給用戶端。

除了使用lambda外,我們還可以指定一個路徑,指向一個備用管道進行異常處理,這個備用管道對于MVC來說,一般是Controller中的Action,例如MVC模闆預設的<code>/Home/Error</code>:

當捕獲到異常時,你會看到類似如下的頁面:

了解ASP.NET Core - 錯誤處理(Handle Errors)

你可以在Action<code>Error</code>中自定義錯誤處理邏輯,就像lambda一樣。

需要注意的是,不要随意對<code>Error</code>添加<code>[HttpGet]</code>、<code>[HttpPost]</code>等限定Http請求方法的特性。一旦你加上了<code>[HttpGet]</code>,那麼該方法隻能處理<code>Get</code>請求的異常。

不過,如果你就是打算将不同方法的Http請求分别進行處理,你可以類似如下進行處理:

另外,還需要提醒一下,如果在請求備用管道(如示例中的<code>Error</code>)時也報錯了,無論是Http請求管道中的中間件報錯,還是<code>Error</code>裡面報錯,此時<code>ExceptionHandlerMiddleware</code>均會重新引發原始異常,而不是向外抛出備用管道的異常。

一般異常處理程式頁是面向所有使用者的,是以請保證它可以匿名通路。

下面一塊看一下<code>ExceptionHandlerMiddleware</code>吧:

預設情況下,當ASP.NET Core遇到沒有正文的400-599Http錯誤狀态碼時,不會為其提供頁面,而是傳回狀态碼和空響應正文。可是,為了良好的使用者體驗,一般我們會對常見的錯誤狀态碼(404)提供友好的頁面,如gitee404

請注意,本節所涉及到的中間件與上兩節所講解的錯誤異常進行中間件不沖突,可以同時使用。确切的說,本節并不是處理異常,隻是為了提升使用者體驗。

我們可以通過<code>StatusCodePagesMiddleware</code>中間件實作該功能:

注意,一定要在異常進行中間件之後,請求進行中間件之前調用<code>UseStatusCodePages</code>。

現在,你可以請求一個不存在的路徑,例如<code>Home/Index2</code>,你會在浏覽器中看到如下輸出:

<code>UseStatusCodePages</code>也提供了重載,允許我們自定義響應内容類型和正文内容,如:

浏覽器輸出為:

同樣地,我們也可以通過向<code>UseStatusCodePages</code>傳入lambda表達式進行處理:

介紹了那麼多,你也看到了,事實上<code>UseStatusCodePages</code>效果并不好,是以我們在生産環境一般是不會用這玩意的,那用啥呢?請随我繼續往下看。

該擴充方法,内部實際上是通過調用<code>UseStatusCodePages</code>并傳入lambda進行實作的,該方法:

接收一個Http資源定位字元串。同樣的,會有一個占位符<code>{0}</code>,用于填充Http狀态碼

向用戶端發送Http狀态碼302-已找到

然後将用戶端重定向到指定的終結點,在該終結點中,可以針對不同錯誤狀态碼分别進行處理

現在你可以自己試一下。

不知道你有沒有注意:當我們請求一個不存在的路徑時,它的确會跳轉到404頁面,但是,Url也變了,變成了<code>/Home/StatusCodeError?code=404</code>,而且,響應狀态碼也變了,變成了<code>200Ok</code>。可以通過源碼看一下咋回事(我相信,大家看到302其實也都明白了):

如果你不想更改原始請求的Url,而且保留原始狀态碼,那麼你應該使用接下來要介紹的<code>UseStatusCodePagesWithReExecute</code>。

同樣的,該擴充方法,内部也是通過調用<code>UseStatusCodePages</code>并傳入lambda進行實作的,不過該方法:

接收1個路徑字元串和和1個查詢字元串。同樣的,會有一個占位符<code>{0}</code>,用于填充Http狀态碼

Url保持不變,并向用戶端傳回原始Http狀态碼

執行備用管道,用于生成響應正文

具體例子就不再列舉了,用上面的就行了。現在來看看源碼:

在MVC中,你可以通過給控制器或其中的Action方法添加<code>[SkipStatusCodePages]</code>特性,可以略過<code>StatusCodePagesMiddleware</code>。

除了錯誤進行中間件外,ASP.NET Core 還提供了異常過濾器,用于錯誤處理。

異常過濾器:

通過實作接口<code>IExceptionFilter</code>或<code>IAsyncExceptionFilter</code>來自定義異常過濾器

可以捕獲Controller建立時(也就是隻捕獲構造函數中抛出的異常)、模型綁定、Action Filter和Action中抛出的未處理異常

其他地方抛出的異常不會捕獲

本節僅介紹異常過濾器,有關過濾器的詳細内容,後續文章将會介紹

先來看一下這兩個接口:

<code>OnException</code>和<code>OnExceptionAsync</code>方法都包含一個類型為<code>ExceptionContext</code>參數,很顯然,它就是與異常有關的上下文,我們的異常處理邏輯離不開它。那接着來看一下它的結構吧:

除此之外,<code>ExceptionContext</code>還繼承了<code>FilterContext</code>,而<code>FilterContext</code>又繼承了<code>ActionContext</code>(這也從側面說明,過濾器是為Action服務的),也就是說我們也能夠擷取到一些過濾器和Action相關的資訊,看看都有什麼吧:

更多參數細節,我會在專門講過濾器的文章中詳細介紹。

下面,我們就來實作一個自定義的異常處理器:

接着,找到<code>/Views/Shared/Error.cshtml</code>,展示一下錯誤消息:

最後,将服務<code>MyExceptionFilterAttribute</code>注冊到DI容器:

現在,我們将該異常處理器加在<code>/Home/Index</code>上,并抛個異常:

當請求<code>/Home/Index</code>時,你會得到如下頁面:

了解ASP.NET Core - 錯誤處理(Handle Errors)

現在,我們已經介紹了兩種錯誤處理的方法——錯誤進行中間件和異常過濾器。現在來比較一下它們的異同,以及我們何時應該選擇哪種處理方式。

錯誤進行中間件:

可以捕獲後續中間件的所有未處理異常

擁有<code>RequestDelegate</code>,操作更加靈活

粒度較粗,僅可針對全局進行配置

錯誤進行中間件适合用于處理全局異常。

僅可捕獲Controller建立時(也就是構造函數中抛出的異常)、模型綁定、Action Filter和Action中抛出的未處理異常,其他地方抛出的異常捕獲不到

粒度更小,可以靈活針對Controller或Action配置不同的異常過濾器

異常過濾器非常适合用于捕獲并處理Action中的異常。

在我們的應用中,可以同時使用錯誤進行中間件和異常過濾器,隻有充分發揮它們各自的優勢,才能處理好程式中的錯誤。