gRPC exception handling

Recently, I used gRPC to create a service for the first time. On the server side, I want to throw a custom exception directly so that the client can see it. At first, I tried this:

//        responseObserver.onError(new CustomException("custom exception"));
        throw new CustomException("one error occurs");

But I got an awkward result:

io.grpc.StatusRuntimeException: UNKNOWN

The client can't see the exception error message that my custom throws. After some research, we find two kinds of clients that can get the custom exception information thrown out by the server.

Mode 1: set the description from the exception message to Status

The server implementation is as follows:

    // Custom exception handling
    @Override
    public void customException(EchoRequest request, StreamObserver<EchoResponse> responseObserver) {

        try {
            if (request.getMessage().equals("error")) {
                throw new CustomException("custom exception message");
            }
            EchoResponse echoResponse = EchoResponse.newBuilder().build();
            responseObserver.onNext(echoResponse);
            responseObserver.onCompleted();
        } catch (CustomException e) {
            responseObserver.onError(Status.INVALID_ARGUMENT
                  // Here is our custom exception information
                    .withDescription(e.getMessage())
                    .withCause(e)
                    .asRuntimeException());
        }
    }

Use status.invalid'argument to specify the exception code. This code also indicates that there is a problem with the parameter. The exceptions that need to be explicitly thrown by the normal server are mostly parameter problems. If it is a service problem, you don't need to do special processing. Let it throw directly.

Client call:

   try {
            EchoResponse echoResponse = stub.customException(
                    EchoRequest.newBuilder().setMessage("error").build());
            System.out.println(echoResponse.getMessage());
        } catch (StatusRuntimeException e) {
            e.printStackTrace();
            // INVALID_ARGUMENT: occurs exception
            // This message will contain invalid menu argument, which is not what we want
            System.out.println(e.getMessage());
            if (e.getStatus().getCode() == Status.Code.INVALID_ARGUMENT) {
                // This is the custom exception information we want
                System.out.println(e.getStatus().getDescription());
                // Throw CustomException for our ExceptionHandler
                throw new CustomException(e.getStatus().getDescription());
            } else {
                throw e;
            }
        }

Mode 2: pass more detailed error information through MetaData

In this way, an ErrorInfo is customized in the proto file:

message ErrorInfo {
  // There are a lot of error messages in the list
  repeated string message = 1;
}

The ErrorInfo defined here can carry a lot of information, for example, you can define a code field in it, and then you can represent more abundant information.

In the server implementation class:

    private static final Metadata.Key<ErrorInfo> ERROR_INFO_TRAILER_KEY =
            ProtoUtils.keyForProto(ErrorInfo.getDefaultInstance());
    
    @Override
    public void detailErrorMessage(EchoRequest request, StreamObserver<EchoResponse> responseObserver) {
        try {
            if (request.getMessage().equals("error")) {
                throw new CustomException("custom exception message");
            }
            EchoResponse echoResponse = EchoResponse.newBuilder().build();
            responseObserver.onNext(echoResponse);
            responseObserver.onCompleted();
        } catch (CustomException e) {
            Metadata trailers = new Metadata();
            ErrorInfo.Builder builder = ErrorInfo.newBuilder()
                    .addMessage(e.getMessage());
            trailers.put(ERROR_INFO_TRAILER_KEY, builder.build());
            responseObserver.onError(Status.INVALID_ARGUMENT
                    .withCause(e)
                    .asRuntimeException(trailers));
        }
    }

Then look at the client call:

 try {
            EchoResponse echoResponse = stub.detailErrorMessage(
                    EchoRequest.newBuilder().setMessage("error").build());
            System.out.println(echoResponse.getMessage());
        } catch (StatusRuntimeException e) {
            if (e.getStatus().getCode() == Status.Code.INVALID_ARGUMENT) {
                Metadata trailers = Status.trailersFromThrowable(e);
                if (trailers.containsKey(ERROR_INFO_TRAILER_KEY)) {
                    ErrorInfo errorInfo = trailers.get(ERROR_INFO_TRAILER_KEY);
                    if (errorInfo.getMessageList() != null && errorInfo.getMessageList().size() != 0) {
                        // This is the custom exception information we want
                        System.out.println(errorInfo.getMessageList());
                    }
                }
            } else {
                throw e;
            }
        }

The above are all exception handling of synchronous invocation of client. There are some small differences in exception handling of asynchronous invocation. For complete code, please refer to: https://github.com/jiaobuchong/grpc-learning/tree/master/grpc-error-handling

Reference resources:
https://github.com/grpc/grpc-java/tree/master/examples
Introduction to gRPC
https://grpc.github.io/grpc/core/md_doc_statuscodes.html
https://stackoverflow.com/questions/48748745/pattern-for-rich-error-handling-in-grpc

140 original articles published, 186 praised, 740000 visitors+
His message board follow

Keywords: github Java

Added by maxxx on Sat, 29 Feb 2020 07:36:27 +0200