-->

Interceptors in gRPC

Posted by : at

Category : go   grpc


Photo by Thom on Unsplash Many people I talk to are well aware of what middleware is. However, Interceptors seems to be less understood. Let’s dissect interceptors and learn how to use and write them and why we should do it.

gRPC is a great technology and I’ve been replacing many of my APIs with it. I’ve found that writing gRPC is easier than the regular HTTP based APIs. I suggest you invest time in learning it.

This is part 2 of a series, however, you don’t need prior knowledge and can continue without part 1 and 1.1, but I suggest reading them.

In a regular HTTP server, we would have a middleware wrapping our handler on the server. This middleware can be used to perform anything the server wants to do before actually serving the correct content, it can be Authentication or logging or anything.

gRPC is different, It allows Interceptors to be used in both the server and client. This is pretty nice since it allows users or consumers of APIs to add any wanted interceptor, such as custom logging.

Before we dig deep in interceptors I believe there is something in gRPC itself we need to understand. It’s the core concept of gRPC services.

gRPC core concepts

There are two words used in gRPC services that we must understand.
Unary is what most of us are use too. You send ONE request and get ONE response.
Stream is when you instead send or receive a data pipeline of protobuf messages. This means that if an gRPC service responds with a stream, the consumer can expect more than one response inside this stream. Streams are noticed by the keyword Stream, easy enough right?

In short gRPC allows four different RPC calls.

  • Unary RPC Unary RPC is when a client can send ONE request, and get ONE response.
  • Server Streaming RPC
    Server streaming RPC is when the client sends a unary request, but is returned with a stream
  • Client Streaming RPC
    Client streaming RPC is when the client sends a stream request, and is returned with a unary response. The server response is sent when all messages are consumed.
  • Bidirectional Streaming RPC
    Bidirectional streaming means that the client sends a stream request, and is returned with a stream response.

So how do we know what these calls looks like?
Here are some examples from a fictional User RPC service.

// Unary example, One request, One Response  
rpc GetUser(UserRequest) returns (UserResponse);  
// Server streaming example, Unary request, Stream response  
rpc GetNewUsers(EmptyRequest) returns (stream UserResponse);  
// Client streaming example, Stream request, Unary Response  
rpc RegisterUsers(stream UserRequest) returns (StatusResponse);  
// Bidirectional example, Stream request, Stream response  
rpc FindUser(stream UserRequest) returns (stream UserResponse);

Now that we understand the messages that gRPC supports it’s easier to learn interceptors. Let’s start by going through what an interceptor is and the different kinds that exists.

An interceptor is what it sounds like, it intercepts API requests before they are executed. This can be used to log, authenticate or anything you want to happen before your API requests are handled. With HTTP APIs this is easy in Golang, you wrap your HTTP handlers with a middleware. With gRPC it requires some more knowledge, hence the explanations before about Unary and Stream.

There are two kinds of interceptors, and two sides of the communication.

The two kinds of interceptors

  • UnaryInterceptors —Used in API calls which is One client request and one server response.
  • StreamInterceptors — Used in API calls in which a client sends a request but receives a data stream back, allowing the server to respond multiple items over time. Actually, since gRPC is bidirectional this can be used for clients to send data as well.

The two sides of interceptors

  • Client — Interceptors that are triggered on the client side
  • Server — Interceptors that are triggered on the server side

Remember that I said gRPC interceptors can be more difficult to grasp than middlewares in HTTP? Its because of this, we actually have too know which kind of gRPC call we want to intercept.

2 + 2 = 4

Since gRPC allows interceptors in both Client and Server, and in Unary and Streamed calls. We have 4 different interceptors.

If we go to the go-grpc library to see how they handle this we can see the four different use cases. Both interceptor types are available for both Server and Client.

The gist below shows the definition of all gRPC interceptor kinds available.

Metadata

So we now know what interceptors are. It’s time to discuss what I like to use them for and why. gRPC allows custom metadata to be sent. Metadata is a pretty simple concept of Key Value.

If we look at the golang metadata specification we can see its a map[string][]string.

Metadata can be sent as header or trailer.

  • Header should be sent before the data.
  • Trailer should be sent after the processing is complete

Metadata allows us to add data to requests without changing the protobuf messages. This is commonly used for adding data that is related to the request but not part of it. For instance, we can add JWT tokens as authentication in the metadata of a request. This allows us to extend an API endpoint with logic without altering the actual server logic. This can be useful for authentication, ratelimiting or loggging.

Lets Go!

Enough theory! I believe we are ready to start testing it.
If you haven’t already, I suggest reading [Part 1] — using gRPC with TLS, Golang, React without Envoy.
If you don’t want too — well here is the repository that we will use as a base.

Its a simple server running a Ping gRPC API. We will update this Ping API with some interceptors to learn how to do them.

Start by grabbing the repository, build the web app and generate valid TLS keys by running

