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: