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.
- Protocol Buffers – A Simple Introduction
- gRPC – An Introduction Guide
- gRPC Unary API In Java – Easy Steps
- 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.
- We introduce few custom error codes using enum
- We also have a dedicated object called ErrorResponse with custom parameters.
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) {};
}
- Server side
- We build the custom error response object and we send it to the client via Metadata.
@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();
}
- Client side
- We do the try-catch as usual. But we could also access metadata and the custom ErrorResponse object from the exception.
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.
- protobuf
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) {};
}
- Server side
@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();
}
- Client side
- Client is not expected to receive any exception. Instead it will receive 2 possible response types. SUCCESS_RESPONSE or ERROR_RESPONSE. Depends on the type of object we receive, appropriate action will be performed.
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;
}
- I send 2 inputs. One for 10 and another for 50. I get response as shown below.
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 🙂