Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Xử lý lỗi (Error Handling)

Global Error Handling

UseExceptionHandler

// Program.cs
var app = builder.Build();

// Development - hiển thị chi tiết lỗi
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    // Production - redirect đến error page
    app.UseExceptionHandler("/Error");
    
    // Hoặc custom error handler
    app.UseExceptionHandler(errorApp =>
    {
        errorApp.Run(async context =>
        {
            context.Response.StatusCode = 500;
            context.Response.ContentType = "application/json";
            
            var exception = context.Features.Get<IExceptionHandlerFeature>();
            var error = new
            {
                statusCode = 500,
                message = "An unexpected error occurred",
                traceId = context.TraceIdentifier
            };
            
            await context.Response.WriteAsJsonAsync(error);
        });
    });
}

UseStatusCodePages

// Xử lý các status code 4xx, 5xx
app.UseStatusCodePages();

// Hoặc redirect
app.UseStatusCodePagesWithRedirects("/Error/{0}");

// Hoặc re-execute
app.UseStatusCodePagesWithReExecute("/Error", "?statusCode={0}");

ProblemDetails (RFC 7807)

Cấu trúc ProblemDetails

┌─────────────────────────────────────────────────────────────────┐
│                    PROBLEMDETAILS STRUCTURE                      │
├─────────────────────────────────────────────────────────────────┤
│ {                                                               │
│   "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", │
│   "title": "Bad Request",                                       │
│   "status": 400,                                                │
│   "detail": "The input was invalid",                           │
│   "instance": "/api/products",                                  │
│   "traceId": "00-abc123-xyz789-00",                            │
│   "errors": {                                                   │
│     "Name": ["The Name field is required"],                     │
│     "Price": ["Price must be greater than 0"]                   │
│   }                                                             │
│ }                                                               │
└─────────────────────────────────────────────────────────────────┘

Cấu hình ProblemDetails

// Program.cs
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Extensions["traceId"] = 
            context.HttpContext.TraceIdentifier;
        context.ProblemDetails.Extensions["requestId"] = 
            context.HttpContext.Request.Headers["X-Request-Id"].FirstOrDefault();
    };
});

// Sử dụng trong controller
[HttpPost]
public IActionResult CreateProduct(Product product)
{
    if (product.Price <= 0)
    {
        return Problem(
            title: "Invalid Price",
            detail: "Price must be greater than 0",
            statusCode: 400,
            type: "https://tools.ietf.org/html/rfc7231#section-6.5.1",
            instance: "/api/products");
    }
    
    return Ok(product);
}

Custom Exception Filter

Global Exception Handler

public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        _logger.LogError(exception, "Unhandled exception occurred");

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "Server Error",
            Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1",
            Instance = httpContext.Request.Path
        };

        httpContext.Response.StatusCode = problemDetails.Status.Value;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        return true; // Exception handled
    }
}

// Đăng ký
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();

Exception Type Handler

public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }
}

public class ValidationException : Exception
{
    public IDictionary<string, string[]> Errors { get; }

    public ValidationException(IDictionary<string, string[]> errors)
        : base("Validation failed")
    {
        Errors = errors;
    }
}

// Handler cho từng loại exception
public class NotFoundExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        if (exception is not NotFoundException notFoundEx)
        {
            return false; // Không handle, để handler khác xử lý
        }

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status404NotFound,
            Title = "Not Found",
            Detail = notFoundEx.Message,
            Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4"
        };

        httpContext.Response.StatusCode = 404;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
        return true;
    }
}

// Đăng ký theo thứ tự ưu tiên
builder.Services.AddExceptionHandler<NotFoundExceptionHandler>();
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

Error Handling trong Minimal APIs

var app = builder.Build();

// Global error handler
app.UseExceptionHandler(exceptionHandlerApp =>
{
    exceptionHandlerApp.Run(async context =>
    {
        var exception = context.Features.Get<IExceptionHandlerFeature>();
        var problemDetails = new ProblemDetails
        {
            Status = 500,
            Title = "Internal Server Error",
            Instance = context.Request.Path
        };

        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(problemDetails);
    });
});

// Typed Results với error handling
app.MapPost("/api/products", async (Product product, AppDbContext db) =>
{
    try
    {
        db.Products.Add(product);
        await db.SaveChangesAsync();
        return Results.Created($"/api/products/{product.Id}", product);
    }
    catch (DbUpdateException ex)
    {
        return Results.Problem(
            title: "Database Error",
            detail: ex.Message,
            statusCode: 500);
    }
});

Best Practices

1. Không expose sensitive information

// ❌ Bad - expose internal details
return Problem(
    detail: $"SQL Error: {ex.Message} Connection: {connectionString}",
    statusCode: 500);

// ✅ Good - generic message
return Problem(
    detail: "An unexpected error occurred. Please try again later.",
    statusCode: 500);

2. Log errors properly

// ✅ Log với context
_logger.LogError(ex, 
    "Failed to process order {OrderId} for user {UserId}", 
    orderId, userId);

// ✅ Include request details
_logger.LogWarning(
    "Rate limit exceeded for IP {IP} on endpoint {Endpoint}",
    context.Connection.RemoteIpAddress,
    context.Request.Path);

3. Use appropriate status codes

Status CodeWhen to Use
400 Bad RequestInvalid input, validation failed
401 UnauthorizedMissing or invalid authentication
403 ForbiddenAuthenticated but no permission
404 Not FoundResource doesn’t exist
409 ConflictBusiness rule violation, duplicate
422 UnprocessableValid syntax but semantic errors
429 Too Many RequestsRate limit exceeded
500 Internal Server ErrorUnexpected server error
503 Service UnavailableMaintenance or overload