Error Middleware
El ExceptionMiddleware
es un middleware de ASP.NET Core diseñado para interceptar y manejar excepciones que ocurren durante el procesamiento de una solicitud HTTP en una API REST. Su objetivo principal es proporcionar respuestas de error consistentes y formateadas en formato JSON, mejorando así la experiencia del cliente y facilitando la depuración y el manejo de errores.
using System.Diagnostics;using System.Net;using CodeDesignPlus.Net.Core.Abstractions.Options;using CodeDesignPlus.Net.Exceptions;using CodeDesignPlus.Net.Exceptions.Models;using CodeDesignPlus.Net.Serializers;using FluentValidation;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.Extensions.Hosting;
namespace CodeDesignPlus.Net.Microservice.Commons.EntryPoints.Rest.Middlewares;
/// <summary>/// Middleware for handling exceptions in HTTP requests./// </summary>/// <remarks>/// This middleware catches exceptions thrown during the processing of HTTP requests and formats them into a standardized problem details response based on RFC 7807 and RFC 9457/// </remarks>/// <param name="next">The next middleware in the pipeline.</param>/// <param name="logger">Logger for logging exceptions.</param>/// <param name="env">The hosting environment to determine if the application is in production.</param>/// <param name="options">Options for configuring the middleware.</param>public class ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger, IHostEnvironment env, IOptions<CoreOptions> options){ private readonly Newtonsoft.Json.JsonSerializerSettings serializerSettings = new() { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(), Formatting = Newtonsoft.Json.Formatting.None };
/// <summary> /// Invokes the middleware to handle the HTTP context. /// </summary> /// <param name="context">The HTTP context.</param> /// <returns>A task representing the asynchronous operation.</returns> public async Task InvokeAsync(HttpContext context) { try { await next(context); } catch (ValidationException ex) { logger.LogWarning(ex, "Validation error occurred: {ValidationErrors}", string.Join(", ", ex.Errors.Select(e => $"{e.PropertyName}: {e.ErrorMessage}")));
await HandleExceptionsAsync(context, ex); } catch (CodeDesignPlusException ex) { logger.LogWarning(ex, "CodeDesignPlusException occurred. Layer: {Layer}, Code: {Code}, Message: {CustomMessage}", ex.Layer, ex.Code, ex.Message);
await HandleExceptionsAsync(context, ex); } catch (Exception ex) { logger.LogError(ex, "An unhandled exception has occurred.");
await HandleExceptionsAsync(context, ex); } }
/// <summary> /// Handles validation exceptions. /// </summary> /// <param name="context">The HTTP context.</param> /// <param name="exception">The validation exception.</param> /// <returns>A task representing the asynchronous operation.</returns> private Task HandleExceptionsAsync(HttpContext context, ValidationException exception) { context.Response.ContentType = "application/problem+json"; context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
var traceId = GetTraceId(context);
var problemDetails = new ProblemDetails { Type = $"{options.Value.ApiDocumentationBaseUrl}validation-error", Title = "Error de validación.", Status = context.Response.StatusCode, Detail = "Uno o más campos no pasaron la validación.", Instance = traceId };
var invalidParams = exception.Errors .Select(e => new InvalidParamDetail( ToCamelCase(e.PropertyName), e.ErrorMessage, e.ErrorCode )).ToList();
problemDetails.Extensions["invalid_params"] = invalidParams;
return context.Response.WriteAsync(JsonSerializer.Serialize(problemDetails, serializerSettings)); }
/// <summary> /// Handles CodeDesignPlus exceptions. /// </summary> /// <param name="context">The HTTP context.</param> /// <param name="exception">The CodeDesignPlus exception.</param> /// <returns>A task representing the asynchronous operation.</returns> private Task HandleExceptionsAsync(HttpContext context, CodeDesignPlusException exception) { context.Response.ContentType = "application/problem+json"; context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
var traceId = GetTraceId(context);
var messageTemplate = GetMessageTemplate(exception);
var detailMessage = GetDetailMessage(exception);
var layerName = exception.Layer.ToString().ToLowerInvariant();
var problemDetails = new ProblemDetails { Type = $"{options.Value.ApiDocumentationBaseUrl}{options.Value.AppName}/{layerName}-{exception.Code}", Title = messageTemplate, Status = context.Response.StatusCode, Detail = detailMessage, Instance = traceId };
problemDetails.Extensions["layer"] = exception.Layer.ToString(); problemDetails.Extensions["error_code"] = exception.Code;
return context.Response.WriteAsync(JsonSerializer.Serialize(problemDetails, serializerSettings)); }
/// <summary> /// Handles general exceptions. /// </summary> /// <param name="context">The HTTP context.</param> /// <param name="exception">The general exception.</param> /// <returns>A task representing the asynchronous operation.</returns> private Task HandleExceptionsAsync(HttpContext context, Exception exception) { context.Response.ContentType = "application/problem+json"; context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var traceId = GetTraceId(context);
var problemDetails = new ProblemDetails { Type = $"{options.Value.ApiDocumentationBaseUrl}internal-error", Title = "Internal Server Error", Status = context.Response.StatusCode, Detail = "An error occurred while processing your request, please try again later or contact support.", Instance = traceId };
if (!env.IsProduction()) problemDetails.Extensions["exception_message"] = exception.Message;
return context.Response.WriteAsync(JsonSerializer.Serialize(problemDetails, serializerSettings)); }
/// <summary> /// Converts a string to camel case. /// </summary> /// <param name="str">The string to convert.</param> /// <returns>A camel-cased string.</returns> private static string ToCamelCase(string str) { if (string.IsNullOrEmpty(str) || !char.IsUpper(str[0])) return str;
var chars = str.ToCharArray(); chars[0] = char.ToLowerInvariant(chars[0]); return new string(chars); }
/// <summary> /// Gets a detailed message for the exception based on its layer. /// This method provides a user-friendly message that indicates where the error occurred in the application architecture /// </summary> /// <param name="exception">The CodeDesignPlusException to get the detail message for.</param> /// <returns>A string containing the detail message.</returns> private static string GetDetailMessage(CodeDesignPlusException exception) { return exception.Layer switch { Layer.Domain => $"An error occurred in the domain layer - {exception.Code} ({exception.Message})", Layer.Infrastructure => $"An error occurred in the infrastructure layer - {exception.Code} ({exception.Message})", Layer.Application => $"An error occurred in the application layer - {exception.Code} ({exception.Message})", _ => $"An internal error occurred in the SDK CodeDesignPlus - {exception.Code} ({exception.Message})" }; }
/// <summary> /// Gets a message template based on the exception's layer. /// This method provides a standardized message template that can be used in the problem details response. /// </summary> /// <param name="exception">The CodeDesignPlusException to get the message template for.</param> /// <returns>A string containing the message template.</returns> private static string GetMessageTemplate(CodeDesignPlusException exception) { return exception.Layer switch { Layer.Domain => "Domain Error", Layer.Infrastructure => "Infrastructure Error", Layer.Application => "Application Error", _ => "Internal Error Sdk CodeDesignPlus" }; }
/// <summary> /// Gets the trace identifier for the current HTTP context. /// This identifier is used to track the request across different services and logs. /// </summary> /// <param name="context">HTTP context containing the trace identifier</param> /// <returns>The trace identifier</returns> private static string GetTraceId(HttpContext context) { return Activity.Current?.Id ?? context.TraceIdentifier; }
}
¿Cómo Funciona?
Section titled “¿Cómo Funciona?”El ExceptionMiddleware
se inserta en el pipeline de procesamiento de solicitudes HTTP de ASP.NET Core. Captura cualquier excepción que no sea manejada por los controladores de la API. Cuando una excepción ocurre, el middleware intercepta el flujo de ejecución, analiza el tipo de excepción y genera una respuesta HTTP con información detallada sobre el error. La respuesta se formatea en JSON, siguiendo una estructura común definida por la clase ErrorResponse
.
El middleware maneja los siguientes tipos de excepciones de forma específica:
ValidationException
(FluentValidation): Extrae los errores de validación del objeto de excepción y los incluye en la respuesta JSON, proporcionando detalles sobre cada campo que falló en la validación. La respuesta se envía con un código de estado HTTP 400 (Bad Request).CodeDesignPlusException
: Utiliza la información específica de este tipo de excepción (código, mensaje y capa de origen) para generar una respuesta JSON detallada, incluyendo el código de error personalizado. La respuesta se envía con un código de estado HTTP 400 (Bad Request).Exception
(genérica): Maneja cualquier otra excepción no capturada. Genera una respuesta JSON con un código de error genérico y un mensaje de error, indicando que ha ocurrido un error interno del servidor. La respuesta se envía con un código de estado HTTP 500 (Internal Server Error).
El middleware utiliza el JsonSerializer
de CodeDesignPlus.Net.Serializers
para serializar la respuesta en formato JSON. Además, el TraceIdentifier del contexto HTTP se incluye en la respuesta para facilitar el rastreo de errores.
Métodos
Section titled “Métodos”El ExceptionMiddleware
implementa los siguientes métodos y funciones principales:
InvokeAsync
Section titled “InvokeAsync”Type: public async Task InvokeAsync(HttpContext context)
Método principal del middleware que se ejecuta para cada solicitud HTTP. Envuelve el resto del pipeline de procesamiento de solicitudes (delegado next
) en un bloque try-catch. En caso de que se produzca alguna excepción, delega el manejo a los métodos correspondientes (HandleExceptionsAsync
).
HandleExceptionsAsync (CodeDesignPlusException)
Section titled “HandleExceptionsAsync (CodeDesignPlusException)”Type: private static Task HandleExceptionsAsync(HttpContext context, CodeDesignPlusException exception)
Maneja excepciones de tipo CodeDesignPlusException
. Establece el tipo de contenido de la respuesta como “application/json”, el código de estado HTTP como 400 (Bad Request) y construye un objeto ErrorResponse
a partir de la información de la excepción. Serializa la respuesta en formato JSON y la envía al cliente.
HandleExceptionsAsync (ValidationException)
Section titled “HandleExceptionsAsync (ValidationException)”Type: private static Task HandleExceptionsAsync(HttpContext context, ValidationException exception)
Maneja excepciones de tipo ValidationException
(de FluentValidation). Establece el tipo de contenido de la respuesta como “application/json”, el código de estado HTTP como 400 (Bad Request) y construye un objeto ErrorResponse
a partir de los errores de validación. Serializa la respuesta en formato JSON y la envía al cliente.
HandleExceptionsAsync (Exception)
Section titled “HandleExceptionsAsync (Exception)”Type: private static Task HandleExceptionsAsync(HttpContext context, Exception exception)
Maneja excepciones de tipo genérico (Exception
). Establece el tipo de contenido de la respuesta como “application/json”, el código de estado HTTP como 500 (Internal Server Error) y construye un objeto ErrorResponse
con un código de error genérico y un mensaje de error. Serializa la respuesta en formato JSON y la envía al cliente.
Ejemplo de Uso
Section titled “Ejemplo de Uso”Imagina que tienes un controlador de API que recibe un objeto de tipo CreateUserRequest
y que dicho objeto debe ser validado usando FluentValidation
.
// Ejemplo de un validadorusing FluentValidation;
public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>{ public CreateUserRequestValidator() { RuleFor(x => x.Name).NotEmpty().WithMessage("Name is required."); RuleFor(x => x.Email).EmailAddress().WithMessage("Email is not valid."); }}
// Ejemplo de un controladorusing Microsoft.AspNetCore.Mvc;using FluentValidation;using System.Net;
[ApiController][Route("[controller]")]public class UsersController : ControllerBase{ private readonly IValidator<CreateUserRequest> _validator; public UsersController(IValidator<CreateUserRequest> validator){ _validator = validator; }
[HttpPost] [ProducesResponseType((int)HttpStatusCode.OK)] [ProducesResponseType((int)HttpStatusCode.BadRequest)] public IActionResult CreateUser([FromBody]CreateUserRequest request) { // Validamos el request var validationResult = _validator.Validate(request);
if(!validationResult.IsValid){ throw new ValidationException(validationResult.Errors); }
// Lógica de negocio ... return Ok(); }}
Si el CreateUserRequest
es invalido, el ExceptionMiddleware
capturara la excepcion de tipo ValidationException
y retornara una respuesta con el estado BadRequest
junto con el detalle de los errores de validación en formato JSON.
Conclusiones
Section titled “Conclusiones”El ExceptionMiddleware
es un componente esencial para construir APIs REST robustas y fáciles de usar. Centraliza el manejo de excepciones, garantiza respuestas consistentes y detalladas para los clientes y simplifica la depuración de errores. Al interceptar y manejar excepciones de validación, excepciones personalizadas y excepciones generales, el middleware contribuye significativamente a la estabilidad y la calidad de la API.