ASP.NET Core錯誤處理中間件[4]: 響應狀態碼錯誤頁面


StatusCodePagesMiddleware中間件與ExceptionHandlerMiddleware中間件類似,它們都是在后續請求處理過程中“出錯”的情況下利用一個錯誤處理器來接收針對當前請求的處理。它們之間的差異在於對“錯誤”的認定上:ExceptionHandlerMiddleware中間件所謂的錯誤就是拋出異常;StatusCodePagesMiddleware中間件則將400~599的響應狀態碼視為錯誤。更多關於ASP.NET Core的文章請點這里]

目錄
一、StatusCodePagesMiddleware
二、阻止處理異常
三、UseStatusCodePages
四、UseStatusCodePagesWithRedirects
五、UseStatusCodePagesWithReExecute

一、StatusCodePagesMiddleware

如下面的代碼片段所示,StatusCodePagesMiddleware中間件也采用“標准”的定義方式,針對它的配置選項通過一個對應的對象以Options模式的形式提供給它。

public class StatusCodePagesMiddleware
{
    public StatusCodePagesMiddleware(RequestDelegate next, IOptions<StatusCodePagesOptions> options);
    public Task Invoke(HttpContext context);
}

除了對錯誤的認定方式,StatusCodePagesMiddleware中間件和ExceptionHandlerMiddleware中間件對錯誤處理器的表達也不相同。ExceptionHandlerMiddleware中間件的處理器是一個RequestDelegate委托對象,而StatusCodePagesMiddleware中間件的處理器則是一個Func<StatusCodeContext, Task>委托對象。如下面的代碼片段所示,配置選項StatusCodePagesOptions的唯一目的就是提供作為處理器的Func<StatusCodeContext, Task>對象。

public class StatusCodePagesOptions
{
    public Func<StatusCodeContext, Task> HandleAsync { get; set; }
}

一個RequestDelegate對象相當於一個Func<HttpContext, Task>類型的委托對象,而一個StatusCodeContext對象也是對一個HttpContext上下文的封裝,這兩個委托對象並沒有本質上的不同。如下面的代碼片段所示,除了從StatusCodeContext對象中獲取當前HttpContext上下文,我們還可以通過其Next屬性得到一個RequestDelegate對象,並利用它將請求再次分發給后續中間件進行處理。StatusCodeContext對象的Options屬性返回創建 StatusCodePagesMiddleware中間件時指定的StatusCodePagesOptions對象。

public class StatusCodeContext
{
    public HttpContext HttpContext { get; }
    public RequestDelegate Next { get; }
    public StatusCodePagesOptions Options { get; }

    public StatusCodeContext(HttpContext context, StatusCodePagesOptions options, RequestDelegate next);
}

由於采用了針對響應狀態碼的錯誤處理策略,所以實現在StatusCodePagesMiddleware中間件的錯誤處理操作只會發生在當前響應狀態碼為400~599的情況下,如下所示的代碼片段就體現了這一點。從下面給出的代碼片段可以看出,StatusCodePagesMiddleware中間件除了會查看當前響應狀態碼,還會查看響應內容及媒體類型。如果響應報文已經包含響應內容或者設置了媒體類型,StatusCodePagesMiddleware中間件將不會執行任何操作,因為這正是后續中間件管道希望回復給客戶端的響應,該中間件不應該再畫蛇添足。

public class StatusCodePagesMiddleware
{
    private RequestDelegate _next;
    private StatusCodePagesOptions _options;

    public StatusCodePagesMiddleware(RequestDelegate next, IOptions<StatusCodePagesOptions> options)
    {
        _next = next;
        _options = options.Value;
    }

    public async Task Invoke(HttpContext context)
    {
        await _next(context);
        var response = context.Response;
        if ((response.StatusCode >= 400 && response.StatusCode <= 599) && !response.ContentLength.HasValue && string.IsNullOrEmpty(response.ContentType))
        {
            await _options.HandleAsync(new StatusCodeContext(context, _options, _next));
        }
    }
}

StatusCodePagesMiddleware中間件對錯誤的處理非常簡單,它只需要從StatusCodePagesOptions對象中提取出作為錯誤處理器的Func<StatusCodeContext, Task>對象,然后創建一個StatusCodeContext對象作為輸入參數調用這個委托對象即可。

