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>();