ErrorInterceptor
El ErrorInterceptor
es un interceptor gRPC diseñado para manejar excepciones que puedan ocurrir durante la ejecución de llamadas gRPC. Su objetivo principal es convertir estas excepciones en respuestas gRPC consistentes, proporcionando información detallada sobre el error y su origen, facilitando así la depuración y el manejo de errores en aplicaciones que utilizan gRPC.
using CodeDesignPlus.Net.Exceptions;using CodeDesignPlus.Net.Exceptions.Models;using FluentValidation;using Grpc.Core;using Grpc.Core.Interceptors;
namespace CodeDesignPlus.Net.Microservice.Commons.EntryPoints.gRpc.Interceptors;
/// <summary>/// Interceptor for handling errors in gRPC calls./// </summary>public class ErrorInterceptor : Interceptor{ /// <summary> /// Handles unary server calls. /// </summary> /// <typeparam name="TRequest">Type of the request.</typeparam> /// <typeparam name="TResponse">Type of the response.</typeparam> /// <param name="request">Client request.</param> /// <param name="context">Server call context.</param> /// <param name="continuation">Continuation method for the call.</param> /// <returns>Server response.</returns> public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>( TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation) { try { return await continuation(request, context); } catch (Exception ex) { throw HandleException(ex); } }
/// <summary> /// Handles client streaming server calls. /// </summary> /// <typeparam name="TRequest">Type of the request.</typeparam> /// <typeparam name="TResponse">Type of the response.</typeparam> /// <param name="requestStream">Client request stream.</param> /// <param name="context">Server call context.</param> /// <param name="continuation">Continuation method for the call.</param> /// <returns>Server response.</returns> public override async Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>( IAsyncStreamReader<TRequest> requestStream, ServerCallContext context, ClientStreamingServerMethod<TRequest, TResponse> continuation) { try { return await continuation(requestStream, context); } catch (Exception ex) { throw HandleException(ex); } }
/// <summary> /// Handles server streaming server calls. /// </summary> /// <typeparam name="TRequest">Type of the request.</typeparam> /// <typeparam name="TResponse">Type of the response.</typeparam> /// <param name="request">Client request.</param> /// <param name="responseStream">Server response stream.</param> /// <param name="context">Server call context.</param> /// <param name="continuation">Continuation method for the call.</param> /// <returns>Completed task.</returns> public override async Task ServerStreamingServerHandler<TRequest, TResponse>( TRequest request, IServerStreamWriter<TResponse> responseStream, ServerCallContext context, ServerStreamingServerMethod<TRequest, TResponse> continuation) { try { await continuation(request, responseStream, context); } catch (Exception ex) { throw HandleException(ex); } }
/// <summary> /// Handles duplex streaming server calls. /// </summary> /// <typeparam name="TRequest">Type of the request.</typeparam> /// <typeparam name="TResponse">Type of the response.</typeparam> /// <param name="requestStream">Client request stream.</param> /// <param name="responseStream">Server response stream.</param> /// <param name="context">Server call context.</param> /// <param name="continuation">Continuation method for the call.</param> /// <returns>Completed task.</returns> public override async Task DuplexStreamingServerHandler<TRequest, TResponse>( IAsyncStreamReader<TRequest> requestStream, IServerStreamWriter<TResponse> responseStream, ServerCallContext context, DuplexStreamingServerMethod<TRequest, TResponse> continuation) { try { await continuation(requestStream, responseStream, context); } catch (Exception ex) { throw HandleException(ex); } }
/// <summary> /// Handles exceptions and converts them to RpcException. /// </summary> /// <param name="exception">Exception to handle.</param> /// <returns>Corresponding RpcException.</returns> private static RpcException HandleException(Exception exception) { return exception switch { ValidationException ex => HandleValidationException(ex), CodeDesignPlusException ex => HandleCodeDesignPlusException(ex), _ => HandleGeneralException(exception), }; }
/// <summary> /// Handles validation exceptions. /// </summary> /// <param name="exception">Validation exception.</param> /// <returns>Corresponding RpcException.</returns> private static RpcException HandleValidationException(ValidationException exception) { var errors = exception.Errors.Select(e => new ErrorDetail ( e.ErrorCode, e.PropertyName, e.ErrorMessage )).ToList();
var response = new ErrorResponse(null, Layer.Application);
response.Errors.AddRange(errors);
var metadata = new Metadata { { "Layer", Layer.Application.ToString() } };
foreach (var error in errors) { metadata.Add("ValidationError", $"Code: {error.Code}, Property: {error.Field}, Message: {error.Message}"); }
var status = new Status(StatusCode.InvalidArgument, "Validation failed");
return new RpcException(status, metadata, response.ToString()); }
/// <summary> /// Handles CodeDesignPlus exceptions. /// </summary> /// <param name="exception">CodeDesignPlus exception.</param> /// <returns>Corresponding RpcException.</returns> private static RpcException HandleCodeDesignPlusException(CodeDesignPlusException exception) { var response = new ErrorResponse(null, exception.Layer);
response.Errors.Add(new ErrorDetail(exception.Code, null, exception.Message));
var metadata = new Metadata { { "Layer", exception.Layer.ToString() }, { "Code", exception.Code }, { "Message", exception.Message } };
var status = new Status(StatusCode.FailedPrecondition, exception.Message);
return new RpcException(status, metadata, response.ToString()); }
/// <summary> /// Handles general exceptions. /// </summary> /// <param name="exception">General exception.</param> /// <returns>Corresponding RpcException.</returns> private static RpcException HandleGeneralException(Exception exception) { var response = new ErrorResponse(null, Layer.None);
response.Errors.Add(new ErrorDetail("0-000", null, exception.Message));
var metadata = new Metadata { { "Layer", Layer.None.ToString() }, { "Code", "0-000" }, { "Message", exception.Message } };
var status = new Status(StatusCode.Internal, "Internal server error");
return new RpcException(status, metadata, response.ToString()); }}
¿Cómo Funciona?
El ErrorInterceptor
intercepta todas las llamadas gRPC entrantes (unarias, de transmisión del cliente, de transmisión del servidor y dúplex). Captura cualquier excepción que se produzca durante la ejecución de la llamada y la convierte en un RpcException
. La conversión de la excepción se basa en el tipo de excepción:
ValidationException
(FluentValidation): Convierte las excepciones de validación enRpcException
con estadoInvalidArgument
, incluyendo detalles de cada error de validación.CodeDesignPlusException
: Convierte las excepciones personalizadas de la libreríaCodeDesignPlus
enRpcException
con estadoFailedPrecondition
, conservando el código y el mensaje del error.Exception
(genérica): Convierte cualquier otra excepción en unaRpcException
con estadoInternal
e información de error genérica.
El interceptor añade metadatos a la respuesta RpcException
para proporcionar información adicional sobre la capa en la que se produjo el error (aplicación, infraestructura, etc.) y el código/mensaje del error.
Métodos
El ErrorInterceptor
implementa varios métodos privados para manejar diferentes tipos de excepciones y construir respuestas RpcException
detalladas. A continuación se describen los métodos más importantes:
UnaryServerHandler
Type: public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
Intercepta llamadas gRPC unarias (request/response). Ejecuta el método de continuación (la llamada al servicio gRPC real) dentro de un bloque try-catch. Si se produce una excepción, la maneja con el método HandleException
y la propaga como un RpcException
.
ClientStreamingServerHandler
Type: public override async Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>(IAsyncStreamReader<TRequest> requestStream, ServerCallContext context, ClientStreamingServerMethod<TRequest, TResponse> continuation)
Intercepta llamadas gRPC de transmisión del cliente (múltiples requests/un response). Funciona de manera similar a UnaryServerHandler
, ejecutando el método de continuación y manejando cualquier excepción que se produzca.
ServerStreamingServerHandler
Type: public override async Task ServerStreamingServerHandler<TRequest, TResponse>(TRequest request, IServerStreamWriter<TResponse>, ServerCallContext context, ServerStreamingServerMethod<TRequest, TResponse> continuation)
Intercepta llamadas gRPC de transmisión del servidor (un request/múltiples responses). Ejecuta el método de continuación y maneja cualquier excepción que se produzca.
DuplexStreamingServerHandler
Type: public override async Task DuplexStreamingServerHandler<TRequest, TResponse>(IAsyncStreamReader<TRequest> requestStream, IServerStreamWriter<TResponse> responseStream,ServerCallContext context, DuplexStreamingServerMethod<TRequest, TResponse> continuation)
Intercepta llamadas gRPC de transmisión dúplex (múltiples requests/múltiples responses). Ejecuta el método de continuación y maneja cualquier excepción que se produzca.
HandleException
Type: private static RpcException HandleException(Exception exception)
Método privado que clasifica la excepción recibida y delega el manejo a los métodos HandleValidationException
, HandleCodeDesignPlusException
o HandleGeneralException
, según el tipo de la excepción. Devuelve una instancia de RpcException
con la información del error.
HandleValidationException
Type: private static RpcException HandleValidationException(ValidationException exception)
Método privado que maneja las excepciones de tipo ValidationException
(de FluentValidation). Extrae los errores de validación de la excepción y construye una RpcException
con estado InvalidArgument
y metadatos detallados de los errores de validación.
HandleCodeDesignPlusException
Type: private static RpcException HandleCodeDesignPlusException(CodeDesignPlusException exception)
Método privado que maneja las excepciones de tipo CodeDesignPlusException
. Construye una RpcException
con estado FailedPrecondition
, incluyendo metadatos de la capa de origen y el código del error.
HandleGeneralException
Type: private static RpcException HandleGeneralException(Exception exception)
Método privado que maneja excepciones de tipo genérico. Crea una RpcException
con estado Internal
e información general del error.
Ejemplo de Uso
Imagina que tienes un servicio gRPC 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 servicio gRPCusing Grpc.Core;using FluentValidation;
public class UserService : User.UserBase{ private readonly IValidator<CreateUserRequest> _validator;
public UserService(IValidator<CreateUserRequest> validator) { _validator = validator; }
public override Task<CreateUserResponse> CreateUser(CreateUserRequest request, ServerCallContext context) { // Validamos el request var validationResult = _validator.Validate(request);
if(!validationResult.IsValid){ throw new ValidationException(validationResult.Errors); }
// Lógica de negocio ... }}
Si el CreateUserRequest
es invalido, el ErrorInterceptor
capturara la excepcion de tipo ValidationException
y retornara una respuesta con el estado InvalidArgument
junto con metadatos describiendo el error.
Conclusiones
El ErrorInterceptor
es una herramienta útil para centralizar el manejo de excepciones en aplicaciones gRPC. Al proporcionar respuestas de error consistentes y detalladas, facilita la depuración y mejora la experiencia del desarrollador al trabajar con microservicios basados en gRPC. Su capacidad para manejar excepciones de validación, excepciones personalizadas y excepciones generales lo convierte en una pieza clave para construir aplicaciones gRPC robustas y confiables.