二、阻止處理異常

通過《呈現錯誤信息》的內容我們知道,如果某些內容已經被寫入響應的主體部分,或者響應的媒體類型已經被預先設置,StatusCodePagesMiddleware中間件就不會再執行任何錯誤處理操作。由於應用程序往往具有自身的異常處理策略,它們可能會顯式地返回一個狀態碼為400~599的響應,在此情況下,StatusCodePagesMiddleware中間件是不應該對當前響應做任何干預的。從這個意義上來講,StatusCodePagesMiddleware中間件僅僅是作為一種后備的錯誤處理機制而已。

更進一步來講,如果后續的某個中間件返回了一個狀態碼為400~599的響應,並且這個響應只有報頭集合沒有主體(媒體類型自然也不會設置),那么按照我們在上面給出的錯誤處理邏輯來看,StatusCodePagesMiddleware中間件還是會按照自己的策略來處理並響應請求。為了解決這種情況,我們必須賦予后續中間件能夠阻止StatusCodePagesMiddleware中間件進行錯誤處理的功能。

阻止StatusCodePagesMiddleware中間件進行錯誤處理的功能是借助一個通過IStatusCodePagesFeature接口表示的特性來實現的。如下面的代碼片段所示,IStatusCodePagesFeature接口定義了唯一的Enabled屬性,StatusCodePagesFeature類型是對該接口的默認實現,它的Enabled屬性默認返回True。

public interface IStatusCodePagesFeature
{
    bool Enabled { get; set; }
}

public class StatusCodePagesFeature : IStatusCodePagesFeature
{
    public bool Enabled { get; set; } = true ;
}

StatusCodePagesMiddleware中間件在將請求交付給后續管道之前,會創建一個StatusCodePagesFeature對象,並將其添加到當前HttpContext上下文的特性集合中。在最終決定是否執行錯誤處理操作的時候,它還會通過這個特性檢驗后續的某個中間件是否不希望其進行不必要的錯誤處理,如下所示的代碼片段很好地體現了這一點。

public class StatusCodePagesMiddleware
{
    ...
    public async Task Invoke(HttpContext context)
    {
        var feature = new StatusCodePagesFeature();
        context.Features.Set<IStatusCodePagesFeature>(feature);

        await _next(context);
        var response = context.Response;
        if ((response.StatusCode >= 400 && response.StatusCode <= 599) && !response.ContentLength.HasValue && string.IsNullOrEmpty(response.ContentType) && feature.Enabled)
        {
            await _options.HandleAsync(new StatusCodeContext(context, _options, _next));
        }
    }
}

下面通過一個簡單的實例來演示如何利用StatusCodePagesFeature特性來屏蔽StatusCodePagesMiddleware中間件。在如下所示的代碼片段中,我們將針對請求的處理定義在ProcessAsync方法中,該方法會返回一個狀態碼為“401 Unauthorized”的響應。我們通過隨機數讓這個方法在50%的概率下利用StatusCodePagesFeature特性來阻止StatusCodePagesMiddleware中間件自身對錯誤的處理。我們通過調用UseStatusCodePages擴展方法注冊的StatusCodePagesMiddleware中間件會直接響應一個內容為“Error occurred!”的字符串。

public class Program
{
    private static readonly Random _random = new Random();
    public static void Main()
    {
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder.Configure(app => app
                .UseStatusCodePages(HandleAsync)
                .Run(ProcessAsync)))
            .Build()
            .Run();

        static Task HandleAsync(StatusCodeContext context) => context.HttpContext.Response.WriteAsync("Error occurred!");

        static Task ProcessAsync(HttpContext context)
        {
            context.Response.StatusCode = 401;
            if (_random.Next() % 2 == 0)
            {
                context.Features.Get<IStatusCodePagesFeature>().Enabled = false;
            }
            return Task.CompletedTask;
        }

    }
}

對於針對該應用的請求來說,我們會得到如下兩種不同的響應。沒有主體內容的響應是通過ProcessAsync方法產生的,這種情況發生在StatusCodePagesMiddleware中間件通過StatusCodePagesFeature特性被屏蔽的時候。有主體內容的響應則是ProcessAsync方法和StatusCodePagesMiddleware中間件共同作用的結果。

