using System.Net; using System.Text.Json; using MyNewProjectName.Contracts.DTOs.Responses; using MyNewProjectName.Domain.Exceptions; using Serilog; namespace MyNewProjectName.WebAPI.Middleware; /// /// Global exception handling middleware /// Catches all unhandled exceptions, logs them to Serilog with full details, /// and returns a standardized error response without exposing stack traces /// public class ExceptionHandlingMiddleware { private readonly RequestDelegate _next; private readonly Serilog.ILogger _logger; public ExceptionHandlingMiddleware(RequestDelegate next, Serilog.ILogger logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { try { await _next(context); } catch (Exception ex) { await HandleExceptionAsync(context, ex); } } private async Task HandleExceptionAsync(HttpContext context, Exception exception) { // Get correlation ID from header (set by CorrelationIdMiddleware) var correlationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault() ?? "unknown"; // Log full exception details to Serilog (including stack trace) // This will be searchable by CorrelationId _logger.Error( exception, "Unhandled exception occurred. CorrelationId: {CorrelationId}, Path: {Path}, Method: {Method}", correlationId, context.Request.Path, context.Request.Method ); // Safety check: If response has already started, we cannot modify headers/status code // This prevents InvalidOperationException: "Headers are read-only, response has already started" if (context.Response.HasStarted) { _logger.Warning( "Response has already started. Cannot modify HTTP status code or headers. CorrelationId: {CorrelationId}", correlationId ); return; } var response = context.Response; // Clear any existing response data response.Clear(); response.ContentType = "application/json"; // Determine status code and user-friendly message var (statusCode, message, errors) = exception switch { ValidationException validationEx => ( HttpStatusCode.BadRequest, "Validation failed", validationEx.Errors.SelectMany(e => e.Value).ToList()), NotFoundException => ( HttpStatusCode.NotFound, exception.Message, new List()), DomainException => ( HttpStatusCode.BadRequest, exception.Message, new List()), UnauthorizedAccessException => ( HttpStatusCode.Unauthorized, "Unauthorized", new List()), _ => ( HttpStatusCode.InternalServerError, "An error occurred while processing your request", new List()) }; response.StatusCode = (int)statusCode; // Return standardized error response (NO stack trace exposed to client) var result = JsonSerializer.Serialize(new ApiResponse { Success = false, Message = message, Errors = errors.Any() ? errors : null }, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); await response.WriteAsync(result); } }