Exception Handling and Error Propagation in gRPC Java
It is quite important to propagate detailed error information from the server to the client in case something goes wrong, but the gRPC documentation lacks details on this topic.
In this tutorial, we are going to look at how to handle exceptions in the gRPC Java server and provide information about them to clients.
1. Server and Client
To begin, we will create a simple server and client.
1.1. Proto
message GreetingRequest {
string name = 1;
}
message GreetingResponse {
string greeting = 1;
}
service GreetingService {
rpc greeting (GreetingRequest) returns (GreetingResponse);
}
1.2. Server
public class GreetingServer {
public static void main(String[] args) throws IOException, InterruptedException {
Server server = ServerBuilder
.forPort(8080)
.addService(new GreetingService())
.build();
server.start();
System.out.println("gRPC Server started, listening on port:" + server.getPort());
server.awaitTermination();
}
private static class GreetingService extends GreetingServiceGrpc.GreetingServiceImplBase {
@Override
public void greeting(GreetingRequest request, StreamObserver<GreetingResponse> responseObserver) {
String name = request.getName();
String greeting = String.format("Hello, %s!", name.isBlank() ? "World" : name);
GreetingResponse response = GreetingResponse.newBuilder()
.setGreeting(greeting)
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
}
1.3. Client
public class GreetingClient {
public static void main(String[] args) {
var channel = ManagedChannelBuilder.forAddress("localhost", 8080)
.usePlaintext()
.build();
var stub = GreetingServiceGrpc.newBlockingStub(channel);
GreetingRequest request = GreetingRequest.newBuilder().build();
GreetingResponse response = stub.greeting(request);
System.out.println(response.getGreeting());
}
}
1.4. Result
We created a simple server and client to it, which sends the request and outputs the response to the console.
Note: on the server, we process requests with a missing name and use World
in this case,
and since our client does not include any name in the request, we will see Hello, World!
in the output.
2. Exception Handling
Let’s move on to the main issue, exception handling.
2.1 Throw Exception
Now we do not want to accept requests with a missing name, so we update our server to throw exception in this case. We replace the line:
String greeting = String.format("Hello, %s!", name.isBlank() ? "World" : name);
With:
if (name.isBlank()) {
throw new IllegalArgumentException("Missing name");
}
String greeting = String.format("Hello, %s!", name);
If we start our server and client, we will see an exception in the server console:
java.lang.IllegalArgumentException: Missing name
We will also see an exception in the client console, but there is no information about what went wrong:
Exception in thread "main" io.grpc.StatusRuntimeException: UNKNOWN
2.2 Handle Exception
Our goal is to provide more detailed information about exceptions on the server to the client. To do this, we create an interceptor that will catch exceptions and handle them:
public class ExceptionHandler implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall, Metadata metadata,
ServerCallHandler<ReqT, RespT> serverCallHandler) {
ServerCall.Listener<ReqT> listener = serverCallHandler.startCall(serverCall, metadata);
return new ExceptionHandlingServerCallListener<>(listener, serverCall, metadata);
}
private class ExceptionHandlingServerCallListener<ReqT, RespT>
extends ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT> {
private ServerCall<ReqT, RespT> serverCall;
private Metadata metadata;
ExceptionHandlingServerCallListener(ServerCall.Listener<ReqT> listener, ServerCall<ReqT, RespT> serverCall,
Metadata metadata) {
super(listener);
this.serverCall = serverCall;
this.metadata = metadata;
}
@Override
public void onHalfClose() {
try {
super.onHalfClose();
} catch (RuntimeException ex) {
handleException(ex, serverCall, metadata);
throw ex;
}
}
@Override
public void onReady() {
try {
super.onReady();
} catch (RuntimeException ex) {
handleException(ex, serverCall, metadata);
throw ex;
}
}
private void handleException(RuntimeException exception, ServerCall<ReqT, RespT> serverCall, Metadata metadata) {
if (exception instanceof IllegalArgumentException) {
serverCall.close(Status.INVALID_ARGUMENT.withDescription(exception.getMessage()), metadata);
} else {
serverCall.close(Status.UNKNOWN, metadata);
}
}
}
}
Note: in the interceptor, we use a private class that inherits SimpleForwardingServerCallListener
,
and overrides onHalfClose
and onReady
methods to handle exceptions.
There is no point to override onCancel
and onComplete
, since it is already too late.
Next we need to add an interceptor to the server:
Server server = ServerBuilder
// ...
.intercept(new ExceptionHandler())
.build();
Now, when we start the server and client, we will see a more informative exception in the client console:
Exception in thread "main" io.grpc.StatusRuntimeException: INVALID_ARGUMENT: Missing name
3. Conclusion
In our example, we handle only IllegalArgumentException
, but you can handle any other exceptions in the same way
and also include much more information in the response besides the exception message.
Full source code can be found on GitHub.