ASP.NET Core Exception Handle & RFC 7807 Problem Details for HTTP APIs
2024-07-08
筆記 ASP.NET Core Exception Hadnel 與 RFC 7807 Problem Details for HTTP APIs 在
說明
Exception Handle 的設計目標:
- Production 不要出現 Exception 的 Stack Trace 避免暴露資訊
- Development 可以看到 Exception 的 Stack Trace 方便 Debug
- Response 要符合 RFC 7807 Problem Details for HTTP APIs
- 針對 HTTP Status Code 4xx, 5xx 的 Exception 進行處理
設計
為了要在 Development 環境可以看到 Exception 的 Stack Trace,可以透過 UseDeveloperExceptionPage
Middleware 來達成,而在 .NET 8 當中預設已經有加入這個 Middleware,在 Development 環境下會自動加入,但是在 Production 環境下不會加入。
簡易驗證的程式碼:
app.MapGet("/Exception", () =>
{
throw new NotImplementedException();
});
如果要測試 Production 環境下的效果,可以透過 ASPNETCORE_ENVIRONMENT
或直接在 Program.cs
設定 builder.Environment
來進行設定。
app.Environment.EnvironmentName = Environments.Production;
所以預設上不需要特別調整 Program.cs
就可達到 Exception Stack Trace 針對 Development 環境顯示,Production 環境不顯示的效果。
而如果要再加上符合 RFC 7807 Problem Details for HTTP APIs 的 Response,可以透過 UseProblemDetails
Middleware 來達成,這個 Middleware 會將 Exception 轉換成符合 RFC 7807 的 Response。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
var app = builder.Build();
完成以上的設定對於 HTTP Status Code 5xx 的處理已經完成,但是對於 HTTP Status Code 4xx 的處理則需要另外處理。
app.UseStatusCodePages(async statusCodeContext =>
{
await Results.Problem(statusCode: statusCodeContext.HttpContext.Response.StatusCode).ExecuteAsync(statusCodeContext.HttpContext);
});
app.Run();
Results.Problem
能夠與 AddProblemDetails
一樣產生符合 RFC 7807 的 Response,這樣就能夠將 HTTP Status Code 4xx 的 Exception 也轉換成符合 RFC 7807 的 Response。
同時 Problem
所提供的參數,例如 title
, detail
, instance
來呈現更多的資訊。
而如果想要針對 Exception 有更多客製化處理需求,可以透過 UseExceptionHandler
Middleware 來進行處理。
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exceptionHandlerFeature = context.Features.Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>();
if (exceptionHandlerFeature?.Error is Exception ex)
{
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An unhandled exception occurred.");
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = MediaTypeNames.Application.Json;
var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "An error occurred while processing your request.",
Detail = "Please try again later.",
Instance = context.Request.Path
};
var json = JsonSerializer.Serialize(problemDetails);
await context.Response.WriteAsync(json);
}
});
});
TLDR
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exceptionHandlerFeature = context.Features.Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>();
if (exceptionHandlerFeature?.Error is Exception ex)
{
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An unhandled exception occurred.");
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = MediaTypeNames.Application.Json;
var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "An error occurred while processing your request.",
Detail = "Please try again later.",
Instance = context.Request.Path
};
var json = JsonSerializer.Serialize(problemDetails);
await context.Response.WriteAsync(json);
}
});
});
}
app.UseStatusCodePages(async statusCodeContext =>
{
await Results.Problem(statusCode: statusCodeContext.HttpContext.Response.StatusCode).ExecuteAsync(statusCodeContext.HttpContext);
});