Similar to many popular frameworks, gRPC has the concept of an interceptor. Conceptually, it’s very similar to the middleware/interceptor found in other frameworks, for example, Node JS middleware, Spring Boot interceptor, Django middleware, etc. The gRPC interceptor allows us to intercept gRPC remote procedure calls (RPC) and add code to handle the crosscutting concerns.
In this article, we will understand the gRPC unary interceptor with code examples in Java. This blog assumes that you have a general understanding of gRPC, but if you are new to gRPC, you can check the following articles.
Let’s get started.
In this article
What are gRPC Interceptors?
As the name suggests, the gRPC interceptor allows you to intercept RPC. By using an interceptor, you can write reusable code for cross-cutting concerns such as logging, authentication, metrics, etc.
In gRPC, you can intercept RPC at both the client and server. Also, gRPC supports intercepting both unary and streaming RPC.
There can be many use cases for a gRPC interceptor, such as:
- Tracing: passing tracing information from the client application to the server.
- Logging: logging API calls in both client and server.
- Auth token: passing authentication information, for example, JWT, from client to server.
- Authentication/Authorization: validating authentication tokens or implementing authorization.
- Adding contextual information to the metadata.
- Logging, Metrics, etc.
Client unary interceptor
Client unary interceptors are executed on the client side when a unary request is made to the server. As you may know, in the unary request, the client sends a single request and receives a single response. To define a unary client interceptor, you need to implement ClientInterceptor
interface and implement interceptCall
method. As you have access to the metadata object, you can add arbitrary information, such as traceId, authentication token, etc., to the metadata. The metadata object is propagated to the server, along with the request object, in the RPC call.
You can chain as many interceptors as you want. The below diagram shows how the client-side unary interceptor works.
Intercepting request
The below code shows an example of a client request interceptor, where we modify the metadata object to include the caller’s JWT token.
In a Spring-based web application, this client call can be part of a web API call, and the user token can be stored in the SecurityContext
. Here, UserContext
simulates SecurityContext
behaviour.
public class GrpcClientRequestInterceptor implements ClientInterceptor {
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
final MethodDescriptor<ReqT, RespT> methodDescriptor,
final CallOptions callOptions,
final Channel channel) {
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
channel.newCall(methodDescriptor, callOptions)) {
@Override
public void start(ClientCall.Listener<RespT> responseListener, Metadata headers) {
var userToken = UserContext.getUserContext().getUserToken();
log.info("Setting userToken in header");
headers.put(Metadata.Key.of("JWT", Metadata.ASCII_STRING_MARSHALLER), userToken);
super.start(responseListener, headers);
}
};
}
}
Dissecting GrpcClientRequestInterceptor
implementation:
- The
GrpcClientRequestInterceptor
implements the interfaceClientInterceptor
and provides the implementation of theinterceptCall
method. - The
interceptCall
method implementation expects aClientCall
as return type. - We create a new
ClientCall
object asForwardingClientCall.SimpleForwardingClientCall
, by passing delegate aschannel.newCall(methodDescriptor, callOptions)
the constructor argument. - We override
start
method and add custom logic, and then callsuper.start(..)
.
Intercepting response
If needed, you can intercept the response from the server before the client application processes it. The below code shows an example of intercepting the server response on the client interceptor.
public class GrpcClientResponseInterceptor implements ClientInterceptor {
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
final MethodDescriptor<ReqT, RespT> methodDescriptor,
final CallOptions callOptions,
final Channel channel) {
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
channel.newCall(methodDescriptor, callOptions)) {
@Override
public void start(Listener<RespT> responseListener, Metadata headers) {
super.start(
new ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT>(
responseListener) {
@Override
public void onMessage(RespT message) {
log.debug("Received response from Server: {}", message);
super.onMessage(message);
}
},
headers);
}
};
}
}
This code is similar to the above, except we wrap responseListener
as new ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener)
and override onMessage(RespT message)
method.
Configuring client interceptor
You can associate ClientInterceptors
with the channel
as:
var managedChannel =
ManagedChannelBuilder.forAddress(host, port)
.intercept(new GrpcClientResponseInterceptor(), new GrpcClientRequestInterceptor())
.usePlaintext()
.build();
The interceptors run in the reverse order in which they are added.
Server unary interceptor
Server unary interceptors are executed on the server side when a unary request is received from the client. As shown in the below diagram, you can intercept both requests to the server and responses sent from the server.
Like client interceptors, you can write different interceptors to solve different concerns and chain them together. The following diagram shows how the server-side unary interceptor works.
Intercepting request
To define a server interceptor, you need to implement the ServerInterceptor
interface and provide an implementation of interceptCall
method.
The below code shows an example of a server request interceptor, which authenticates a service call by validating the JWT token passed in the metadata object from the client call.
public class GrpcServerRequestInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> serverCall, Metadata metadata, ServerCallHandler<ReqT, RespT> next) {
log.info("Validating user token");
var userToken = metadata.get(Metadata.Key.of("JWT", Metadata.ASCII_STRING_MARSHALLER));
validateUserToken(userToken);
return next.startCall(serverCall, metadata);
}
private void validateUserToken(String userToken) {
// Logic to validate token
}
}
Intercepting response
Similar to the client interceptor, you can intercept a response from the server before it’s sent back to the client. The below code shows an example of a server response interceptor.
public class GrpcServerResponseInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> serverCall, Metadata metadata, ServerCallHandler<ReqT, RespT> next) {
return next.startCall(
new ForwardingServerCall.SimpleForwardingServerCall<>(serverCall) {
@Override
public void sendMessage(RespT message) {
log.info("Message being sent to client : " + message);
super.sendMessage(message);
}
},
metadata);
}
}
To get access to the response
object, you can wrap the serverCall
object with ForwardingServerCall.SimpleForwardingServerCall
and override sendMessage
method.
Configuring server interceptor
You can associate the server interceptor with the gRPC services
as:
Server server =
ServerBuilder.forPort(port)
.addService(
ServerInterceptors.intercept(
productService,
new GrpcServerResponseInterceptor(),
new GrpcServerRequestInterceptor()))
.build();
Context propagation in gRPC
Distributed tracing is one of the main pillars of microservices observability. One of the common patterns followed in the microservices architecture is to log traceId
with every log so that end-to-end request calls can be traced. In other words, you can filter the complete log associated with a particular request provided your application does structure logging and stores logs in a central storage solution that provides query capabilities, for example, Elastic search, Stackdriver, etc. . In the java based web applications, for example, Spring Boot application, this is typically done by storing traceId in MDC, which is nothing but a ThreadLocal
variable.
If you plan to use the ThreadLocal
variable in the interceptor, then it’ll not work. This is because the default transport protocol of gRPC is based on Netty, an asynchronous, non-blocking I/O web server. For that reason, dispatch/callback from the interceptor can happen in different threads.
If you need to propagate context in gRPC, then you can use gRPC context. A context propagation mechanism allows you to carry scoped values across API boundaries and between threads. Examples of states propagated via context include:
- Security principals and credentials.
- Local and distributed tracing information.
ThreadLocal variable in the interceptor
As mentioned above, storing the ThreadLocal
variable in the interceptor does not work. But if you can’t use gRPC Context, and you need to store value in the ThreadLocal variable, then you can achieve that by overriding each callback of the listner
as shown below.
public class GrpcMDCInterceptor implements ServerInterceptor {
private static final String TRACE_ID = "traceId";
@Override
public <R, S> ServerCall.Listener<R> interceptCall(
ServerCall<R, S> serverCall, Metadata metadata, ServerCallHandler<R, S> next) {
log.info("Setting user context, metadata {}", metadata);
var traceId = metadata.get(Metadata.Key.of("traceId", Metadata.ASCII_STRING_MARSHALLER));
MDC.put(TRACE_ID, traceId);
try {
return new WrappingListener<>(next.startCall(serverCall, metadata), traceId);
} finally {
MDC.clear();
}
}
private static class WrappingListener<R>
extends ForwardingServerCallListener.SimpleForwardingServerCallListener<R> {
private final String traceId;
public WrappingListener(ServerCall.Listener<R> delegate, String traceId) {
super(delegate);
this.traceId = traceId;
}
@Override
public void onMessage(R message) {
MDC.put(TRACE_ID, traceId);
try {
super.onMessage(message);
} finally {
MDC.clear();
}
}
@Override
public void onHalfClose() {
MDC.put(TRACE_ID, traceId);
try {
super.onHalfClose();
} finally {
MDC.clear();
}
}
@Override
public void onCancel() {
MDC.put(TRACE_ID, traceId);
try {
super.onCancel();
} finally {
MDC.clear();
}
}
@Override
public void onComplete() {
MDC.put(TRACE_ID, traceId);
try {
super.onComplete();
} finally {
MDC.clear();
}
}
@Override
public void onReady() {
MDC.put(TRACE_ID, traceId);
try {
super.onReady();
} finally {
MDC.clear();
}
}
}
}
Code Example
The working code example of this article is listed on GitHub . To run the example, clone the repository, and import grpc-unary-rpc as a project in your favourite IDE as a Gradle project.
To build the project and generate client and server stubs, run the command .\gradlew clean build
. You can start the gRPC server in IDE by running the main method of the class GrpcServer
. The gRPC server runs on localhost:3000.
You can find the client interceptor code in the package dev.techdozo.client.interceptor
in the grpc-client
module and the server interceptor code in the package dev.techdozo.product.interceptor
in the grpc-server
module.
Summary
The gRPC interceptor allows us to intercept gRPC remote procedure calls (RPC) and add code to handle the crosscutting concerns. A gRPC interceptor can be of type client and server. gRPC supports interceptors for both unary and streaming RPC.
Typical use cases for a gRPC interceptor are:
- Tracing: passing tracing information from the client application to the server.
- Logging: logging API calls in both client and server.
- Auth token: passing authentication information, for example, JWT from client to server.
- Authentication/Authorization: validating authentication tokens or implementing authorization.
- Adding contextual information to the metadata.
- Logging metrics, etc.