Xử lý lỗi trong ASP.NET Core APIs
Bài viết này mô tả cách xử lý lỗi trong ASP.NET Core APIs, bao gồm cả Minimal APIs và controller-based APIs.
Developer Exception Page (Trang ngoại lệ dành cho nhà phát triển)
Developer Exception Page (Trang ngoại lệ dành cho nhà phát triển) hiển thị thông tin chi tiết về các exception (ngoại lệ) yêu cầu chưa được xử lý. Nó sử dụng DeveloperExceptionPageMiddleware để bắt các exception đồng bộ và bất đồng bộ từ HTTP pipeline và tạo ra các response lỗi. Developer exception page chạy sớm trong middleware pipeline để có thể bắt các exception chưa được xử lý từ middleware phía sau.
Các ứng dụng ASP.NET Core bật developer exception page theo mặc định khi cả hai điều kiện sau:
- Đang chạy trong môi trường
Development. - Ứng dụng được tạo với các template hiện tại, tức là sử dụng WebApplication.CreateBuilder.
Ứng dụng được tạo bằng các template cũ hơn, tức là sử dụng WebHost.CreateDefaultBuilder, có thể bật developer exception page bằng cách gọi app.UseDeveloperExceptionPage.
Cảnh báo: Không bật Developer Exception Page trừ khi ứng dụng đang chạy trong môi trường Development. Không chia sẻ thông tin exception chi tiết công khai khi ứng dụng chạy trong môi trường production. Để biết thêm về cấu hình môi trường, xem ASP.NET Core runtime environments.
Developer Exception Page có thể bao gồm thông tin sau về exception và yêu cầu:
- Stack trace (dấu vết ngăn xếp)
- Tham số query string (nếu có)
- Cookies (nếu có)
- Headers (tiêu đề)
- Endpoint metadata (siêu dữ liệu endpoint, nếu có)
Developer Exception Page không đảm bảo cung cấp bất kỳ thông tin nào. Sử dụng Logging để có thông tin lỗi đầy đủ.
Để xem Developer Exception Page trong Minimal API:
- Chạy ứng dụng mẫu trong môi trường
Development. - Điều hướng đến endpoint
/exception.
Phần này tham chiếu ứng dụng mẫu sau để minh họa cách xử lý exception trong Minimal API. Nó ném một exception khi endpoint /exception được yêu cầu:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/exception", () =>
{
throw new InvalidOperationException("Sample Exception");
});
app.MapGet("/", () => "Test by calling /exception");
app.Run();Exception handler (Xử lý ngoại lệ)
Trong các môi trường không phải development, hãy sử dụng Exception Handler Middleware để tạo ra payload (tải trọng) lỗi.
Với Minimal APIs
Để cấu hình Exception Handler Middleware, gọi UseExceptionHandler. Ví dụ, đoạn code sau thay đổi ứng dụng để phản hồi với payload tuân thủ RFC 7807 cho client. Xem phần Problem Details phía dưới để biết thêm thông tin.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseExceptionHandler(exceptionHandlerApp
=> exceptionHandlerApp.Run(async context
=> await Results.Problem()
.ExecuteAsync(context)));
app.MapGet("/exception", () =>
{
throw new InvalidOperationException("Sample Exception");
});
app.MapGet("/", () => "Test by calling /exception");
app.Run();Với Controller-based APIs
- Trong
Program.cs, gọi UseExceptionHandler để thêm Exception Handling Middleware:
```csharp var app = builder.Build();
app.UseHttpsRedirection();
if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/error"); }
app.UseAuthorization();
app.MapControllers();
app.Run(); ```
- Cấu hình controller action để phản hồi với route
/error:
``csharp [Route("/error")] public IActionResult HandleError() => Problem(); ``
Action HandleError trên gửi payload tuân thủ RFC 7807 cho client.
Cảnh báo: Không đánh dấu action method xử lý lỗi bằng các thuộc tính HTTP method như HttpGet. Các verb (động từ) tường minh ngăn một số yêu cầu đến được action method.
Với các web API sử dụng Swagger / OpenAPI, hãy đánh dấu action xử lý lỗi bằng thuộc tính [\[ApiExplorerSettings\]](/en-us/dotnet/api/microsoft.aspnetcore.mvc.apiexplorersettingsattribute) và đặt thuộc tính IgnoreApi thành true. Cấu hình này loại trừ action xử lý lỗi khỏi đặc tả OpenAPI của ứng dụng:
``csharp
[ApiExplorerSettings(IgnoreApi = true)]
``
Cho phép truy cập ẩn danh đến phương thức nếu người dùng chưa xác thực nên thấy lỗi.
Phản hồi lỗi Client và Server
Với Minimal APIs
Xem xét Minimal API app sau:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/users/{id:int}", (int id)
=> id <= 0 ? Results.BadRequest() : Results.Ok(new User(id)));
app.MapGet("/", () => "Test by calling /users/{id:int}");
app.Run();
public record User(int Id);Endpoint /users tạo ra 200 OK với biểu diễn json của User khi id lớn hơn 0, nếu không là status code 400 BAD REQUEST không có response body.
Status Code Pages middleware có thể được cấu hình để tạo nội dung body chung, khi trống, cho tất cả các HTTP client (400-499) hoặc server (500-599) response. Middleware được cấu hình bằng cách gọi extension method UseStatusCodePages.
Ví dụ sau thay đổi ứng dụng để phản hồi với payload tuân thủ RFC 7807 cho client với tất cả client và server response, bao gồm cả lỗi routing (ví dụ: 404 NOT FOUND):
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseStatusCodePages(async statusCodeContext
=> await Results.Problem(statusCode: statusCodeContext.HttpContext.Response.StatusCode)
.ExecuteAsync(statusCodeContext.HttpContext));
app.MapGet("/users/{id:int}", (int id)
=> id <= 0 ? Results.BadRequest() : Results.Ok(new User(id)) );
app.MapGet("/", () => "Test by calling /users/{id:int}");
app.Run();
public record User(int Id);Với Controller-based APIs
Đối với controller-based APIs, response lỗi có thể được cấu hình theo một trong các cách sau:
- Sử dụng problem details service
- Triển khai ProblemDetailsFactory
- Sử dụng ApiBehaviorOptions.ClientErrorMapping
Một error result (kết quả lỗi) được định nghĩa là kết quả có HTTP status code từ 400 trở lên. Đối với web API controller, MVC biến đổi error result để tạo ra ProblemDetails.
Việc tự động tạo ProblemDetails cho các status code lỗi được bật theo mặc định.
Problem details (Chi tiết vấn đề)
Problem Details không phải là định dạng response duy nhất để mô tả lỗi HTTP API, tuy nhiên chúng thường được dùng để báo cáo lỗi cho HTTP APIs.
Problem details service triển khai interface IProblemDetailsService, hỗ trợ tạo problem details trong ASP.NET Core. Extension method AddProblemDetails(IServiceCollection) trên IServiceCollection đăng ký triển khai mặc định của IProblemDetailsService.
Trong các ứng dụng ASP.NET Core, middleware sau tạo ra các HTTP response problem details khi AddProblemDetails được gọi, ngoại trừ khi header HTTP Accept không bao gồm một trong các content type được hỗ trợ bởi IProblemDetailsWriter đã đăng ký (mặc định: application/json):
- ExceptionHandlerMiddleware: Tạo problem details response khi không có custom handler nào được xác định.
- StatusCodePagesMiddleware: Tạo problem details response theo mặc định.
- DeveloperExceptionPageMiddleware: Tạo problem details response trong môi trường development khi header HTTP request
Acceptkhông bao gồmtext/html.
Với Minimal APIs
Ứng dụng Minimal API có thể được cấu hình để tạo ra problem details response cho tất cả HTTP client và server error response chưa có body content bằng cách sử dụng extension method AddProblemDetails.
Đoạn code sau cấu hình ứng dụng để tạo problem details:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseExceptionHandler();
app.UseStatusCodePages();
app.MapGet("/users/{id:int}", (int id)
=> id <= 0 ? Results.BadRequest() : Results.Ok(new User(id)));
app.MapGet("/", () => "Test by calling /users/{id:int}");
app.Run();
public record User(int Id);Fallback của IProblemDetailsService
Trong đoạn code sau, httpContext.Response.WriteAsync("Fallback: An error occurred.") trả về lỗi nếu triển khai IProblemDetailsService không thể tạo ra ProblemDetails:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async httpContext =>
{
var pds = httpContext.RequestServices.GetService<IProblemDetailsService>();
if (pds == null
|| !await pds.TryWriteAsync(new() { HttpContext = httpContext }))
{
// Hành vi fallback
await httpContext.Response.WriteAsync("Fallback: An error occurred.");
}
});
});
app.MapGet("/exception", () =>
{
throw new InvalidOperationException("Sample Exception");
});
app.MapGet("/", () => "Test by calling /exception");
app.Run();Đoạn code trên:
- Ghi thông báo lỗi với code fallback nếu
problemDetailsServicekhông thể ghiProblemDetails. Ví dụ: một endpoint mà header Accept chỉ định media type màDefaultProblemDetailsWriterkhông hỗ trợ. - Sử dụng Exception Handler Middleware.
Lưu ý: DefaultProblemDetailsWriter hỗ trợ các media type sau trong header Accept:
- application/json
- application/problem+json
- Các wildcard type như */* và application/*
Các media type không phải JSON như application/xml hoặc text/html không được hỗ trợ và kích hoạt hành vi fallback.
Với Controller-based APIs
Problem details service
ASP.NET Core hỗ trợ tạo Problem Details cho HTTP APIs sử dụng IProblemDetailsService.
Đoạn code sau cấu hình ứng dụng để tạo problem details response cho tất cả HTTP client và server error response chưa có body content:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseExceptionHandler();
app.UseStatusCodePages();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.MapControllers();
app.Run();Tùy chỉnh problem details với CustomizeProblemDetails
Đoạn code sau sử dụng ProblemDetailsOptions để đặt CustomizeProblemDetails:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddProblemDetails(options =>
options.CustomizeProblemDetails = (context) =>
{
var mathErrorFeature = context.HttpContext.Features
.Get<MathErrorFeature>();
if (mathErrorFeature is not null)
{
(string Detail, string Type) details = mathErrorFeature.MathError switch
{
MathErrorType.DivisionByZeroError =>
("Divison by zero is not defined.",
"https://wikipedia.org/wiki/Division_by_zero"),
_ => ("Negative or complex numbers are not valid input.",
"https://wikipedia.org/wiki/Square_root")
};
context.ProblemDetails.Type = details.Type;
context.ProblemDetails.Title = "Bad Input";
context.ProblemDetails.Detail = details.Detail;
}
}
);
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStatusCodePages();
app.UseAuthorization();
app.MapControllers();
app.Run();Triển khai ProblemDetailsFactory
MVC sử dụng Microsoft.AspNetCore.Mvc.Infrastructure.ProblemDetailsFactory để tạo ra tất cả các instance của ProblemDetails và ValidationProblemDetails. Factory này được dùng cho:
- Client error response
- Validation failure error response (phản hồi lỗi xác thực thất bại)
- ControllerBase.Problem và ControllerBase.ValidationProblem
Để tùy chỉnh problem details response, đăng ký triển khai custom của ProblemDetailsFactory trong Program.cs:
builder.Services.AddControllers(); builder.Services.AddTransient<ProblemDetailsFactory, SampleProblemDetailsFactory>();
Sử dụng ApiBehaviorOptions.ClientErrorMapping
Sử dụng thuộc tính ClientErrorMapping để cấu hình nội dung của ProblemDetails response. Ví dụ, đoạn code sau trong Program.cs cập nhật thuộc tính Link cho phản hồi 404:
builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.ClientErrorMapping[StatusCodes.Status404NotFound].Link =
"https://httpstatuses.com/404";
});Các tính năng xử lý lỗi bổ sung
Với Controller-based APIs
Phản hồi lỗi xác thực thất bại
Đối với web API controller, MVC phản hồi với loại response ValidationProblemDetails khi model validation (xác thực model) thất bại. MVC sử dụng kết quả của InvalidModelStateResponseFactory để tạo ra error response cho validation failure. Ví dụ sau thay thế factory mặc định bằng triển khai cũng hỗ trợ định dạng phản hồi dưới dạng XML, trong Program.cs:
builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
new BadRequestObjectResult(context.ModelState)
{
ContentTypes =
{
// using static System.Net.Mime.MediaTypeNames;
Application.Json,
Application.Xml
}
};
})
.AddXmlSerializerFormatters();Sử dụng exception để sửa đổi response
Nội dung của response có thể được sửa đổi từ bên ngoài controller bằng cách sử dụng custom exception và action filter:
- Tạo một well-known exception type có tên
HttpResponseException:
```csharp public class HttpResponseException : Exception { public HttpResponseException(int statusCode, object? value = null) => (StatusCode, Value) = (statusCode, value);
public int StatusCode { get; }
public object? Value { get; } } ```
- Tạo action filter có tên
HttpResponseExceptionFilter:
```csharp public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter { public int Order => int.MaxValue - 10;
public void OnActionExecuting(ActionExecutingContext context) { }
public void OnActionExecuted(ActionExecutedContext context) { if (context.Exception is HttpResponseException httpResponseException) { context.Result = new ObjectResult(httpResponseException.Value) { StatusCode = httpResponseException.StatusCode };
context.ExceptionHandled = true; } } } ```
Filter trên chỉ định Order là giá trị integer tối đa trừ 10. Order này cho phép các filter khác chạy ở cuối pipeline.
- Trong
Program.cs, thêm action filter vào filters collection:
``csharp builder.Services.AddControllers(options => { options.Filters.Add<HttpResponseExceptionFilter>(); }); ``