[鐵人賽 Day17] ASP.NET Core 2 系列 – 例外處理 (Exception Handler)

例外處理(Exception Handler)算是程式開發蠻重要的一件事,尤其程式暴露在外,要是不小心顯示了什麼不該讓使用者看到的東西就糟糕了。
要在 ASP.NET Core 做一個通用的 Exception Handler 可以透過 Middleware 或 Filter,但兩者之間的執行週期確大不相同。
本篇將介紹 ASP.NET Core 透過 Middleware 及 Filter 異常處理的差異。

iT 邦幫忙 2018 鐵人賽 – Modern Web 組參賽文章:
[Day17] ASP.NET Core 2 系列 – 例外處理 (Exception Handler)

實做 Exception Handler 前,需要先了解 Middleware 及 Filter 的特性。
可以參考這兩篇:

Exception Filter

Exception Filter 僅能補捉到 Action 及 Action Filter 所發出的 Exception。
其它的類型的 Filter 或 Middleware 產生的 Exception,並沒有辦法透過 Exception Filter 攔截。
如果要做全站的通用的 Exception Handler,可能就沒有這麼合適。

Exception Filter 範例:

ExceptionFilter.cs

1
2
3
4
5
6
7
8
9
10
// ...
public class ExceptionFilter : IAsyncExceptionFilter
{
    public Task OnExceptionAsync(ExceptionContext context)
    {
        context.HttpContext.Response
            .WriteAsync($"{GetType().Name} catch exception. Message: {context.Exception.Message}");
        return Task.CompletedTask;
    }
}

Exception Filter 全域註冊:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
// ...
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(config =>
        {
            config.Filters.Add(new ExceptionFilter());
        });
    }
}

除非註冊了兩個以上的 Exception Filter,不然 Filter 註冊的先後順序並不重要,執行順序是依照 Filter 的類型,同類型的 Filter 才會關係到註冊的先後順序。

Exception Middleware

Middleware 註冊的層級可以在 Filters 的外層,也就是說所有的 Filter 都會經過 Middleware。
如果再把 Exception Middleware 註冊在所有 Middleware 的最外層,就可以變成全站的 Exception Handler。
Exception Handler 層級示意圖如下:ASP.NET Core 教學 - Exception Handler 層級

Exception Middleware 範例:

ExceptionMiddleware.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ...
public class ExceptionMiddleware
{
    private readonly RequestDelegate _next;

    public ExceptionMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await context.Response
                .WriteAsync($"{GetType().Name} catch exception. Message: {ex.Message}");
        }
    }
}

Exception Middleware 全域註冊:

Startup.cs

1
2
3
4
5
6
7
8
9
// ...
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseMiddleware<ExceptionMiddleware>();
        // Other Middleware...
    }
}

Middleware 的註冊順序很重要,越先註冊的會包在越外層。
把 ExceptionMiddleware 註冊在越外層,能涵蓋的範圍就越多。

Exception Handler

ASP.NET Core 有提供 Exception Handler 的 Pipeline,底層就是用上述 Exception Middleware 的做法,在 Application Builder 使用 UseExceptionHandler 指定錯誤頁面。

Startup.cs

1
2
3
4
5
6
7
8
9
// ...
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseExceptionHandler("/error");
        // Other Middleware...
    }
}

用以下範例模擬錯誤發生:

Controllers\HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using Microsoft.AspNetCore.Mvc;

namespace MyWebsite.Controllers
{
    public class HomeController : Controller
    {
        public void Index()
        {
            throw new System.Exception("This is exception sample from Index().");
        }

        [Route("/api/test")]
        public string Test()
        {
            throw new System.Exception("This is exception sample from Test().");
        }

        [Route("/error")]
        public IActionResult Error()
        {
            return View();
        }
    }
}

Views\Shared\Error.cshtml

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
    <head>
        <title>Error</title>
    </head>
    <body>
        <p>This is error page.</p>
    </body>
</html>

當連入 http://localhost:5000/ 發生錯誤後,就會回傳顯示 This is error page. 的頁面。

注意!不會轉址到 http://localhost:5000/error,而是直接回傳 HomeController.Error() 的內容。

ExceptionHandlerOptions

如果網站中混用 Web API,當 API 發生錯誤時,依然回傳 HomeController.Error() 的內容,就會顯得很奇怪。UseExceptionHandler 除了可以指派錯誤頁面外,也可以自己實作錯誤發生的事件。

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ...
public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseExceptionHandler(new ExceptionHandlerOptions()
        {
            ExceptionHandler = async context =>
            {
                bool isApi = Regex.IsMatch(context.Request.Path.Value, "^/api/", RegexOptions.IgnoreCase);
                if (isApi)
                {
                    context.Response.ContentType = "application/json";
                    var json = @"{ ""Message"": ""Internal Server Error"" }";
                    await context.Response.WriteAsync(json);
                    return;
                }
                context.Response.Redirect("/error");
            }
        });
        // Other Middleware...
    }
}

這次特別處理了 API 的錯誤,當連入 http://localhost:5000/api/* 發生錯誤時,就會回傳 JSON 格式的錯誤。

1
2
3
{ 
    "Message": "Internal Server Error" 
}

同時把 MVC 發生錯誤的行為,改用轉址的方式轉到 http://localhost:5000/error

UseDeveloperExceptionPage

通常在開發期間,還是希望能直接看到錯誤資訊,會比較方便除錯。UseDeveloperExceptionPage 是 ASP.NET Core 提供的錯誤資訊頁面服務,可以在 Application Builder 注入。
在 Startup.Configure 注入 IHostingEnvironment 取得環境變數,判斷在開發階段才套用,反之則用 Exception Handler。

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ...
public class Startup
{
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        // 暫時測試可以直接指派環境名稱
        // env.EnvironmentName = EnvironmentName.Development;
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/error");
        }
        // Other Middleware...
    }
}

env.IsDevelopment() 是從 ASPNETCORE_ENVIRONMENT 而來。
詳細情參考這篇:[鐵人賽 Day16] ASP.NET Core 2 系列 – 多重環境組態管理 (Multiple Environments)

開發環境的錯誤資訊頁面如下:

[鐵人賽 Day17] ASP.NET Core 2 系列 - 例外處理(Exception Handler) - UseDeveloperExceptionPage

參考

Introduction to Error Handling in ASP.NET Core