Site icon Vinsguru

gRPC Error Handling

Overview:

In this tutorial, I would like to demo various options we have for gRPC Error Handling.

I assume that you have a basic understanding of what gRPC is. If not, read the below articles first.

  1. Protocol Buffers – A Simple Introduction
  2. gRPC – An Introduction Guide
  3. gRPC Unary API In Java – Easy Steps
  4. gRPC Server Streaming API In Java

gRPC Error Handling:

gRPC is a great choice for developing client-server application or Microservices development. In a simple client-server application, Client sends a request to the sever & the Server process the request and sends the response back. Sending success response is simple and straight forward. But we can not assume that client would always be sending a valid request. Client might send a request without any auth token or values could be out of range which server can not handle etc.

In those cases, the Server has to properly communicate the message to the client via a message / error code etc.

Sample Application:

Lets consider a simple Calculator application which calculates the square for the given number. The client sends a number for which the server responds the square.

The server is capable of calculating the square only for the numbers between 2 and 20. Anything outside this range should be rejected with appropriate error message.

Protobuf – Service Definition:

Let’s create a service definition for the above scenario. findSquare is going to be the method to be implemented on the server side. This service definition shows what type of input to be sent and what type of output to expect.

syntax = "proto3";

package calculator;

option java_package = "com.vinsguru.calculator";
option java_multiple_files = true;

message Request {
  int32 number = 1;
}

message Response {
  int32 result = 1;
}

service CalculatorService {
  rpc findSquare(Request) returns (Response) {};
}

When we issue the below maven command, maven automatically creates the client and server side code using protoc tool.

mvn clean compile

For example, CalculatorServiceImplBase class in the below picture is auto-generated abstract class which needs to be implemented by the server for the above service definition. Similarly CalculatorServiceStub is the actual implementation class which client should use to make a request.

A simple service definition file does most of the heavy lifting already for the client server communication.

Server Side:

Lets first implement the square calculation without worrying about any range.

public class GrpcSquareService extends CalculatorServiceGrpc.CalculatorServiceImplBase {

    @Override
    public void findSquare(Request request, StreamObserver<Response> responseObserver) {
        int number = request.getNumber();
        Response response = Response.newBuilder()
                                    .setResult(number * number)
                                    .build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

Once the service implementation is done, Let’s add it to the server to serve the client calls. We are listening on port 6565. Start this server by invoking the main method.

public class CalculatorServer {

    public static void main(String[] args) throws IOException, InterruptedException {

        // build gRPC server
        Server server = ServerBuilder.forPort(6565)
                .addService(new GrpcSquareService())
                .build();

        // start
        server.start();

        // shutdown hook
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("gRPC server is shutting down!");
            server.shutdown();
        }));

        server.awaitTermination();

    }

}

Now our server is ready, up and running!

Success Response:

Let’s first see if we receive a response successfully from the server.

public class SquareServiceTest {

    private ManagedChannel channel;
    private CalculatorServiceGrpc.CalculatorServiceBlockingStub clientStub;

    @Before
    public void setup(){
        this.channel = ManagedChannelBuilder.forAddress("localhost", 6565)
                .usePlaintext()
                .build();
        this.clientStub = CalculatorServiceGrpc.newBlockingStub(channel);
    }

    @Test
    public void squareServiceHappyPath(){
        // build the request object
        Request request = Request.newBuilder()
                .setNumber(50)
                .build();
        Response response = this.clientStub.findSquare(request);
        System.out.println("Success Response : " + response.getResult());
    }

    @After
    public void teardown(){
        this.channel.shutdown();
    }

}

Output:

When we run this test, we get the response as shown here.

Success Response : 2500

gRPC Error Handling – OnError:

Ok.! Lets implement the range validation. The server can validate the input and  if it is not in the given range, it can use the StreamObserver’s onError method as shown here to indicate the client that the pre-condition is failed.

public class GrpcSquareService extends CalculatorServiceGrpc.CalculatorServiceImplBase {

    @Override
    public void findSquare(Request request, StreamObserver<Response> responseObserver) {
        int number = request.getNumber();

        if(number < 2 || number > 20){
            Status status = Status.FAILED_PRECONDITION.withDescription("Not between 2 and 20");
            responseObserver.onError(status.asRuntimeException());
            return;
        }
        
        // only valid ranges
        Response response = Response.newBuilder()
                                    .setResult(number * number).
                                    build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
    
}

If the client re-sends a request, we see an exception at the client side as shown below.

io.grpc.StatusRuntimeException: FAILED_PRECONDITION: Not between 2 and 20

