Skip to content

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 en RpcException con estado InvalidArgument, incluyendo detalles de cada error de validación.
  • CodeDesignPlusException: Convierte las excepciones personalizadas de la librería CodeDesignPlus en RpcException con estado FailedPrecondition, conservando el código y el mensaje del error.
  • Exception (genérica): Convierte cualquier otra excepción en una RpcException con estado Internal 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 validador
using 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 gRPC
using 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.

Referencias Adicionales