How to add global exception interceptor in gRPC server?

18,754

Solution 1

Below code will catch all runtime exceptions, Also refer the link https://github.com/grpc/grpc-java/issues/1552

public class GlobalGrpcExceptionHandler implements ServerInterceptor {

   @Override
   public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call,
         Metadata requestHeaders, ServerCallHandler<ReqT, RespT> next) {
      ServerCall.Listener<ReqT> delegate = next.startCall(call, requestHeaders);
      return new SimpleForwardingServerCallListener<ReqT>(delegate) {
         @Override
         public void onHalfClose() {
            try {
               super.onHalfClose();
            } catch (Exception e) {
               call.close(Status.INTERNAL
                .withCause (e)
                .withDescription("error message"), new Metadata());
            }
         }
      };
   }
}

Solution 2

TransmitStatusRuntimeExceptionInterceptor is very similar to what you want, except that it only catches StatusRuntimeException. You can fork it and make it catch all exceptions.

To install an interceptor for all services on a server, you can use ServerBuilder.intercept(), which was added in gRPC 1.5.0

Solution 3

In Kotlin, you have to structure your ServerInterceptor differently. I was using grpc-kotlin in Micronaut and the exceptions never appeared in the SimpleForwardingServerCallListener onHalfClose or other handlers.

Solution 4

If you want to catch exceptions in all gRPC endpoints (including the ones processing client streams) and interceptors, you probably want something similar to the following:

import io.grpc.ForwardingServerCallListener;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.Status;

public class GlobalGrpcExceptionHandler implements ServerInterceptor {

    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall,
                                                                 Metadata requestHeaders,
                                                                 ServerCallHandler<ReqT, RespT> serverCallHandler) {
        try {
            ServerCall.Listener<ReqT> delegate = serverCallHandler.startCall(serverCall, requestHeaders);
            return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(delegate) {
                @Override
                public void onMessage(ReqT message) {
                    try {
                        super.onMessage(message); // Here onNext is called (in case of client streams)
                    } catch (Throwable e) {
                        handleEndpointException(e, serverCall);
                    }
                }

                @Override
                public void onHalfClose() {
                    try {
                        super.onHalfClose(); // Here onCompleted is called (in case of client streams)
                    } catch (Throwable e) {
                        handleEndpointException(e, serverCall);
                    }
                }
            };
        } catch (Throwable t) {
            return handleInterceptorException(t, serverCall);
        }
    }

    private <ReqT, RespT> void handleEndpointException(Throwable t, ServerCall<ReqT, RespT> serverCall) {
        serverCall.close(Status.INTERNAL
                         .withCause(t)
                         .withDescription("An exception occurred in the endpoint implementation"), new Metadata());
    }

    private <ReqT, RespT> ServerCall.Listener<ReqT> handleInterceptorException(Throwable t, ServerCall<ReqT, RespT> serverCall) {
        serverCall.close(Status.INTERNAL
                         .withCause(t)
                         .withDescription("An exception occurred in a **subsequent** interceptor"), new Metadata());

        return new ServerCall.Listener<ReqT>() {
            // no-op
        };
    }
}

DISCLAIMER: I have gathered this by inspecting the implementation, I have not read it in the documentation and I'm not sure if it will change. For reference, I'm referring to io.grpc version 1.30.

Solution 5

Have you read the grpc java examples for interceptor?

So in my case, we use code and message as standard for defining what kind of error the server sent onto client.

Examples: server send response like

{
  code: 409,
  message: 'Id xxx aldready exist'
}

So in the client you can setup client interceptor for getting that code and response with Reflection. Fyi we use Lognet Spring Boot starter for grpc as the server, and Spring boot for the client.

Share:
18,754
smallufo
Author by

smallufo

A Java/Kotlin programmer twitter : http://twitter.com/smallufo

Updated on June 02, 2022

Comments

  • smallufo
    smallufo almost 2 years

    In gRPC , how to add a global exception interceptor that intercepts any RuntimeException and propagate meaningful information to the client ?

    for example , a divide method may throw ArithmeticException with / by zero message . In the server side , I may write :

    @Override
    public void divide(DivideRequest request, StreamObserver<DivideResponse> responseObserver) {
      int dom = request.getDenominator();
      int num = request.getNumerator();
    
      double result = num / dom;
      responseObserver.onNext(DivideResponse.newBuilder().setValue(result).build());
      responseObserver.onCompleted();
    }
    

    If the client passes denominator = 0 , it will get :

    Exception in thread "main" io.grpc.StatusRuntimeException: UNKNOWN
    

    And the server outputs

    Exception while executing runnable io.grpc.internal.ServerImpl$JumpToApplicationThreadServerStreamListener$2@62e95ade
    java.lang.ArithmeticException: / by zero
    

    The client doesn't know what's going on.

    If I want to pass / by zero message to client , I have to modify server to : (as described in this question )

      try {
        double result = num / dom;
        responseObserver.onNext(DivideResponse.newBuilder().setValue(result).build());
        responseObserver.onCompleted();
      } catch (Exception e) {
        logger.error("onError : {}" , e.getMessage());
        responseObserver.onError(new StatusRuntimeException(Status.INTERNAL.withDescription(e.getMessage())));
      }
    

    And if client sends denominator = 0 , it will get :

    Exception in thread "main" io.grpc.StatusRuntimeException: INTERNAL: / by zero
    

    Good , / by zero is passed to client.

    But the problem is , in a truly enterprise environment, there will be a lot of RuntimeExceptions , and if I want to pass these exception's messages to client , I will have to try catch each method , which is very cumbersome.

    Is there any global interceptor that intercepts every method , catching RuntimeException and trigger onError and propagate the error message to client ? So that I don't have to deal with RuntimeExceptions in my server code .

    Thanks a lot !

    Note :

    <grpc.version>1.0.1</grpc.version>
    com.google.protobuf:proton:3.1.0
    io.grpc:protoc-gen-grpc-java:1.0.1