gRPC Interceptor: unary interceptor with code example

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 cross-cutting 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 the gRPC but if are new to gRPC you can check the following articles.

Let’s get started.

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 token 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 the real world Spring 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 behavior.


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 interface ClientInterceptor and provides the implementation of the interceptCall method.
  • The interceptCall method implementation expects a ClientCall as return type.
  • We create a new ClientCall object as ForwardingClientCall.SimpleForwardingClientCall, by passing delegate as channel.newCall(methodDescriptor, callOptions) the constructor argument.
  • We override start method and add custom logic and then call super.start(..).

Intercepting response

If needed, you can intercept the response from the server before it’s processed by the client application. 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. The reason for this is that the default transport protocol of gRPC is based on Netty, which is an asynchronous, non-blocking I/O framework. 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 for some reason 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 favorite IDE as 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 cross-cutting 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 token or implementing authorization.
  • Adding contextual information to the metadata.
  • Logging metrics, etc.

If you like this article, then please follow me on LinkedIn  for more tips on #software architecture.

Social Share !
Default image
Pankaj

Software Architect @ Schlumberger ``` Cloud | Microservices | Programming | Kubernetes | Architecture | Machine Learning | Java | Python ```