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: