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 狀態碼頁面,可以參考我的另一篇文章。
參考資料
- 使用 Enablebuffering 多次讀取 Asp Net Core 請求體
- How to read request body in an asp.net core webapi controller?
- HttpRequestRewindExtensions.EnableBuffering Method