前言
DeveloperExceptionPageMiddleware中間件利用呈現(xiàn)出來的錯(cuò)誤頁面實(shí)現(xiàn)拋出異常和當(dāng)前請(qǐng)求的詳細(xì)信息以輔助開發(fā)人員更好地進(jìn)行糾錯(cuò)診斷工作,而ExceptionHandlerMiddleware中間件則是面向最終用戶的,我們可以利用它來顯示一個(gè)友好的定制化的錯(cuò)誤頁面。按照慣例,我們還是先來看看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類似,我們?cè)趧?chuàng)建一個(gè)ExceptionHandlerMiddleware對(duì)象的時(shí)候同樣需要提供一個(gè)攜帶配置選項(xiàng)的對(duì)象,從上面的代碼可以看出這是一個(gè)ExceptionHandlerOptions。具體來說,一個(gè)ExceptionHandlerOptions對(duì)象通過其ExceptionHandler屬性提供了一個(gè)最終用來處理請(qǐng)求的RequestDelegate對(duì)象。如果希望發(fā)生異常后自動(dòng)重定向到某個(gè)指定的路徑,我們可以利用ExceptionHandlerOptions對(duì)象的ExceptionHandlingPath屬性來指定這個(gè)路徑。我們一般會(huì)調(diào)用ApplicationBuilder的擴(kuò)展方法UseExceptionHandler來注冊(cè)ExceptionHandlerMiddleware中間件,這些重載的UseExceptionHandler方法會(huì)采用如下的方式完整中間件的注冊(cè)工作。
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中間件處理請(qǐng)求的本質(zhì)就是在后續(xù)請(qǐng)求處理過程中出現(xiàn)異常的情況下采用注冊(cè)的異常處理器來處理并響應(yīng)請(qǐng)求,這個(gè)異常處理器就是我們?cè)偈煜げ贿^的RequestDelegate對(duì)象。該中間件采用的請(qǐng)求處理邏輯大體上可以通過如下所示的這段代碼來體現(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ù)的請(qǐng)求處理過程中出現(xiàn)異常,ExceptionHandlerMiddleware中間件會(huì)利用一個(gè)作為異常處理器的RequestDelegate對(duì)象來完成最終的請(qǐng)求處理工作。如果在創(chuàng)建ExceptionHandlerMiddleware時(shí)提供的ExceptionHandlerOptions攜帶著這么一個(gè)RequestDelegate對(duì)象,那么它將作為最終使用的異常處理器,否則作為異常處理器的實(shí)際上就是后續(xù)的中間件。換句話說,如果我們沒有通過ExceptionHandlerOptions顯式指定一個(gè)異常處理器,ExceptionHandlerMiddleware中間件會(huì)在后續(xù)管道處理請(qǐng)求拋出異常的情況下將請(qǐng)求再次傳遞給后續(xù)管道。
當(dāng)ExceptionHandlerMiddleware最終利用異常處理器來處理請(qǐng)求之前,它會(huì)對(duì)請(qǐng)求做一些前置處理工作,比如它會(huì)將響應(yīng)狀態(tài)碼設(shè)置為500,比如清空當(dāng)前所有響應(yīng)內(nèi)容等。如果我們利用ExceptionHandlerOptions的ExceptionHandlingPath屬性設(shè)置了一個(gè)重定向路徑,它會(huì)將該路徑設(shè)置為當(dāng)前請(qǐng)求的路徑。除了這些,ExceptionHandlerMiddleware中間件實(shí)際上做了一些沒有反應(yīng)在上面這段代碼片段中的工作。
二、異常的傳遞與請(qǐng)求路徑的恢復(fù)
由于ExceptionHandlerMiddleware中間件總會(huì)利用一個(gè)作為異常處理器的RequestDelegate對(duì)象來完成最終的異常處理工作,為了讓后者能夠得到拋出的異常,該中間件應(yīng)該采用某種方式將異常傳遞給它。除此之外,由于ExceptionHandlerMiddleware中間件會(huì)改變當(dāng)前請(qǐng)求的路徑,當(dāng)整個(gè)請(qǐng)求處理完成之后,它必須將請(qǐng)求路徑恢復(fù)成原始的狀態(tài),否則前置的中間件就無法獲取到正確的請(qǐng)求路徑。
請(qǐng)求處理過程中拋出的異常和原始請(qǐng)求路徑的恢復(fù)是通過相應(yīng)的特性完成的。具體來說,傳遞這兩者的特性分別叫做ExceptionHandlerFeature和ExceptionHandlerPathFeature,對(duì)應(yīng)的接口分別為IExceptionHandlerFeature和IExceptionHandlerPathFeature,如下面的代碼片段所示,后者繼承前者。默認(rèn)使用的ExceptionHandlerFeature實(shí)現(xiàn)了這兩個(gè)接口。
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)前請(qǐng)求的HttpContext傳遞給請(qǐng)求處理器之前,它會(huì)按照如下所示的方式根據(jù)拋出的異常的原始的請(qǐng)求路徑創(chuàng)建一個(gè)ExceptionHandlerFeature對(duì)象,該對(duì)象最終被添加到HttpContext之上。當(dāng)整個(gè)請(qǐng)求處理流程完全結(jié)束之后,ExceptionHandlerMiddleware中間件會(huì)借助這個(gè)特性得到原始的請(qǐng)求路徑,并將其重新應(yīng)用到當(dāng)前請(qǐ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)行異常處理的時(shí)候,我們可以從當(dāng)前HttpContext中提取這個(gè)ExceptionHandlerFeature對(duì)象,進(jìn)而獲取拋出的異常和原始的請(qǐng)求路徑。如下面的代碼所示,我們利用HandleError方法來呈現(xiàn)一個(gè)定制的錯(cuò)誤頁面。在這個(gè)方法中,我們正式借助于這個(gè)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>"); }
在上面這個(gè)應(yīng)用中,我們注冊(cè)了一個(gè)模板為“error”的路由指向這個(gè)HandleError方法。對(duì)于通過調(diào)用擴(kuò)展方法UseExceptionHandler注冊(cè)的ExceptionHandlerMiddleware來說,我們將該路徑設(shè)置為異常處理路徑。那么對(duì)于任意從瀏覽器發(fā)出的請(qǐng)求,都會(huì)得到如下圖所示的錯(cuò)誤頁面。
三、清除緩存
對(duì)于一個(gè)用于獲取資源的GET請(qǐng)求來說,如果請(qǐng)求目標(biāo)是一個(gè)相對(duì)穩(wěn)定的資源,我們可以采用客戶端緩存的方式避免相同資源的頻繁獲取和傳輸。對(duì)于作為資源提供者的Web應(yīng)用來說,當(dāng)它在處理請(qǐng)求的時(shí)候,除了將目標(biāo)資源作為響應(yīng)的主體內(nèi)容之外,它還需要設(shè)置用于控制緩存的相關(guān)響應(yīng)報(bào)頭。由于緩存在大部分情況下只適用于成功的響應(yīng),如果服務(wù)端在處理請(qǐng)求過程中出現(xiàn)異常,之前設(shè)置的緩存報(bào)頭是不應(yīng)該出現(xiàn)在響應(yīng)報(bào)文中。對(duì)于ExceptionHandlerMiddleware中間件來說,清楚緩存報(bào)頭也是它負(fù)責(zé)的一項(xiàng)重要工作。
我們同樣可以通過一個(gè)簡(jiǎn)單的實(shí)例來演示ExceptionHandlerMiddleware中間件針對(duì)緩存響應(yīng)報(bào)頭的清除。在如下這個(gè)應(yīng)用中,我們將針對(duì)請(qǐng)求的處理實(shí)現(xiàn)在Invoke方法中,它有50%的可能會(huì)拋出異常。不論是返回正常的響應(yīng)內(nèi)容還是拋出異常,這個(gè)方法都會(huì)先設(shè)置一個(gè)“Cache-Control”的響應(yīng)報(bào)頭,并將緩存時(shí)間設(shè)置為1個(gè)小時(shí)(“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注冊(cè)的ExceptionHandlerMiddleware中間件在處理異常時(shí)會(huì)響應(yīng)一個(gè)內(nèi)容為“Error occurred!”的字符串。如下所示的兩個(gè)響應(yīng)報(bào)文分別對(duì)應(yīng)于正常響應(yīng)和拋出異常的情況,我們會(huì)發(fā)現(xiàn)程序中設(shè)置的緩存報(bào)頭“Cache-Control: max-age=3600”只會(huì)出現(xiàn)在狀態(tài)碼為“200 OK”的響應(yīng)中。至于狀態(tài)碼為“500 Internal Server Error”的響應(yīng)中,則會(huì)出現(xiàn)三個(gè)與緩存相關(guān)的報(bào)頭,它們的目的都會(huì)為了禁止緩存(或者指示緩存過期)。
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中間件針對(duì)緩存響應(yīng)報(bào)頭的清除體現(xiàn)在如下所示的代碼片段中。我們可以看出它通過調(diào)用HttpResponse的OnStarting方法注冊(cè)了一個(gè)回調(diào)(ClearCacheHeaders),上述的這三個(gè)緩存報(bào)頭在這個(gè)回調(diào)中設(shè)置的。除此之外,我們還看到這個(gè)回調(diào)方法還會(huì)清除ETag報(bào)頭,這也很好理解:由于目標(biāo)資源沒有得到正常的響應(yīng),表示資源“簽名”的ETag報(bào)頭自然不應(yīng)該出現(xiàn)在響應(yīng)報(bào)文中。
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é)
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com