[小菜一碟] 在 ASP.NET Core MVC 自訂 ExceptionHandler

ASP.NET Core MVC 預設的 ExceptionHandler 是幫我們導到 /Home/Error,稍嫌陽春了一點,如果我們要在 Exception 發生時,記錄下額外的資訊,會需要自訂 ExceptionHandler,這個不難,我們來看一下怎麼做?

ExceptionHandlerOptions

我們修改 Startup.cs 改用 UseExceptionHandler() 方法的另一個多載,指定 ExceptionHandlerOptions 物件給它,我們在裡面自訂 Exception 的處理邏輯,發生的 Exception 我們可以透過 IExceptionHandlerFeature 這個 Feature 來取得。

public class Startup
{
    // ...

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseStaticFiles();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            // 防守範圍 400 ~ 599
            app.UseStatusCodePagesWithReExecute("/Home/StatusCodePage/{0}");

            app.UseExceptionHandler(
                new ExceptionHandlerOptions
                {
                    ExceptionHandler = async context =>
                        {
                            var exceptionFeature = context.Features.Get<IExceptionHandlerFeature>();
                            var request = context.Request;

                            var messageBuilder = new StringBuilder();

                            var baseException = exceptionFeature.Error.GetBaseException();

                            messageBuilder.Append($"{baseException.GetType().FullName}: {baseException.Message}");
                            messageBuilder.AppendLine();
                            messageBuilder.AppendLine();
                            messageBuilder.Append($"{request.Method} {request.GetEncodedPathAndQuery()}");
                            messageBuilder.AppendLine();
                            messageBuilder.AppendLine();
                            messageBuilder.Append($"Remote IP: {GetRemoteIpAddress(request)}");
                            messageBuilder.AppendLine();
                            messageBuilder.AppendLine();
                            messageBuilder.Append($"Cookie: {request.Headers[HeaderNames.Cookie]}");
                            messageBuilder.AppendLine();
                            messageBuilder.AppendLine();
                            messageBuilder.Append($"Body: {await ReadBodyAsync(request)}");

                            // Log to somewhere
                        }
                });
        }

        app.Use(
            (context, next) =>
                {
                    // Buffer request for multiple reading.
                    context.Request.EnableBuffering();

                    return next();
                });

        // ...
    }

    private static string GetRemoteIpAddress(HttpRequest request)
    {
        string ipaddress = request.Headers["X-Forwarded-For"];

        if (string.IsNullOrEmpty(ipaddress)) ipaddress = request.HttpContext.Connection.RemoteIpAddress.ToString();

        return ipaddress;
    }

    private async Task<string> ReadBodyAsync(HttpRequest request)
    {
        try
        {
            string body;

            using (var sr = new StreamReader(request.Body, Encoding.UTF8, false, 1024, true))
            {
                body = await sr.ReadToEndAsync();
            }

            request.Body.Seek(0, SeekOrigin.Begin);

            return body;
        }
        catch
        {
            // ignored
        }

        return string.Empty;
    }
}

EnableBuffering()

有一個要特別注意的地方,就是當 Exception 發生的時候,我有去記錄 Request Body,如果我們直接去讀取 Request Body 是讀不到東西的,我們需要為每個 Request EnableBuffering(),讓 Request Body 允許被多次讀取,這樣我們才取得到 Request Body,不過這個會多消耗一點記憶體空間,要權衡一下。

搭配 StatusCodePages Middleware

另外,我在 ExceptionHandler 裡面其實沒有指定,發生 Exception 時要導向到哪一個頁面去,那是因為我在 StatusCodePages Middleware 已經自訂好一個 500(Internal Server Error)的頁面,最終在 Exception 被記錄了之後,會回傳我自訂的 500 頁面。

有關於如何自訂 HTTP 狀態碼頁面,可以參考我的另一篇文章

參考資料

C# 指南ASP.NET 教學ASP.NET MVC 指引
Azure SQL Database 教學SQL Server 教學Xamarin.Forms 教學