HTTP/1.1 401 Unauthorized
Date: Sat, 21 Sep 2019 13:37:31 GMT
Server: Kestrel
Content-Length: 15

Error occurred!
HTTP/1.1 401 Unauthorized
Date: Sat, 21 Sep 2019 13:37:36 GMT
Server: Kestrel
Content-Length: 0

我們在大部分情況下都會調用IApplicationBuilder接口相應的擴展方法來注冊StatusCodePagesMiddleware中間件。對於StatusCodePagesMiddleware中間件的注冊來說,除了UseStatusCodePages方法,還有其他方法可供選擇。

三、UseStatusCodePages

我們可以調用如下所示的3個UseStatusCodePages擴展方法重載來注冊StatusCodePagesMiddleware中間件。不論調用哪個重載,系統最終都會根據提供的StatusCodePagesOptions對象調用構造函數來創建這個中間件,而且StatusCodePagesOptions必須具有一個作為錯誤處理器的Func<StatusCodeContext, Task>對象。

public static class StatusCodePagesExtensions
{   
    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app)
        => app.UseMiddleware<StatusCodePagesMiddleware>();

    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, StatusCodePagesOptions options)
        => app.UseMiddleware<StatusCodePagesMiddleware>(Options.Create(options)); 
    
    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, Func<StatusCodeContext, Task> handler)
        => app.UseStatusCodePages(new StatusCodePagesOptions
        {
            HandleAsync = handler
        });
}

由於StatusCodePagesMiddleware中間件最終的目的還是將定制的錯誤信息響應給客戶端,所以可以在注冊該中間件時直接指定響應的內容和媒體類型,這樣的注冊方式可以通過調用如下所示的UseStatusCodePages方法來完成。從如下所示的代碼片段可以看出,通過參數bodyFormat指定的實際上是一個模板,它可以包含一個表示響應狀態碼的占位符({0})。

public static class StatusCodePagesExtensions
{   
    public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, string contentType, string bodyFormat)
    {
        return app.UseStatusCodePages(context =>
        {
            var body = string.Format(CultureInfo.InvariantCulture, bodyFormat, context.HttpContext.Response.StatusCode);
            context.HttpContext.Response.ContentType = contentType;
            return context.HttpContext.Response.WriteAsync(body);
        });
    }
}

四、UseStatusCodePagesWithRedirects

如果調用UseStatusCodePagesWithRedirects擴展方法,就可以使注冊的StatusCodePagesMiddleware中間件向指定的路徑發送一個客戶端重定向。從如下所示的代碼片段可以看出,參數locationFormat指定的重定向地址也是一個模板,它可以包含一個表示響應狀態碼的占位符({0})。我們可以指定一個完整的地址,也可以指定一個相對於PathBase的相對路徑,后者需要包含表示基地址的前綴“~/”。

public static class StatusCodePagesExtensions
{       
    public static IApplicationBuilder UseStatusCodePagesWithRedirects(this IApplicationBuilder app, string locationFormat)
    {
        if (locationFormat.StartsWith("~"))
        {
            locationFormat = locationFormat.Substring(1);
            return app.UseStatusCodePages(context =>
            {
                var location = string.Format(CultureInfo.InvariantCulture, locationFormat, context.HttpContext.Response.StatusCode);
                context.HttpContext.Response.Redirect(context.HttpContext.Request.PathBase + location);
                return Task.CompletedTask;
            });
        }
        else
        {
            return app.UseStatusCodePages(context =>
            {
                var location = string.Format(CultureInfo.InvariantCulture, locationFormat, context.HttpContext.Response.StatusCode);
                context.HttpContext.Response.Redirect(location);
                return Task.CompletedTask;
            });
        }
    }
}

下面通過一個簡單的應用來演示針對客戶端重定向的錯誤頁面呈現方式。我們在如下所示的應用中注冊了一個路由模板為“error/{statuscode}”的路由,路由參數statuscode代表響應的狀態碼。在作為路由處理器的HandleAsync方法中,我們會直接響應一個包含狀態碼的字符串。我們調用UseStatusCodePagesWithRedirects方法注冊StatusCodePagesMiddleware中間件時將重定義路徑設置為“error/{0}”。

