ASP.NET Core應(yīng)用錯誤處理之ExceptionHandlerMiddleware中間件呈現(xiàn)“定制化錯誤頁面”
前言
DeveloperExceptionPageMiddleware中間件利用呈現(xiàn)出來的錯誤頁面實現(xiàn)拋出異常和當(dāng)前請求的詳細(xì)信息以輔助開發(fā)人員更好地進(jìn)行糾錯診斷工作,而ExceptionHandlerMiddleware中間件則是面向最終用戶的,我們可以利用它來顯示一個友好的定制化的錯誤頁面。按照慣例,我們還是先來看看ExceptionHandlerMiddleware的類型定義。
public class ExceptionHandlerMiddleware { public ExceptionHandlerMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions<ExceptionHandlerOptions> options, DiagnosticSource diagnosticSource); public Task Invoke(HttpContext context); } public class ExceptionHandlerOptions { public RequestDelegate ExceptionHandler { get; set; } public PathString ExceptionHandlingPath { get; set; } }
與DeveloperExceptionPageMiddleware類似,我們在創(chuàng)建一個ExceptionHandlerMiddleware對象的時候同樣需要提供一個攜帶配置選項的對象,從上面的代碼可以看出這是一個ExceptionHandlerOptions。具體來說,一個ExceptionHandlerOptions對象通過其ExceptionHandler屬性提供了一個最終用來處理請求的RequestDelegate對象。如果希望發(fā)生異常后自動重定向到某個指定的路徑,我們可以利用ExceptionHandlerOptions對象的ExceptionHandlingPath屬性來指定這個路徑。我們一般會調(diào)用ApplicationBuilder的擴(kuò)展方法UseExceptionHandler來注冊ExceptionHandlerMiddleware中間件,這些重載的UseExceptionHandler方法會采用如下的方式完整中間件的注冊工作。
public static class ExceptionHandlerExtensions { public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app)=> app.UseMiddleware<ExceptionHandlerMiddleware>(); public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, ExceptionHandlerOptions options) => app.UseMiddleware<ExceptionHandlerMiddleware>(Options.Create(options)); public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, string errorHandlingPath) { return app.UseExceptionHandler(new { ExceptionHandlingPath = new PathString(errorHandlingPath) }); } public static IApplicationBuilder UseExceptionHandler(this IApplicationBuilder app, Action<IApplicationBuilder> configure) { IApplicationBuilder newBuilder = app.New(); configure(newBuilder); return app.UseExceptionHandler(new ExceptionHandlerOptions { ExceptionHandler = newBuilder.Build() }); } }
一、異常處理器
ExceptionHandlerMiddleware中間件處理請求的本質(zhì)就是在后續(xù)請求處理過程中出現(xiàn)異常的情況下采用注冊的異常處理器來處理并響應(yīng)請求,這個異常處理器就是我們再熟悉不過的RequestDelegate對象。該中間件采用的請求處理邏輯大體上可以通過如下所示的這段代碼來體現(xiàn)。
public class ExceptionHandlerMiddleware { private RequestDelegate _next; private ExceptionHandlerOptions _options; public ExceptionHandlerMiddleware(RequestDelegate next, IOptions<ExceptionHandlerOptions> options,…) { _next = next; _options = options.Value; … } public async Task Invoke(HttpContext context) { try { await _next(context); } catch { context.Response.StatusCode = 500; context.Response.Clear(); if (_options.ExceptionHandlingPath.HasValue) { context.Request.Path = _options.ExceptionHandlingPath; } RequestDelegate handler = _options.ExceptionHandler ?? _next; await handler(context); } } }
如上面的代碼片段所示,如果后續(xù)的請求處理過程中出現(xiàn)異常,ExceptionHandlerMiddleware中間件會利用一個作為異常處理器的RequestDelegate對象來完成最終的請求處理工作。如果在創(chuàng)建ExceptionHandlerMiddleware時提供的ExceptionHandlerOptions攜帶著這么一個RequestDelegate對象,那么它將作為最終使用的異常處理器,否則作為異常處理器的實際上就是后續(xù)的中間件。換句話說,如果我們沒有通過ExceptionHandlerOptions顯式指定一個異常處理器,ExceptionHandlerMiddleware中間件會在后續(xù)管道處理請求拋出異常的情況下將請求再次傳遞給后續(xù)管道。
當(dāng)ExceptionHandlerMiddleware最終利用異常處理器來處理請求之前,它會對請求做一些前置處理工作,比如它會將響應(yīng)狀態(tài)碼設(shè)置為500,比如清空當(dāng)前所有響應(yīng)內(nèi)容等。如果我們利用ExceptionHandlerOptions的ExceptionHandlingPath屬性設(shè)置了一個重定向路徑,它會將該路徑設(shè)置為當(dāng)前請求的路徑。除了這些,ExceptionHandlerMiddleware中間件實際上做了一些沒有反應(yīng)在上面這段代碼片段中的工作。
二、異常的傳遞與請求路徑的恢復(fù)
由于ExceptionHandlerMiddleware中間件總會利用一個作為異常處理器的RequestDelegate對象來完成最終的異常處理工作,為了讓后者能夠得到拋出的異常,該中間件應(yīng)該采用某種方式將異常傳遞給它。除此之外,由于ExceptionHandlerMiddleware中間件會改變當(dāng)前請求的路徑,當(dāng)整個請求處理完成之后,它必須將請求路徑恢復(fù)成原始的狀態(tài),否則前置的中間件就無法獲取到正確的請求路徑。
請求處理過程中拋出的異常和原始請求路徑的恢復(fù)是通過相應(yīng)的特性完成的。具體來說,傳遞這兩者的特性分別叫做ExceptionHandlerFeature和ExceptionHandlerPathFeature,對應(yīng)的接口分別為IExceptionHandlerFeature和IExceptionHandlerPathFeature,如下面的代碼片段所示,后者繼承前者。默認(rèn)使用的ExceptionHandlerFeature實現(xiàn)了這兩個接口。
public interface IExceptionHandlerFeature { Exception Error { get; } } public interface IExceptionHandlerPathFeature : IExceptionHandlerFeature { string Path { get; } } public class ExceptionHandlerFeature : IExceptionHandlerPathFeature, { public Exception Error { get; set; } public string Path { get; set; } }
當(dāng)ExceptionHandlerMiddleware中間件將代碼當(dāng)前請求的HttpContext傳遞給請求處理器之前,它會按照如下所示的方式根據(jù)拋出的異常的原始的請求路徑創(chuàng)建一個ExceptionHandlerFeature對象,該對象最終被添加到HttpContext之上。當(dāng)整個請求處理流程完全結(jié)束之后,ExceptionHandlerMiddleware中間件會借助這個特性得到原始的請求路徑,并將其重新應(yīng)用到當(dāng)前請求上下文上。
public class ExceptionHandlerMiddleware { ... public async Task Invoke(HttpContext context) { try { await _next(context); } catch(Exception ex) { context.Response.StatusCode = 500; var feature = new ExceptionHandlerFeature() { Error = ex, Path = context.Request.Path, }; context.Features.Set<IExceptionHandlerFeature>(feature); context.Features.Set<IExceptionHandlerPathFeature>(feature); if (_options.ExceptionHandlingPath.HasValue) { context.Request.Path = _options.ExceptionHandlingPath; } RequestDelegate handler = _options.ExceptionHandler ?? _next; try { await handler(context); } finally { context.Request.Path = originalPath; } } } }
在具體進(jìn)行異常處理的時候,我們可以從當(dāng)前HttpContext中提取這個ExceptionHandlerFeature對象,進(jìn)而獲取拋出的異常和原始的請求路徑。如下面的代碼所示,我們利用HandleError方法來呈現(xiàn)一個定制的錯誤頁面。在這個方法中,我們正式借助于這個ExceptionHandlerFeature特性得到拋出的異常,并將它的類型、消息以及堆棧追蹤顯示出來。
public class Program { public static void Main() { new WebHostBuilder() .UseKestrel() .ConfigureServices(svcs=>svcs.AddRouting()) .Configure(app => app .UseExceptionHandler("/error") .UseRouter(builder=>builder.MapRoute("error", HandleError)) .Run(context=> Task.FromException(new InvalidOperationException("Manually thrown exception")))) .Build() .Run(); } private async static Task HandleError(HttpContext context) { context.Response.ContentType = "text/html"; Exception ex = context.Features.Get<IExceptionHandlerPathFeature>().Error; await context.Response.WriteAsync("<html><head><title>Error</title></head><body>"); await context.Response.WriteAsync($"<h3>{ex.Message}</h3>"); await context.Response.WriteAsync($"<p>Type: {ex.GetType().FullName}"); await context.Response.WriteAsync($"<p>StackTrace: {ex.StackTrace}"); await context.Response.WriteAsync("</body></html>"); }
在上面這個應(yīng)用中,我們注冊了一個模板為“error”的路由指向這個HandleError方法。對于通過調(diào)用擴(kuò)展方法UseExceptionHandler注冊的ExceptionHandlerMiddleware來說,我們將該路徑設(shè)置為異常處理路徑。那么對于任意從瀏覽器發(fā)出的請求,都會得到如下圖所示的錯誤頁面。
三、清除緩存
對于一個用于獲取資源的GET請求來說,如果請求目標(biāo)是一個相對穩(wěn)定的資源,我們可以采用客戶端緩存的方式避免相同資源的頻繁獲取和傳輸。對于作為資源提供者的Web應(yīng)用來說,當(dāng)它在處理請求的時候,除了將目標(biāo)資源作為響應(yīng)的主體內(nèi)容之外,它還需要設(shè)置用于控制緩存的相關(guān)響應(yīng)報頭。由于緩存在大部分情況下只適用于成功的響應(yīng),如果服務(wù)端在處理請求過程中出現(xiàn)異常,之前設(shè)置的緩存報頭是不應(yīng)該出現(xiàn)在響應(yīng)報文中。對于ExceptionHandlerMiddleware中間件來說,清楚緩存報頭也是它負(fù)責(zé)的一項重要工作。
我們同樣可以通過一個簡單的實例來演示ExceptionHandlerMiddleware中間件針對緩存響應(yīng)報頭的清除。在如下這個應(yīng)用中,我們將針對請求的處理實現(xiàn)在Invoke方法中,它有50%的可能會拋出異常。不論是返回正常的響應(yīng)內(nèi)容還是拋出異常,這個方法都會先設(shè)置一個“Cache-Control”的響應(yīng)報頭,并將緩存時間設(shè)置為1個小時(“Cache-Control: max-age=3600”)。
public class Program { public static void Main() { new WebHostBuilder() .UseKestrel() .ConfigureServices(svcs => svcs.AddRouting()) .Configure(app => app .UseExceptionHandler(builder => builder.Run(async context => await context.Response.WriteAsync("Error occurred!"))) .Run(Invoke)) .Build() .Run(); } private static Random _random = new Random(); private async static Task Invoke(HttpContext context) { context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue { MaxAge = TimeSpan.FromHours(1) }; if (_random.Next() % 2 == 0) { throw new InvalidOperationException("Manually thrown exception..."); } await context.Response.WriteAsync("Succeed..."); } }
通過調(diào)用擴(kuò)展方法 UseExceptionHandler注冊的ExceptionHandlerMiddleware中間件在處理異常時會響應(yīng)一個內(nèi)容為“Error occurred!”的字符串。如下所示的兩個響應(yīng)報文分別對應(yīng)于正常響應(yīng)和拋出異常的情況,我們會發(fā)現(xiàn)程序中設(shè)置的緩存報頭“Cache-Control: max-age=3600”只會出現(xiàn)在狀態(tài)碼為“200 OK”的響應(yīng)中。至于狀態(tài)碼為“500 Internal Server Error”的響應(yīng)中,則會出現(xiàn)三個與緩存相關(guān)的報頭,它們的目的都會為了禁止緩存(或者指示緩存過期)。
HTTP/1.1 200 OK Date: Sat, 17 Dec 2016 14:39:02 GMT Server: Kestrel Cache-Control: max-age=3600 Content-Length: 10 Succeed... HTTP/1.1 500 Internal Server Error Date: Sat, 17 Dec 2016 14:38:39 GMT Server: Kestrel Cache-Control: no-cache Pragma: no-cache Expires: -1 Content-Length: 15 Error occurred!
ExceptionHandlerMiddleware中間件針對緩存響應(yīng)報頭的清除體現(xiàn)在如下所示的代碼片段中。我們可以看出它通過調(diào)用HttpResponse的OnStarting方法注冊了一個回調(diào)(ClearCacheHeaders),上述的這三個緩存報頭在這個回調(diào)中設(shè)置的。除此之外,我們還看到這個回調(diào)方法還會清除ETag報頭,這也很好理解:由于目標(biāo)資源沒有得到正常的響應(yīng),表示資源“簽名”的ETag報頭自然不應(yīng)該出現(xiàn)在響應(yīng)報文中。
public class ExceptionHandlerMiddleware { ... public async Task Invoke(HttpContext context) { try { await _next(context); } catch (Exception ex) { … context.Response.OnStarting(ClearCacheHeaders, context.Response); RequestDelegate handler = _options.ExceptionHandler ?? _next; await handler(context); } } private Task ClearCacheHeaders(object state) { var response = (HttpResponse)state; response.Headers[HeaderNames.CacheControl] = "no-cache"; response.Headers[HeaderNames.Pragma] = "no-cache"; response.Headers[HeaderNames.Expires] = "-1"; response.Headers.Remove(HeaderNames.ETag); return Task.CompletedTask; } }
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
- 詳解ASP.NET Core中間件Middleware
- 理解ASP.NET Core 中間件(Middleware)
- 探究ASP.NET Core Middleware實現(xiàn)方法
- ASP.NET Core Middleware的實現(xiàn)方法詳解
- ASP.NET Core應(yīng)用錯誤處理之StatusCodePagesMiddleware中間件針對響應(yīng)碼呈現(xiàn)錯誤頁面
- ASP.NET Core應(yīng)用錯誤處理之DeveloperExceptionPageMiddleware中間件呈現(xiàn)“開發(fā)者異常頁面”
- 利用Asp.Net Core的MiddleWare思想如何處理復(fù)雜業(yè)務(wù)流程詳解
- ASP.NET?Core使用Middleware設(shè)置有條件允許訪問路由
相關(guān)文章
asp.net 獲取Datalist中Checkbox的值的小結(jié)
最近開發(fā)過程中遇到一個小問題,要獲取checkbox的值,在網(wǎng)上搜索了一下,發(fā)現(xiàn)基本上都是用JS實現(xiàn)的,現(xiàn)在我將自己的做法記錄一下,以便以后繼續(xù)使用。2010-04-04ASP.NET實現(xiàn)可以縮放和旋轉(zhuǎn)的圖片預(yù)覽頁效果
本文詳細(xì)介紹了如何在ASP.NET?WebForms中實現(xiàn)一個功能豐富的圖片預(yù)覽頁面,通過結(jié)合HTML、CSS和JavaScript,用戶可以方便地對圖片進(jìn)行放大、縮小以及旋轉(zhuǎn)操作,感興趣的朋友跟隨小編一起看看吧2024-08-08SQL Server 2008 R2:error 26 開啟遠(yuǎn)程連接詳解
本篇文章小編為大家介紹,SQL Server 2008 R2:error 26 開啟遠(yuǎn)程連接詳解。需要的朋友參考下2013-04-04