Generalising gRPC Interceptors

Kieran Warwick
Thirdfort
Published in
4 min readOct 11, 2022

When I was creating gRPC server interceptors for zerolog and Sentry, I was repeating code for both the Stream and Unary interceptors. There was always some action before the handling of the call, and an action after.

The logical approach would be to create the functions BeforeHandler and AfterHandler. However, that alone isn’t very fun or inspiring, so I wanted to take it a step further: I wanted to generalise both interceptors together.

Creating a General Interceptor

Define the interface

I first thought about what functions an interceptor has, and so devised two methods: BeforeHandler and AfterHandler. The natural conclusion was to construct an Interceptor interface to handle this:

type Interceptor interface{
BeforeHandler()
AfterHandler()
}

This means that for any interceptor that implements this interface, it will perform code before and after the gRPC call. The functions can also be empty, but must always be defined in order to implement our interface.

Implement the interface

To use our general interceptor, we have to implement the interface. Here is a basic interceptor that will print out to the log ‘before’ and ‘after’:

// Struct can contain values such as configs or shared variables
type FooInter struct {}
func (FooInter) BeforeHandler() {
fmt.Println("before")
}
func (FooInter) AfterHandler() {
fmt.Println("after")
}

Now that we have defined our own general interceptor, how do we actually use it?

Using the General Interceptor

Convert back into Unary / Stream Interceptors

The challenge now is how to convert the general interceptor into a gRPC interceptor so that it may be used by the gRPC package.

To perform this, we’ll use the power of functional programming to do our conversion by using Closures:

These functions will take a general interceptor, and convert it either into a StreamServerInterface or UnaryServerInterceptor function. Now that we have a way to get the gRPC interceptor back, how can we effectively pass it back into the server?

Pass the converted interceptor into the gRPC server

You can try our this code by passing in the interceptor we made earlier. You’ll need to convert it into a gRPC interceptor and pass it into the server on creation.

grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(
UnaryInterceptor(FooInter{})
),
grpc.StreamInterceptor(
StreamInterceptor(FooInter{})
)
)

Now for every call made to the server, it will print out a log befere and after each gRPC call. Perhaps this would be useful for timing gRPC calls 😉.

Extending the Interceptor with Context and Errors

The usefulness of this is limited because we cannot modify the context that gets sent to the handlers, nor do we do anything with the error returned by the handlers. Let’s redefine our interface to include those.

type Interceptor interface{
BeforeHandler(ctx context.Context) context.Context
AfterHandler(ctx context.Context, err error)
}

We’ll also need new interceptor member functions, in which we’ll add a value to the context

func (FooInter) BeforeHandler(ctx context.Context) context.Context {
fmt.Println("before")
ctx = context.WithValue(ctx, key{}, "value")
return ctx
}
func (FooInter) AfterHandler(ctx context.Context, err error) {
fmt.Println("after")
}

This would work for unary interceptors. However, stream interceptors are different: the context is a private variable withinServerStream. To expose this, we need to wrap the ServerStream interface into a local struct which exposes a modifiable context like so:

type WrappedServerStream struct {
grpc.ServerStream
Ctx context.Context
}
// Override to return wrapper context rather than private context
func (w WrappedServerStream) Context() context.Context {
return w.Ctx
}

Lastly, we update our interceptor generators to include these new functions, and set up the modifiable context for the stream interceptor by wrapping the ServerStream

The context passed to the functions within the handlers of our server will now include our custom value. This can be useful if e.g. you want to store a Sentry hub, or a logger on the context.

A full package can now be imported from GitHub! 🚀

Join Thirdfort

At Thirdfort, we’re on a mission to make moving house easier for everyone involved. We use tech, design, and data to make working with clients secure and friction-free for lawyers, property, and finance professionals. Since launching in 2017, we’ve grown from a single London office to an international team with sites in Manchester and Sri Lanka. We’re backed by leading investors like Alex Chesterman (Founder of Zoopla and Cazoo) and HM Land Registry.

Want to help shape Thirdfort’s story in 2022? We’d love to hear from you. Find your next role on our Careers Hub or reach out to careers@thirdfort.com.

--

--