31 What Is an Interceptor in gRPC?
gRPC has become the backbone for migrating many monolithic systems toward microservices architectures. With its high performance, multi-language support, and ease of integration, it’s no surprise that gRPC is now widely used as the transport protocol between modern services. One powerful yet often underrated feature of gRPC is the Interceptor.
In this article, we’ll cover everything you need to know:
- What an Interceptor is in gRPC
- How it works under the hood
- Usage examples (code)
- A simulated case study
- An execution flow diagram
Let’s get started!
What Is an Interceptor in gRPC?
Put simply, a gRPC Interceptor is much like the middleware found in web frameworks such as Express.js, Spring, or Gin. An Interceptor lets us “inject additional logic” in the form of cross-cutting concerns into the lifecycle of every RPC that gets processed.
Common cross-cutting concerns that are typically implemented through an Interceptor:
- Logging
- Authentication & Authorization
- Metrics & Tracing (Distributed Tracing)
- Exception Handling & Retry Logic
- Rate Limiting
With an Interceptor, the code that handles the concerns above can be separated from the service’s core business logic, making the codebase cleaner, more maintainable, and more reusable.
Core Concepts of Interceptors in gRPC
Fundamentally, an Interceptor provides a hook into the execution of an RPC on both the client and the server side.
- A Client Interceptor runs logic before the RPC is sent to the server, or when the response is received.
- A Server Interceptor captures the request before it reaches the handler, or before the response is returned to the client.
Server-side Interceptors come in two forms:
- Unary Server Interceptor (for unary RPCs)
- Stream Server Interceptor (for streaming RPCs)
On the client side, the concept is the same.
Interceptor Flow Diagram
Let’s simulate the execution flow of a request passing through several server-side Interceptors using the mermaid diagram below:
flowchart LR
Client(Request) --> |RPC Call| Interceptor1
Interceptor1 --> Interceptor2
Interceptor2 --> InterceptorN
InterceptorN --> ServiceHandler
ServiceHandler --> InterceptorN
InterceptorN --> Interceptor2
Interceptor2 --> Interceptor1
Interceptor1 --> |Response| Client
- Each Interceptor can run logic before forwarding the request to the next Interceptor or to the Service Handler (the handler’s core logic).
- After the handler finishes, the Interceptor can also modify the response.
Implementing Interceptors: An Example with gRPC-Go
Let’s get hands-on using the Go language with gRPC-Go.
1. A Simple Unary Server Interceptor (Logging)
1import (
2 "context"
3 "google.golang.org/grpc"
4 "log"
5 "time"
6)
7
8// Unary Interceptor Function Type
9func LoggingInterceptor(
10 ctx context.Context,
11 req interface{},
12 info *grpc.UnaryServerInfo,
13 handler grpc.UnaryHandler,
14) (resp interface{}, err error) {
15 start := time.Now()
16 log.Printf("gRPC method: %s; at: %v", info.FullMethod, start)
17 resp, err = handler(ctx, req) // call the next interceptor / actual handler
18 log.Printf("gRPC method: %s; finished in %v, err = %v", info.FullMethod, time.Since(start), err)
19 return resp, err
20}1grpcServer := grpc.NewServer(
2 grpc.UnaryInterceptor(LoggingInterceptor),
3)2. Chaining Multiple Interceptors
gRPC v1.38+ supports chaining multiple interceptors using a helper:
1grpc.ChainUnaryInterceptor(
2 LoggingInterceptor,
3 AuthInterceptor,
4 TracingInterceptor,
5)3. Client Interceptor
Similar to the server side:
1func ClientLoggingInterceptor(
2 ctx context.Context,
3 method string,
4 req, reply interface{},
5 cc *grpc.ClientConn,
6 invoker grpc.UnaryInvoker,
7 opts ...grpc.CallOption,
8) error {
9 log.Printf("Client calling: %s", method)
10 err := invoker(ctx, method, req, reply, cc, opts...)
11 log.Printf("Client finished: %s, err: %v", method, err)
12 return err
13}
14
15conn, err := grpc.Dial(
16 "localhost:50051",
17 grpc.WithUnaryInterceptor(ClientLoggingInterceptor),
18)Case Study: Auth & Logging Interceptors
Let’s simulate implementing two Interceptors on a gRPC server: one for Authentication and another for Logging.
Auth Interceptor
1func AuthInterceptor(
2 ctx context.Context,
3 req interface{},
4 info *grpc.UnaryServerInfo,
5 handler grpc.UnaryHandler,
6) (interface{}, error) {
7 md, ok := metadata.FromIncomingContext(ctx)
8 if !ok || len(md["authorization"]) == 0 {
9 return nil, status.Errorf(codes.Unauthenticated, "token missing")
10 }
11 token := md["authorization"][0]
12 if token != "valid-token" {
13 return nil, status.Errorf(codes.Unauthenticated, "invalid token")
14 }
15 // continue to the next handler
16 return handler(ctx, req)
17}Combining Interceptors
1grpcServer := grpc.NewServer(
2 grpc.ChainUnaryInterceptor(
3 LoggingInterceptor,
4 AuthInterceptor,
5 ),
6)Simulation Table
| Request Flow | Logging Interceptor | Auth Interceptor | Handler |
|---|---|---|---|
| No Authentication Header | Logs request | Fails, Unauthenticated | Does not run |
| Wrong Token | Logs request | Fails, Unauthenticated | Does not run |
| Valid Token | Logs request | Continues | Handler runs |
| Error in Handler | Logs request | Continues | Logs Error |
Advantages of Using Interceptors
- Separation of Concern: The code for logging/auth/rate limiting doesn’t get mixed into the core handler.
- Reusability: Interceptors can be reused across gRPC services.
- Stackable/Composable: You can build an INTERCEPTOR pipeline tailored to your needs.
Best Practices & Tips
- Don’t carry local state in an Interceptor. Interceptors are invoked per-RPC, so prefer using the context and avoid any race conditions.
- Handle errors properly. Return errors with a gRPC status so they’re easy for the client to diagnose.
- Be careful when modifying the context. When needed, use context.WithValue and then pass it along.
- Streaming Interceptors differ from Unary ones. For streaming (server/client), the interceptor interface is different and requires extra care.
When Should You Use an Interceptor?
- When you need to apply validation, authentication, metrics, and so on uniformly across ALL methods.
- To keep your service’s business logic clean and free of other concerns.
- For observability purposes: consistent logging/tracing.
Conclusion
The Interceptor is one of those gRPC features that gives engineers the power to keep a codebase clean, scalable, and observable. In modern microservices, building an ecosystem of cross-cutting concerns without Interceptors is like laying bricks without mortar: hard to read and prone to falling apart.
Is your gRPC service already taking advantage of the power of Interceptors? If not, now is the perfect time to refactor and enjoy the maintainability it brings!
If you have any questions or want to share your experience, feel free to leave a comment!
References: