using System.Net;
using System.Text.Json;
using MyNewProjectName.Contracts.DTOs.Responses;
using MyNewProjectName.Domain.Exceptions;
using Serilog;
namespace MyNewProjectName.AdminAPI.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);
}
}