public class Program
{
    private static readonly Random _random = new Random();
    public static void Main()
    {
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder
                .ConfigureServices(svcs => svcs.AddRouting())
                .Configure(app => app
                    .UseStatusCodePagesWithRedirects("~/error/{0}")
                    .UseRouting()
                    .UseEndpoints(endpoints => endpoints.MapGet("error/{statuscode}", HandleAsync))
                    .Run(ProcessAsync)))
            .Build()
            .Run();

        static async Task HandleAsync(HttpContext context)
        {
            var statusCode = context.GetRouteData().Values["statuscode"];
            await context.Response.WriteAsync($"Error occurred ({statusCode})");
        }

        static Task ProcessAsync(HttpContext context)
        {
            context.Response.StatusCode = _random.Next(400, 599);
            return Task.CompletedTask;
        }
    }
}

針對該應用的請求總是得到一個狀態碼為400~599的響應,StatusCodePagesMiddleware中間件在此情況下會向指定的路徑(“~/error/{statuscode}”)發送一個客戶端重定向。由於重定向請求的路徑與注冊的路由相匹配,所以作為路由處理器的HandleError方法會響應下圖所示的錯誤頁面。

16-11

五、UseStatusCodePagesWithReExecute

除了可以采用客戶端重定向的方式來呈現錯誤頁面,還可以調用UseStatusCodePagesWithReExecute方法注冊StatusCodePagesMiddleware中間件,並讓它采用服務端重定向的方式來處理錯誤請求。如下面的代碼片段所示,當我們調用這個方法的時候不僅可以指定重定向的路徑,還可以指定查詢字符串。這里作為重定向地址的參數pathFormat依舊是一個路徑模板,它可以包含一個表示響應狀態碼的占位符({0})。

public static class StatusCodePagesExtensions
{
    public static IApplicationBuilder UseStatusCodePagesWithReExecute( this IApplicationBuilder app, string pathFormat, string queryFormat = null);
}

現在我們對前面演示的這個實例略做修改來演示采用服務端重定向呈現的錯誤頁面。如下面的代碼片段所示,我們將針對UseStatusCodePagesWithRedirects方法的調用替換成針對UseStatusCodePagesWithReExecute方法的調用。

public class Program
{
    private static readonly Random _random = new Random();
    public static void Main()
    {
        Host.CreateDefaultBuilder()
            .ConfigureWebHostDefaults(builder => builder
                .ConfigureServices(svcs => svcs.AddRouting())
                .Configure(app => app
                    .UseStatusCodePagesWithReExecute("/error/{0}")
                    .UseRouting()
                    .UseEndpoints(endpoints => endpoints.MapGet("error/{statuscode}", HandleAsync))
                    .Run(ProcessAsync)))
            .Build()
            .Run();

        static async Task HandleAsync(HttpContext context)
        {
            var statusCode = context.GetRouteData().Values["statuscode"];
            await context.Response.WriteAsync($"Error occurred ({statusCode})");
        }

        static Task ProcessAsync(HttpContext context)
        {
            context.Response.StatusCode = _random.Next(400, 599);
            return Task.CompletedTask;
        }
    }
}

對於前面演示的實例,由於錯誤頁面是通過客戶端重定向的方式呈現的,所以瀏覽器地址欄顯示的是重定向地址。我們在選擇這個實例時采用了服務端重定向,雖然顯示的頁面內容並沒有不同,但是地址欄上的地址是不會發生改變的,如下圖所示。(S1615)

16-12

之所以命名為UseStatusCodePagesWithReExecute,是因為通過這個方法注冊的StatusCodePagesMiddleware中間件進行錯誤處理時,它僅僅將提供的重定向路徑和查詢字符串應用到當前HttpContext上下文,然后分發給后續管道重新執行。UseStatusCodePagesWithReExecute方法中注冊StatusCodePagesMiddleware中間件的實現總體上可以由如下所示的代碼片段來體現。