    at io.grpc.stub.ClientCalls.toStatusRuntimeException(ClientCalls.java:244)
    at io.grpc.stub.ClientCalls.getUnchecked(ClientCalls.java:225)
    at io.grpc.stub.ClientCalls.blockingUnaryCall(ClientCalls.java:142)

We can catch the exception in a try-catch block as usual. We can also access the Status object from the exception.

try{
    Response response = this.clientStub.findSquare(request);
    System.out.println("Success Response : " + response.getResult());
}catch (Exception e){
    Status status = Status.fromThrowable(e);
    System.out.println(status.getCode() + " : " + status.getDescription());
}

gRPC Error Handling – Metadata:

The above approach works fine. However we were able to send only one of gRPC predefined error codes. What if we need to send some custom error code / message / object. In this case, we first define how our error response should be using protobuf.

syntax = "proto3";

package calculator;

option java_package = "com.vinsguru.calculator";
option java_multiple_files = true;

message Request {
  int32 number = 1;
}

message Response {
  int32 result = 1;
}

enum ErrorCode {
  ABOVE_20 = 0;
  BELOW_2 = 1;
}

message ErrorResponse {
  int32 input = 1;
  ErrorCode error_code = 2;
}

service CalculatorService {
  rpc findSquare(Request) returns (Response) {};
}
@Override
public void findSquare(Request request, StreamObserver<Response> responseObserver) {
    int number = request.getNumber();

    if(number < 2 || number > 20){
        Metadata metadata = new Metadata();
        Metadata.Key<ErrorResponse> responseKey = ProtoUtils.keyForProto(ErrorResponse.getDefaultInstance());
        ErrorCode errorCode = number > 20 ? ErrorCode.ABOVE_20 : ErrorCode.BELOW_2;
        ErrorResponse errorResponse = ErrorResponse.newBuilder()
                .setErrorCode(errorCode)
                .setInput(number)
                .build();
        // pass the error object via metadata
        metadata.put(responseKey, errorResponse);
        responseObserver.onError(Status.FAILED_PRECONDITION.asRuntimeException(metadata));
        return;
    }

    // only valid ranges
    Response response = Response.newBuilder()
                                .setResult(number * number).
                                build();
    responseObserver.onNext(response);
    responseObserver.onCompleted();
}
try{
    Response response = this.clientStub.findSquare(request);
    System.out.println("Success Response : " + response.getResult());
}catch (Exception e){
    Metadata metadata = Status.trailersFromThrowable(e);
    ErrorResponse errorResponse = metadata.get(ProtoUtils.keyForProto(ErrorResponse.getDefaultInstance()));
    System.out.println(errorResponse.getInput() + " : " + errorResponse.getErrorCode());
}

It prints the output as shown below for the input 50.

50 : ABOVE_20

gRPC Error Handling – OneOf:

Why do we treat anything outside the range as an error & throw exception? It could also be treated as a possible input and we could send appropriate error response instead of exception. The server can have 2 possible responses and will send one of them using oneof.

message Request {
  int32 number = 1;
}

message SuccessResponse {
  int32 result = 1;
}

enum ErrorCode {
  ABOVE_20 = 0;
  BELOW_2 = 1;
}

message ErrorResponse {
  int32 input = 1;
  ErrorCode error_code = 2;
}

message Response {
  oneof response {
    SuccessResponse success_response = 1;
    ErrorResponse error_response = 2;
  }
}

service CalculatorService {
  rpc findSquare(Request) returns (Response) {};
}
@Override
public void findSquare(Request request, StreamObserver<Response> responseObserver) {
    int number = request.getNumber();

    Response.Builder builder = Response.newBuilder();
    if(number < 2 || number > 20){
        ErrorCode errorCode = number > 20 ? ErrorCode.ABOVE_20 : ErrorCode.BELOW_2;
        ErrorResponse errorResponse = ErrorResponse.newBuilder()
                .setInput(number)
                .setErrorCode(errorCode)
                .build();
        builder.setErrorResponse(errorResponse);
    }else{
        // only valid ranges
        builder.setSuccessResponse(SuccessResponse.newBuilder().setResult(number * number).build());
    }
    responseObserver.onNext(builder.build());
    responseObserver.onCompleted();
}
Response response = this.clientStub.findSquare(request);

switch (response.getResponseCase()){
    case SUCCESS_RESPONSE:
        System.out.println("Success Response : " + response.getSuccessResponse().getResult());
        break;
    case ERROR_RESPONSE:
        System.out.println("Error Response : " + response.getErrorResponse().getErrorCode());
        break;
}
Success Response : 100
Error Response : ABOVE_20

Summary:

We were able to successfully demonstrate gRPC Error Handling – different ways to respond with client for an invalid input. You can go with any of these options depending on your use-case.

Learn gRPC – a complete tutorial here.

Read more about gRPC:

The source code is available here.

Happy learning 🙂

Share This:

Exit mobile version