mkdir grpcexample-interceptors  
git init  
git pull [https://github.com/percybolmer/grpcexample](https://github.com/percybolmer/grpcexample)  
cd cert && sudo bash certgen.sh  
cd ../ui/pingpongapp && npm install && npm run build && cd ../../

Make sure everything works before keep moving on by running the program and then visiting localhost.

go run \* .go

The first thing we will do is adding a PingCounter to our API. Lets pretend that this amazing Ping application is sold to costumers. And we use the same gRPC Server and Client for many customers. But this one customer we are working for today wants to count the Pings that’s been performed. This is great, we can wrap our Unary Ping function with a Fancy New Interceptor without bloating the code for our Server.

The counting will be done in the Server. This means the first interceptor we will create is a UnaryServerInterceptor. If you don’t remember what that means, feel free to reread.

A UnaryServerInterceptor was defined in the go-grpc library as:

type UnaryServerInterceptor func(ctx context.Context, req interface{},  info \*UnaryServerInfo, handler UnaryHandler) (resp interface{}, err  error)  

Lets begin by making a new folder called interceptors, and in there a file called pingCounter.go.

mkdir interceptors  
touch pingCounter.go

Open pingCounter.go, this is the place where we will write the interceptor and the package will be called interceptors. Now we are going to create a struct which holds an integer that counts the number of Ping requests. I like using structs with Interceptors as methods, it’s easy to access needed data this way. It also allows for access to databases since your struct can contain that, which can be good in authentication interceptors. You could use an global variable instead, but I tend to avoid them. I know it’s also common for developers to return a gRPC interceptor in a method. It’s another way of doing it that looks like the following gist, code from go-grpc-middleware.

The gist below is from go-grpc-middleware repository and shows how they instead return a function.

Our Interceptor is super easy, it will count the number of Ping requests since starting the API, and attach that to the metadata of the response.

This code shows a super simple UnaryServerInterceptor Actually once we are inside pingCounter.go, we can take the time to also create a UnaryClientInterceptor. They are defined as

type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc \*ClientConn, invoker UnaryInvoker, opts ...CallOption) error

This is an Example of a UnaryClientInterceptor. Now my pingCounter.go looks like the following gist.

A full example of how pingCounter.go looks

We will now need to apply this interceptor to the server and the client. Open up main.go and scroll down to line 68 where you will find GenerateTLSApi. This function returns a grpc.Server, lets apply the interceptor to it. Adding a interceptor is done by adding it as an argument to the grpc.NewServer(). We will look at chaining interceptors later on. As you can see, we haven’t actually changed any logic on the grpc service, only the initialization of it. This is nice if our server were some super advanced authorization service. We easily added functionality without touching any logic in server.go

We have added a PingCounter interceptor to the server

Lets also open up client/main.go. We will use the client to make sure everything works before we move on into also adding the new features to the react application. We will want to wrap our gRPC connection with our ClientPingCounter to see how many pings are performed by the client.

Example of how to adding an interceptor to the Client. Now run the server and also run the client to test things out. You should see a message displaying how many pings your client is responsible for (always 1).

Example of running the server and testing the client

Lets also update our web application so that we can use the metadata attached to the header. Open ui/pingpongapp/src/App.js. The first thing I want to do is add the metadata that the server sends us. This can be done by listening on the ‘metadata’ event. gRPC-web has two events that are related to metadata. The header metadata can be found in the metadata event. The Trailer metadata can be found in the status event. I want to have the same output as the golang client has, so lets add a listener on metadata.

An example of how to listen on the metadata event which carries our ping-counts

Great, lets also update our web application to count the number of requests it makes. First we need to clear something out. gRPC interceptors are intercepting the actual request/responses from leaving. What we will do in our react application wont actually be a interceptor, but will provide the same functionality. Usually what you want to do in a interceptor is grab / add authorization data like a token. If you want to send metadata to the server in grpc-web I strongly suggest doing it in the client method call. The gRPC calls in the javascript client looks like the code snippet below. It’s super easy to wrap that and add for example a authentication-token to the metadata object.

client.functionname(request, metadata,callback)  
// An example what we use  
client.ping(pingRequest, metadata,function(err,response){  
    var pong = response.toObject();  
    setStatus(pong.ok);});

The reason behind me saying this is because it’s A LOT easier to add metadata to your client call than intercepting it and adding the metadata in there.
I’m going to do it the way I think is easier in this tutorial, if you want to do a proper gRPC-web interceptor, there are examples.

Here is an example of a real interceptor, also here for web clients, and here is the discussion about why Unary interceptors are called Stream in gRPC-web.

Feel free to implement the gRPC-web interceptor based on the links above. I’m going to use my own way of doing it, the gist below shows an example where I add a JWT token to the metadata before sending the request.

An example where I add a JWT token to metadata before sending a gRPC request.

In this PingPong app, we only want to count the client pings. So I’ll add a new state that counts them, and increase it as I send the request. This is my full App.js afterward.

It’s time to make sure everything work, so lets rebuild the react application after modifying App.js. And then restart the server, lets then visit the application. I’ll also try using the golang client after visiting the website to make sure the server interceptor works correctly.

This is how the website looks for me.

So, we can check UnaryClientInterceptor and UnaryServerInterceptor off our to-do list.

Before we conclude this part, lets try chaining interceptors. I’ve created a file called logger.go inside the interceptors folder. Its a super simple logger that prints to stdout that a request has been made, and for what gRPC endpoint.

This interceptor will log the paint /main.PingPong/Ping to stdout

We also need to update main.go to use this new logger. We will modify the GenerateTLSApi again. We will add grpc.ChainUnaryInterceptor which takes a variable amount of interceptors and executes them in order.

The new GenerateTLSApi with chaining The final main.go

After running the server and then using the golang client we should see an output in the terminal like this

We can see the logger is working

This is great, we now have an gRPC API with Unary interceptors and are using the metadata. I recommend checking out gRPC-ecosystems. They try to create plug and playable interceptors that anyone can use.

The full code can be found at github

The next part of the series can be found here, in which we discuss streaming data.


About Programming Percy
Programming Percy

Hey, I'm Percy Bolmér. I'm a software engineer, writer and tech enthusiast. I've been professionally building software for 7+ years, and absoluetly love sharing any ideas I come across.

Email : programmingpercy@gmail.com

Website : https://programmingpercy.tech

About Percy Bolmér

Hey, I'm Percy Bolmér. I'm a software engineer, writer and tech enthusiast. I've been professionally building software for 7+ years, and absoluetly love sharing any ideas I come across.

Useful Links