public static class StatusCodePagesExtensions
{    
    public static IApplicationBuilder UseStatusCodePagesWithReExecute( this IApplicationBuilder app, string pathFormat, string queryFormat = null)
    {
        return app.UseStatusCodePages(async context =>
        {
            var newPath = new PathString(string.Format(CultureInfo.InvariantCulture, pathFormat, context.HttpContext.Response.StatusCode));
            var formatedQueryString = queryFormat == null ? null : string.Format(CultureInfo.InvariantCulture, queryFormat, context.HttpContext.Response.StatusCode);
            
            context.HttpContext.Request.Path = newPath;
            context.HttpContext.Request.QueryString = newQueryString;
            await context.Next(context.HttpContext);
        });
    }
}

與ExceptionHandlerMiddleware中間件類似,StatusCodePagesMiddleware中間件在處理請求的過程中會改變當前請求上下文的狀態,具體體現在它會將指定的請求路徑和查詢字符串重新應用到當前請求上下文中。為了不影響前置中間件對請求的正常處理,StatusCodePagesMiddleware中間件在完成自身處理流程之后必須將當前請求上下文恢復到原始狀態。StatusCodePagesMiddleware中間件依舊采用一個特性來保存原始路徑和查詢字符串。這個特性對應的接口是具有如下定義的IStatusCodeReExecuteFeature,但是該接口僅僅包含兩個針對路徑的屬性,並沒有用於攜帶原始請求上下文的屬性,但是默認實現類型StatusCodeReExecuteFeature包含了這個屬性。

public interface IStatusCodeReExecuteFeature
{
    string OriginalPath { get; set; }
    string OriginalPathBase { get; set; }
}

public class StatusCodeReExecuteFeature : IStatusCodeReExecuteFeature
{
    public string OriginalPath { get; set; }
    public string OriginalPathBase { get; set; }
    public string OriginalQueryString { get; set; }
}

在StatusCodePagesMiddleware中間件處理異常請求的過程中,在將指定的重定向路徑和查詢字符串應用到當前請求上下文之前,它會根據原始的上下文創建一個StatusCodeReExecuteFeature特性對象,並將其添加到當前HttpContext上下文的特性集合中。當整個請求處理過程結束之后,StatusCodePagesMiddleware中間件還會將這個特性從當前HttpContext上下文中移除,並恢復原始的請求路徑和查詢字符串。如下所示的代碼片段體現了UseStatusCodePagesWithReExecute方法的實現邏輯。

public static class StatusCodePagesExtensions
{
    public static IApplicationBuilder UseStatusCodePagesWithReExecute( this IApplicationBuilder app,string pathFormat, string queryFormat = null)
    {    
        return app.UseStatusCodePages(async context =>
        {
            var newPath = new PathString( string.Format(CultureInfo.InvariantCulture, pathFormat, context.HttpContext.Response.StatusCode));
            var formatedQueryString = queryFormat == null ? null : string.Format(CultureInfo.InvariantCulture, queryFormat, context.HttpContext.Response.StatusCode);
            var newQueryString = queryFormat == null ? QueryString.Empty : new QueryString(formatedQueryString);

            var originalPath = context.HttpContext.Request.Path;
            var originalQueryString = context.HttpContext.Request.QueryString;

            context.HttpContext.Features.Set<IStatusCodeReExecuteFeature>(new StatusCodeReExecuteFeature()
            {
                OriginalPathBase = context.HttpContext.Request.PathBase.Value,
                OriginalPath = originalPath.Value,
                OriginalQueryString = originalQueryString.HasValue ? originalQueryString.Value : null,
            });

            context.HttpContext.Request.Path = newPath;
            context.HttpContext.Request.QueryString = newQueryString;
            try
            {
                await context.Next(context.HttpContext);
            }
            finally
            {
                context.HttpContext.Request.QueryString = originalQueryString;
                context.HttpContext.Request.Path = originalPath;
                context.HttpContext.Features.Set<IStatusCodeReExecuteFeature>(null);
            }
        });
    }
}

ASP.NET Core錯誤處理中間件[1]: 呈現錯誤信息
ASP.NET Core錯誤處理中間件[2]: 開發者異常頁面
ASP.NET Core錯誤處理中間件[3]: 異常處理器
ASP.NET Core錯誤處理中間件[4]: 響應狀態碼頁面


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM