解決 ASP.NET Core 自定義錯誤頁面對 Middleware 異常無效的問題


我們基於 Razor Class Library 實現了自定義錯誤頁面的公用類庫(詳見之前的隨筆),但是在實際使用時發現如果在 middleware 中發生了異常,則不能顯示自定義錯誤頁面,而是返回默認的 500 空白頁面。

public static IApplicationBuilder UseCustomErrorPages(this IApplicationBuilder app)
{
    app.UseExceptionHandler("/errors/500");
    app.UseStatusCodePagesWithReExecute("/errors/{0}");
    return app;
}

自定義錯誤頁面使用的是上面的配置,當發生異常時,會走路由 /errors/500 到達對應的自定義錯誤頁面的 mvc action 。 如果是 mvc 中產生異常,能正常到達;但是當 middleware 中產生異常時,在去往自定義錯誤頁面的途中,又途徑異常 middleware ,從而讓自定義錯誤頁面也產生了異常。這就是自定義錯誤頁面不能顯示的原因。

問題的原因想明白了,接下來通過集成測試重現這個問題。

public class ErrorPageTests : IClassFixture<WebApplicationFactory<Startup>>
{  
    [Fact]
    public async Task ErrorPageForMiddleWareException()
    {
        var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services => services.AddMvc());
            builder.Configure(app =>
            {
                app.UseCustomErrorPages();
                app.Use(next => context => throw new Exception("Failed in middleware"));
                app.UseMvcWithDefaultRoute();                    
            });
        }).CreateClient();

        var response = await client.GetAsync("/");
        Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
        var content = await response.Content.ReadAsStringAsync();
        Assert.Contains($"請求失敗:500", content);
    }
}

先寫好測試的好處之一是在嘗試解決方法時可以快速得到反饋,雖然寫測試代碼會花去更多時間,但僅這一點好處就得遠大於失。

有了測試代碼的保駕護航,這時就可以放心大膽的動手嘗試解決問題了。

既然問題的根源是“在去往自定義錯誤頁面的途中,又途徑異常 middleware”,那我們只要抄近路繞過這些 middlewares ,直接奔向 asp.net core mvc 的 middleware ,問題不就解決了嗎?

那怎么繞過去呢?不用 app.UseExceptionHandler ,自己寫個 middleware ?先別做這個啥事,先搞清楚 app.UseExceptionHandler 究竟干了些啥?如果能通過 app.UseExceptionHandler 解決這個問題,豈不更好?

打開 app.UseExceptionHandler 所在的 github 倉庫 Microsoft.AspNetCore.Diagnostics ,找到對應的中間件實現源碼 ExceptionHandlerMiddleware ,下面的刪減后的源碼:

public class ExceptionHandlerMiddleware
{
    //... 
    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            //...
            PathString originalPath = context.Request.Path;
            if (_options.ExceptionHandlingPath.HasValue)
            {
                context.Request.Path = _options.ExceptionHandlingPath;
            }

            try
            {
                //...
                await _options.ExceptionHandler(context);                    
                //...
            }
            //...
        }
    }
}

原來在 ExceptionHandlerMiddleware 中只是將請求路徑修改為 "/errors/500" 然后調用 ExceptionHandler ,但是我們在 app.UseExceptionHandler 只設置了請求路徑,並沒有設置 ExceptionHandler ,那就是默認  ExceptionHandler ,默認的 ExceptionHandler 是什么?

在 ExceptionHandlerMiddleware 的構造函數中找到了答案 —— 請求管線中的下一個中間件

if (_options.ExceptionHandler == null)
{
    //...
    _options.ExceptionHandler = _next;    
}

只要將這里的 ExceptionHandler 修改為返回自定義錯誤頁面的 handler ,問題就能解決。

ExceptionHandler 的類型是 RequestDelegate ,現在問題變成了如何在 RequestDelegate 中執行 mvc action 並返回響應內容?

在這個地方走了一些彎路,開始想通過 IActionResultExecutor 實現,但沒成功。

后來想到最簡單的方法就是利用已有的 mvc 中間件,基於 app.UseMvcWithDefaultRoute() 構建出 RequestDelegate 。基於這個思路,在 ExceptionHandlerExtensions 中以 Action<IApplicationBuilder> 為參數的擴展方法中學到一招:

var subAppBuilder = app.New();
configure(subAppBuilder);
var exceptionHandlerPipeline = subAppBuilder.Build();

return app.UseExceptionHandler(new ExceptionHandlerOptions
{
    ExceptionHandler = exceptionHandlerPipeline
});

原來可以如此簡單地通過 IApplicationBuilder 構建出 RequestDelegate 。

通過學習的這一招完美地解決了問題!

public static IApplicationBuilder UseCustomErrorPages(this IApplicationBuilder app)
{
    var options = new ExceptionHandlerOptions
    {
        ExceptionHandlingPath = "/errors/500",
        ExceptionHandler = app.New().UseMvcWithDefaultRoute().Build()
    };
    app.UseExceptionHandler(options);
    app.UseStatusCodePagesWithReExecute("/errors/{0}");
    return app;
}

 


免責聲明!

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



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