40 Chain Interceptor with `grpc_middleware`
In modern microservices development, gRPC has become a top choice thanks to its high performance, compact message format, and excellent polyglot support. However, just like REST, gRPC-based service architectures also need a middleware mechanism to handle features such as logging, authentication, validation, and even rate limiting in a consistent and reusable way.
Within the Golang ecosystem, the grpc_middleware
library is the go-to solution for composing interceptors into a chain (chain interceptor). This article breaks down the concept of chain interceptors in gRPC using grpc_middleware, complete with code examples, a simple simulation, and a visual explanation using a mermaid diagram.
What Is a gRPC Interceptor?
Before diving into chaining, let’s quickly recap: an interceptor in gRPC is “middleware” — code that runs before and/or after the main handler of an RPC. There are two kinds of interceptors:
- UnaryInterceptor: For unary RPCs (single request-response)
- StreamInterceptor: For streaming RPCs (stream-typed request/response)
Real-world uses of interceptors include:
- Automatic logging for every RPC
- Header/token validation
- Rate limiting
- Monitoring and instrumentation (Prometheus, OpenTracing)
The Challenge: Stacked Middleware
In large-scale applications, a single RPC endpoint may run many pieces of “middleware” in sequence. Writing all of it directly inside the handler becomes an anti-pattern: hard to maintain and hard to reuse.
This is where grpc_middleware becomes a game changer: it provides an elegant chaining mechanism, similar to Express.js in Node.js.
Installation & Initial Setup
Make sure you already have the following module:
1go get github.com/grpc-ecosystem/go-grpc-middlewareThis package provides helpers for chaining both unary and stream interceptors.
Example: Building a Chain Interceptor
Let’s build three simple interceptors:
- Logging Interceptor: Prints the RPC method being called
- Auth Interceptor: Checks a simple key in the metadata
- Validation Interceptor: Ensures the request isn’t empty
1. Interceptor Example
1import (
2 "context"
3 "google.golang.org/grpc"
4 "google.golang.org/grpc/metadata"
5 "fmt"
6 "errors"
7)
8
9// Logging Interceptor
10func LoggingInterceptor(
11 ctx context.Context,
12 req interface{},
13 info *grpc.UnaryServerInfo,
14 handler grpc.UnaryHandler,
15) (interface{}, error) {
16 fmt.Println("[LOG] gRPC called:", info.FullMethod)
17 return handler(ctx, req)
18}
19
20// Auth Interceptor
21func AuthInterceptor(
22 ctx context.Context,
23 req interface{},
24 info *grpc.UnaryServerInfo,
25 handler grpc.UnaryHandler,
26) (interface{}, error) {
27 md, ok := metadata.FromIncomingContext(ctx)
28 if !ok || len(md["x-api-key"]) == 0 || md["x-api-key"][0] != "secret123" {
29 return nil, errors.New("unauthorized")
30 }
31 return handler(ctx, req)
32}
33
34// Validation Interceptor (dummy)
35func ValidateInterceptor(
36 ctx context.Context,
37 req interface{},
38 info *grpc.UnaryServerInfo,
39 handler grpc.UnaryHandler,
40) (interface{}, error) {
41 if req == nil {
42 return nil, errors.New("empty request")
43 }
44 return handler(ctx, req)
45}2. Chaining with grpc_middleware
With grpc_middleware, we compose the interceptors above into a single chain:
1import "github.com/grpc-ecosystem/go-grpc-middleware"
2
3// ... interceptor definitions ...
4
5server := grpc.NewServer(
6 grpc_middleware.WithUnaryServerChain(
7 LoggingInterceptor,
8 AuthInterceptor,
9 ValidateInterceptor,
10 ),
11)Flow Diagram: Unary Chain Interceptor
flowchart LR
A[gRPC Client] --> |RPC call| B[LoggingInterceptor]
B --> |pass| C[AuthInterceptor]
C --> |pass| D[ValidateInterceptor]
D --> |pass| E[Actual Handler]
E --> |response| D
D --> |return| C
C --> |return| B
B --> |return| A
Explanation:
- Each interceptor has access to the next handler, forming a chain.
- If an error occurs at any point (for example, Auth fails), the process is “cut off” and the error is returned to the client.
Simulation: The Order of Interceptor Execution
Suppose we have a service method SayHello that accepts an empty request along with the x-api-key metadata.
1. Valid Client Request
- Metadata:
x-api-key: secret123 - Request:
{}(not nil)
Execution:
- LoggingInterceptor: records the log
- AuthInterceptor: API key is correct
- ValidateInterceptor: request is valid
- Handler: executed
2. Client Request with WRONG API Key
- Metadata: none
- Request:
{}
Execution:
- LoggingInterceptor: records the log
- AuthInterceptor: API key invalid → STOP, returns an unauthenticated error
3. NIL Client Request
- Metadata:
x-api-key: secret123 - Request:
nil
Execution:
- LoggingInterceptor: records the log
- AuthInterceptor: API key valid
- ValidateInterceptor: request is nil → STOP, returns an error
Simulation Output
| Scenario | Logging | Auth | Validation | Handler | Output |
|---|---|---|---|---|---|
| Valid API key, req ok | Yes | Yes | Yes | Yes | Success (Hello resp.) |
| WRONG API key | Yes | Error | - | - | Unauthorized error |
| NIL Request | Yes | Yes | Error | - | Empty request error |
Notes:
- The flow stops at the column where the error occurs, and the handler is not called.
- Middleware can either cut off or pass through execution.
Advantages of Chain Interceptors
Reusable
Each interceptor can be used across services without copy-pasting.Order Control
The execution order is clear and easy to arrange (similar to the middleware stack in a web framework).Composable
Easy to inject new features (audit, tracing, circuit breaker, etc.).Separation of Concern
The RPC handler stays focused on business logic.
Chaining Stream Interceptors
In addition to unary, grpc_middleware supports streams with an identical API:
1server := grpc.NewServer(
2 grpc_middleware.WithStreamServerChain(
3 MyStreamLoggingInterceptor,
4 AuthStreamInterceptor,
5 ),
6)Stream handlers follow a different signature, but the chaining principle stays the same.
Best Practice Tips
- Arrange from universal to specific (for example: logging → auth → custom)
- Interceptors should be idempotent and must not modify the request arbitrarily
- Keep error handling explicit, and never “panic” inside an interceptor
- Use the context to pass values between interceptors when needed
Conclusion
With chain interceptors from grpc_middleware, gRPC applications in Golang become modular, scalable, and maintainable. Like a series of filters, we can control the flow of a request to the handler — whether for security, logging, or metric observation.
Invest in your interceptor design from the very beginning—because the more your services grow, the more important a well-structured middleware layer becomes. Give it a try and scale up your gRPC professionally!
References:
- https://github.com/grpc-ecosystem/go-grpc-middleware
- https://grpc.io/docs/guides/auth/
- https://dev.to/mohammadne/golang-grpc-middleware-1c8i
Happy coding! 🚀