Skip to content

Add custom middleware for global exception handling #184

@nanotaboada

Description

@nanotaboada

Description

Implement a custom middleware to handle exceptions globally in the application. The middleware should catch unhandled exceptions, log the error details, and return a standardized error response. The response format should follow RFC 7807 (Problem Details for HTTP APIs), ensuring consistency and better error representation in API responses.

Acceptance Criteria

  • Create a middleware component to handle global exceptions.
  • Return standardized error responses in RFC 7807 format including type, title, status, detail, and instance (optional).
  • Ensure the middleware is registered in the application pipeline.
  • Add unit tests to validate the middleware behavior.

Implementation Plan

Scope Decisions

Following RFC 7807 (Problem Details for HTTP APIs) with these implementation choices:

  1. RFC 7807 Compliance: Use standard ProblemDetails model from Microsoft.AspNetCore.Mvc (built-in to .NET 8)

    • type: URI identifying error category
    • title: Short, human-readable summary
    • status: HTTP status code
    • detail: Human-readable error description
    • instance: Request path or trace ID for correlation
  2. Error Detail Level: Include stack traces in detail field only in Development environment (IHostEnvironment.IsDevelopment())

    • Development: Full exception details and stack trace for debugging
    • Production: Generic error message without sensitive information
  3. Validation Error Handling: Keep existing FluentValidation error responses in controllers separate

    • Middleware focuses on unhandled exceptions only
    • Structured validation errors continue using current pattern (maintains backward compatibility)

Implementation Steps

1. Create Middleware/ExceptionMiddleware.cs

  • Wrap next(context) in try-catch block
  • Map exception types to appropriate HTTP status codes:
    • ValidationException → 400 Bad Request
    • DbUpdateConcurrencyException → 409 Conflict
    • OperationCanceledException → 408 Request Timeout
    • ArgumentException/InvalidOperationException → 400 Bad Request
    • Generic Exception → 500 Internal Server Error
  • Return ProblemDetails with application/problem+json content type
  • Log exceptions using ILogger with structured logging (Serilog)
  • Include request ID/trace ID in instance field for correlation

2. Create Extensions/MiddlewareExtensions.cs

  • Add UseExceptionHandling(this WebApplication app) extension method
  • Follow existing project pattern from ServiceCollectionExtensions.cs

3. Update Program.cs

  • Register middleware using app.UseExceptionHandling()
  • Pipeline placement: After UseSerilogRequestLogging() but before other middleware
  • Current order will be: Serilog → Exception Handling → HTTPS Redirection → Health Checks → Rate Limiter → Controllers

4. Create test/.../Unit/ExceptionMiddlewareTests.cs

  • Test middleware catches unhandled exceptions
  • Verify response format matches RFC 7807 (all required fields present)
  • Validate HTTP status code mapping for different exception types
  • Confirm structured logging occurs with exception details
  • Verify Content-Type header is application/problem+json
  • Test Development vs Production detail level differences

Technical Details

No additional dependencies needed — All required components are built-in:

  • Microsoft.AspNetCore.Mvc.ProblemDetails (ASP.NET Core 8.0)
  • Microsoft.AspNetCore.Http for middleware
  • System.Text.Json for serialization
  • Serilog (already configured)

Benefits

  • ✅ Centralized exception handling
  • ✅ Standards-compliant error responses (RFC 7807)
  • ✅ Consistent API error format
  • ✅ Enhanced observability with structured logging
  • ✅ Request traceability via instance/trace IDs
  • ✅ Environment-aware detail level (security)
  • ✅ Zero breaking changes to existing validation handling

Suggested Approach

  • Create ExceptionMiddleware.cs
public class ExceptionMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try { await next(context); }
        catch (Exception ex)
        {
            context.Response.ContentType = "application/problem+json";
            var problem = new ProblemDetails
            {
                Title = "An error occurred",
                Detail = ex.Message,
                Status = (int)HttpStatusCode.InternalServerError
            };
            await context.Response.WriteAsync(JsonSerializer.Serialize(problem));
        }
    }
}
  • Register in Program.cs:
app.UseMiddleware<ExceptionMiddleware>();

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETPull requests that update .NET codeenhancementNew feature or requestgood first issueGood for newcomers

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions