6

I want a Global Exception Handler for my gRPC service. Normally I configure Global Exception Handling as below. But If any exception is thrown in my service method, that is not being handled this way. Any way to accomplish that?

static void Main(string[] args)
        {
            AppDomain.CurrentDomain.UnhandledException += GlobalExceptionHandler;
            throw new Exception();
            // Shutdown.WaitOne();
        }

        static void GlobalExceptionHandler(object sender, UnhandledExceptionEventArgs e) {
            throw new RpcException(new Status(StatusCode.Internal, e.ExceptionObject.ToString()));
        }
kumarmo2
  • 1,381
  • 1
  • 20
  • 35
  • The global "Unhandled Exception" handler is for exceptions that haven't been caught by application code at all. To maintain sanity of the execution environment, gRPC framework does catch exceptions thrown by method handler (implemented by user), terminates the in-progress call (if RpcException is thrown by the user code, the StatusCode from the exception is propagated to the client) and logs that the handler has thrown an exception (normally it should not happen). I'd say this is the behavior that most users would expect, so there are no plans to change this. – Jan Tattermusch Sep 17 '18 at 20:55

1 Answers1

5

You need to use interceptors to do it:

Caught the exceptions on the server to send to the client as Json:

public class GrpcServerInterceptor : Interceptor
{
    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
    {
        try
        {
            return await base.UnaryServerHandler(request, context, continuation);
        }
        catch (Exception exp)
        {
            throw this.TreatException(exp);
        }
    }
    
    public override async Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>(IAsyncStreamReader<TRequest> requestStream, ServerCallContext context, ClientStreamingServerMethod<TRequest, TResponse> continuation)
    {
        try
        {
            return await base.ClientStreamingServerHandler(requestStream, context, continuation);
        }
        catch (Exception exp)
        {
            throw this.TreatException(exp);
        }
    }
    
    public override async Task ServerStreamingServerHandler<TRequest, TResponse>(TRequest request, IServerStreamWriter<TResponse> responseStream, ServerCallContext context, ServerStreamingServerMethod<TRequest, TResponse> continuation)
    {
        try
        {
            await base.ServerStreamingServerHandler(request, responseStream, context, continuation);
        }
        catch (Exception exp)
        {
            throw this.TreatException(exp);
        }
    }
    
    public override async Task DuplexStreamingServerHandler<TRequest, TResponse>(IAsyncStreamReader<TRequest> requestStream, IServerStreamWriter<TResponse> responseStream, ServerCallContext context, DuplexStreamingServerMethod<TRequest, TResponse> continuation)
    {
        try
        {
            await base.DuplexStreamingServerHandler(requestStream, responseStream, context, continuation);
        }
        catch (Exception exp)
        {
            throw this.TreatException(exp);
        }
    }

    private RpcException TreatException(Exception exp)
    {
        // Convert exp to Json
        string exception = JsonConvert.SerializeObject(exp, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto });

        // Convert Json to byte[]
        byte[] exceptionByteArray = Encoding.UTF8.GetBytes(exception);

        // Add Trailer with the exception as byte[]
        Metadata metadata = new Metadata { { "exception-bin", exceptionByteArray } 
    };

        // New RpcException with original exception
        return new RpcException(new Status(StatusCode.Internal, "Error"), metadata);
    }
}

Use the server incerceptor:

// Startup -> ConfigureServices
services.AddGrpc(
    config =>
    {
        config.Interceptors.Add<GrpcServerInterceptor>();
    });

Now on the client you need to define an interceptor too:

private class GrpcClientInterceptor : Interceptor
{
    public override TResponse BlockingUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, BlockingUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        try
        {
            return base.BlockingUnaryCall(request, context, continuation);
        }
        catch (RpcException exp)
        {
            TreatException(exp);
            throw;
        }
    }

    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        AsyncUnaryCall<TResponse> chamada = continuation(request, context);
        return new AsyncUnaryCall<TResponse>(
            this.TreatResponseUnique(chamada.ResponseAsync),
            chamada.ResponseHeadersAsync,
            chamada.GetStatus,
            chamada.GetTrailers,
            chamada.Dispose);
    }

    public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(ClientInterceptorContext<TRequest, TResponse> context, AsyncClientStreamingCallContinuation<TRequest, TResponse> continuation)
    {
        AsyncClientStreamingCall<TRequest, TResponse> chamada = continuation(context);
        return new AsyncClientStreamingCall<TRequest, TResponse>(
            chamada.RequestStream,
            this.TreatResponseUnique(chamada.ResponseAsync),
            chamada.ResponseHeadersAsync,
            chamada.GetStatus,
            chamada.GetTrailers,
            chamada.Dispose);
    }

    public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(TRequest request, ClientInterceptorContext<TRequest, TResponse> context, AsyncServerStreamingCallContinuation<TRequest, TResponse> continuation)
    {
        AsyncServerStreamingCall<TResponse> chamada = continuation(request, context);
        return new AsyncServerStreamingCall<TResponse>(
            new TreatResponseStream<TResponse>(chamada.ResponseStream),
            chamada.ResponseHeadersAsync,
            chamada.GetStatus,
            chamada.GetTrailers,
            chamada.Dispose);
    }

    public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(ClientInterceptorContext<TRequest, TResponse> context, AsyncDuplexStreamingCallContinuation<TRequest, TResponse> continuation)
    {
        AsyncDuplexStreamingCall<TRequest, TResponse> chamada = continuation(context);
        return new AsyncDuplexStreamingCall<TRequest, TResponse>(
            chamada.RequestStream,
            new TreatResponseStream<TResponse>(chamada.ResponseStream),
            chamada.ResponseHeadersAsync,
            chamada.GetStatus,
            chamada.GetTrailers,
            chamada.Dispose);
    }

    internal static void TreatException(RpcException exp)
    {
        // Check if there's a trailer that we defined in the server
        if (!exp.Trailers.Any(x => x.Key.Equals("exception-bin")))
        {
            return;
        }

        // Convert exception from byte[] to  string
        string exceptionString = Encoding.UTF8.GetString(exp.Trailers.GetValueBytes("exception-bin"));

        // Convert string to exception
        Exception exception = JsonConvert.DeserializeObject<Exception>(exceptionString, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto });

        // Required to keep the original stacktrace (https://stackoverflow.com/questions/66707139/how-to-throw-a-deserialized-exception)
        exception.GetType().GetField("_remoteStackTraceString", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(exception, exception.StackTrace);

        // Throw the original exception
        ExceptionDispatchInfo.Capture(exception).Throw();
    }

    private async Task<TResponse> TreatResponseUnique<TResponse>(Task<TResponse> resposta)
    {
        try
        {
            return await resposta;
        }
        catch (RpcException exp)
        {
            TreatException(exp);
            throw;
        }
    }
}

private class TreatResponseStream<TResponse> : IAsyncStreamReader<TResponse>
{
    private readonly IAsyncStreamReader<TResponse> stream;

    public TreatResponseStream(IAsyncStreamReader<TResponse> stream)
    {
        this.stream = stream;
    }

    public TResponse Current => this.stream.Current;

    public async Task<bool> MoveNext(CancellationToken cancellationToken)
    {
        try
        {
            return await this.stream.MoveNext(cancellationToken).ConfigureAwait(false);
        }
        catch (RpcException exp)
        {
            GrpcClientInterceptor.TreatException(exp);
            throw;
        }
    }
}

Now use the client interceptor:

this.MyGrpcChannel.Intercept(new GrpcClientInterceptor()).CreateGrpcService<IService>();
Guilherme Molin
  • 308
  • 4
  • 13