Manejo Global de Excepciones con Middleware Personalizado en .NET Core

Iván PeinadoIván Peinado
5 min read

Cuando desarrollamos aplicaciones web, uno de los aspectos más críticos es el manejo adecuado de errores. En lugar de tener bloques try-catch dispersos por toda nuestra aplicación, .NET Core nos ofrece una solución elegante: el middleware personalizado para el manejo global de excepciones.

🎯 ¿Por qué necesitamos un middleware de excepciones?

En mis años trabajando con .NET Core, he visto aplicaciones donde el manejo de errores es inconsistente. Algunos controladores devuelven mensajes de error diferentes, otros exponen información sensible en producción, y algunos ni siquiera logean los errores correctamente.

El middleware de excepciones nos permite:

  • Centralizar la lógica de manejo de errores
  • Mantener una respuesta consistente ante fallos
  • Proteger información sensible en producción
  • Facilitar el debugging y monitoreo

🔧 Implementando nuestro middleware personalizado

Vamos a crear un middleware que capture todas las excepciones no manejadas y las procese de manera uniforme:

public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;
    private readonly IWebHostEnvironment _environment;

    public GlobalExceptionMiddleware(
        RequestDelegate next,
        ILogger<GlobalExceptionMiddleware> logger,
        IWebHostEnvironment environment)
    {
        _next = next;
        _logger = logger;
        _environment = environment;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Ocurrió una excepción no manejada: {Message}", ex.Message);
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";

        var response = new ErrorResponse();

        switch (exception)
        {
            case ValidationException validationEx:
                response.StatusCode = StatusCodes.Status400BadRequest;
                response.Message = "Error de validación";
                response.Details = validationEx.Message;
                break;

            case NotFoundException notFoundEx:
                response.StatusCode = StatusCodes.Status404NotFound;
                response.Message = "Recurso no encontrado";
                response.Details = notFoundEx.Message;
                break;

            case UnauthorizedException unauthorizedEx:
                response.StatusCode = StatusCodes.Status401Unauthorized;
                response.Message = "No autorizado";
                response.Details = unauthorizedEx.Message;
                break;

            default:
                response.StatusCode = StatusCodes.Status500InternalServerError;
                response.Message = "Error interno del servidor";
                response.Details = _environment.IsDevelopment() 
                    ? exception.Message 
                    : "Ocurrió un error inesperado";
                break;
        }

        context.Response.StatusCode = response.StatusCode;

        var jsonResponse = JsonSerializer.Serialize(response, new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        });

        await context.Response.WriteAsync(jsonResponse);
    }
}

📋 Definiendo nuestras excepciones personalizadas

Para que nuestro middleware sea más efectivo, creemos algunas excepciones personalizadas:

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

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

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

📄 Modelo de respuesta de error

Necesitamos un modelo consistente para nuestras respuestas de error:

public class ErrorResponse
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
    public string Details { get; set; }
    public DateTime Timestamp { get; set; } = DateTime.UtcNow;
    public string TraceId { get; set; }
}

⚙️ Registrando el middleware

En nuestro archivo Program.cs, registramos el middleware:

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Configuración de servicios
        builder.Services.AddControllers();
        builder.Services.AddEndpointsApiExplorer();
        builder.Services.AddSwaggerGen();

        var app = builder.Build();

        // Importante: El middleware debe ir antes de otros middlewares
        app.UseMiddleware<GlobalExceptionMiddleware>();

        if (app.Environment.IsDevelopment())
        {
            app.UseSwagger();
            app.UseSwaggerUI();
        }

        app.UseHttpsRedirection();
        app.UseAuthorization();
        app.MapControllers();

        app.Run();
    }
}

🎨 Mejoras adicionales

Logging estructurado con Serilog

Para un mejor logging, podemos integrar Serilog:

private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
    var traceId = Activity.Current?.TraceId.ToString() ?? context.TraceIdentifier;

    _logger.LogError(exception, 
        "Error procesando request {Method} {Path}. TraceId: {TraceId}", 
        context.Request.Method, 
        context.Request.Path, 
        traceId);

    // ... resto del código

    response.TraceId = traceId;
}

