How to handle gRPC errors in .Net Core

From this article You will learn

  • What is Interceptor & how to use it for handling errors,
  • What is the correct way of transfer exceptions between client & server,
  • How You can change the status code of gRPC request for better application monitoring.

Handling server exceptions by gRPC interceptor

Interceptor in gRPC is a very similar conception like middleware idea from classic, Rest communication. A place which is part of specified gRPC global flow, where You can do some custom magic between endpoints. Can be implemented for server & clients, in this particular sample, I will focus on server interceptors.

Yeep, so probably as You expect, if this is a place where all our server gRPC requests meet & drink a couple of cups of coffee :-). Here we can implement our global error handling. To do this, first, we must create an implementation of abstract class from namespace Grpc.Core.Interceptors Interceptor.

The Interceptor class has a lot of methods for intercept gRPC requests. From the documentation, the best method for server-side incoming calls is the UnaryServerHandler. Soo we will override it…

    public class ExceptionInterceptor: Interceptor
    {
        private readonly ILogger<ExceptionInterceptor> _logger;

        public ExceptionInterceptor(ILogger<ExceptionInterceptor> logger)
        {
            _logger = logger;
        }
        
        public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
            TRequest request,
            ServerCallContext context,
            UnaryServerMethod<TRequest, TResponse> continuation)
        {
            try
            {
                return await continuation(request, context);
            }
            catch (Exception exception)
            {
                //... Handle of error logic 
            }
        }
    }

The next step is to register our useful interceptor to the IoC container (there are 2 samples, one for classic usage, second for CodeFirst which I described here)..

        public void ConfigureServices(IServiceCollection services)
        {
            // Classic sample..
            services.AddGrpc(
                options =>
                {
                    options.Interceptors.Add<ExceptionInterceptor>();
                });
            
            // CodeFirst sample..
            services.AddCodeFirstGrpc(
                options =>
                {
                    options.Interceptors.Add<ExceptionInterceptor>();
                });
        }

Handling exceptions by gRPC client

Ok, but what happens when our server will throw a custom exception for the gRPC client side? Unfortunately, it doesn’t receive a custom exception, so the block below block doesn’t work as we expect..

            try
            {
                await grpc.SendWeatherForecastGeneratedEvent(new SendWeatherForecastEventCommand());
            }
            catch(CustomWeatherException exception)
            {
                
            }

So what we received? What is the correct way to handle custom exceptions from the client-side? The correct answer to these questions is RpcException. From the customer perspective, You will always receive RpcException. From the server-side, You can specify the content of this exception by statuses object. Statuses contains information about call code (gRPC call code) & details. Classic HTTP status code which is known from REST here is not useful, but about it later. So back to the interceptor.

    public class ExceptionInterceptor:  Interceptor
    {
        private readonly ILogger<ExceptionInterceptor> _logger;

        public ExceptionInterceptor(ILogger<ExceptionInterceptor> logger)
        {
            _logger = logger;
        }
        
        public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
            TRequest request,
            ServerCallContext context,
            UnaryServerMethod<TRequest, TResponse> continuation)
        {
            try
            {
                return await continuation(request, context);
            }
            catch (CustomWeatherException weatherException)
            {
                throw new RpcException(new Status(StatusCode.InvalidArgument, "weather is terrible"));
            }
            catch (Exception exception)
            {
                throw new RpcException(new Status(StatusCode.Internal, exception.ToString()));
            }
        }
    }

Yeaa! From this moment our client will exactly know, what happens in our server, but…

gRPC request status code managment

Yes, there is always a but ;-). If You have some communication monitoring, metrics all requests with an exception will be monitored like… requests with status code 200. Terrible, but true. But, yes there is always a but :-), there is some to trick to handle it.

    public class ExceptionInterceptor:  Interceptor
    {
        private readonly ILogger<ExceptionInterceptor> _logger;

        public ExceptionInterceptor(ILogger<ExceptionInterceptor> logger)
        {
            _logger = logger;
        }
        
        public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
            TRequest request,
            ServerCallContext context,
            UnaryServerMethod<TRequest, TResponse> continuation)
        {
            try
            {
                return await continuation(request, context);
            }
            catch (CustomWeatherException weatherException)
            {
                var httpContext = context.GetHttpContext();
                httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
                
                throw new RpcException(new Status(StatusCode.InvalidArgument, "weather is terrible"));
            }
            catch (Exception exception)
            {
                var httpContext = context.GetHttpContext();
                httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
                
                throw new RpcException(new Status(StatusCode.Internal, exception.ToString()));
            }
        }
    }

From the ServerCallContext argument, we can get information about httpContext & we can override 200 status code. Btw, this trick can be also used by the presentation layer.

        public async ValueTask<ICollection<WeatherForecast>> Get(GetWeathersQuery query, CallContext context = default)
        {
            var items = await _forecastService.GetWeatherForNextDays(query.NumberOfDays);
            
            var httpContext = context.ServerCallContext?.GetHttpContext();
            if (httpContext != null && items == null)
            {
                httpContext.Response.StatusCode = StatusCodes.Status404NotFound;
            }

            return items.ToArray();
        }

Why we need it? For better metrics visualizations. For example Azure AppInsights analysis request condition by returned status code. When a failed request will return 200, monitoring will receive it like healthy, so we also, but our customers can have a little different opinion on that :-)!

Here You can find some code sample:

https://github.com/Rogaliusz/Grpc.Performance/blob/main/src/Grpc.Performance.Grpc.CodeFirst/Interceptors/ExceptionInterceptor.cs