Configuración por entorno

Podemos hacer nuestro middleware más flexible con configuración:

public class ExceptionMiddlewareOptions
{
    public bool IncludeStackTrace { get; set; } = false;
    public bool IncludeExceptionDetails { get; set; } = false;
    public Dictionary<string, string> CustomErrorMessages { get; set; } = new();
}

🧪 Probando nuestro middleware

Creemos un controlador simple para probar:

[ApiController]
[Route("api/[controller]")]
public class TestController : ControllerBase
{
    [HttpGet("validation-error")]
    public IActionResult ValidationError()
    {
        throw new ValidationException("El campo 'nombre' es requerido");
    }

    [HttpGet("not-found")]
    public IActionResult NotFound()
    {
        throw new NotFoundException("El usuario solicitado no existe");
    }

    [HttpGet("server-error")]
    public IActionResult ServerError()
    {
        throw new Exception("Error interno simulado");
    }
}

📊 Monitoreo y métricas

Para un mejor monitoreo, podemos agregar métricas:

private readonly IMetrics _metrics;

// En el constructor
public GlobalExceptionMiddleware(
    RequestDelegate next,
    ILogger<GlobalExceptionMiddleware> logger,
    IWebHostEnvironment environment,
    IMetrics metrics)
{
    _next = next;
    _logger = logger;
    _environment = environment;
    _metrics = metrics;
}

// En HandleExceptionAsync
_metrics.CreateCounter("exceptions_total")
    .WithTag("exception_type", exception.GetType().Name)
    .WithTag("status_code", response.StatusCode.ToString())
    .Increment();

🔍 Mejores prácticas

Basándome en mi experiencia, aquí tienes algunas recomendaciones:

1. Orden del middleware

El middleware de excepciones debe ser uno de los primeros en la pipeline para capturar errores de otros middlewares.

2. Logging apropiado

Siempre logea las excepciones con suficiente contexto pero sin exponer información sensible.

3. Respuestas consistentes

Mantén un formato de respuesta consistente en toda tu API.

4. Seguridad

Nunca expongas stack traces o detalles internos en producción.

📝 Consideraciones adicionales

Integración con Application Insights

Si usas Azure, puedes integrar fácilmente con Application Insights:

private readonly TelemetryClient _telemetryClient;

// En HandleExceptionAsync
_telemetryClient.TrackException(exception, new Dictionary<string, string>
{
    ["RequestPath"] = context.Request.Path,
    ["RequestMethod"] = context.Request.Method,
    ["StatusCode"] = response.StatusCode.ToString()
});

Filtros de excepción vs Middleware

Aunque también puedes usar filtros de excepción, el middleware ofrece más control y puede capturar errores que ocurran antes de llegar a los controladores.

🎉 Conclusión

El middleware personalizado para manejo de excepciones es una herramienta poderosa que nos ayuda a mantener nuestras aplicaciones más robustas y fáciles de mantener. Con una implementación bien pensada, podemos asegurar que nuestros usuarios reciban respuestas consistentes y que nosotros tengamos la información necesaria para resolver problemas rápidamente.

Recuerda siempre probar tu middleware en diferentes escenarios y mantener la documentación actualizada para tu equipo. ¡Los errores son inevitables, pero cómo los manejamos marca la diferencia!

0
Subscribe to my newsletter

Read articles from Iván Peinado directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Iván Peinado
Iván Peinado

20 años de experiencia como desarrollador en tecnologías .NET. Desde que comencé mi aventura profesional no he dejado de interesarme por todo lo que rodea a esta tecnología. Me considero un apasionado de mi trabajo, intentando siempre aprender, evolucionar y conseguir unas metas y objetivos. La tecnología cambia constantemente y por ello es necesario tener una base consolidada y seguir adquiriendo nuevos y mayores conocimientos que hagan de nuestro trabajo más fácil. Intento siempre, aprender nuevas herramientas y funcionalidades relacionadas con la tecnología .NET que me ayude a seguir avanzando en mi carrera profesional y aportando nuevas ideas en los proyectos en los que participo.