Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
18 Jul 2025 · 5 min read ·Article 40 / 110
Go

40 Chain Interceptor with `grpc_middleware`

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

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:

sh
1go get github.com/grpc-ecosystem/go-grpc-middleware

This package provides helpers for chaining both unary and stream interceptors.


Example: Building a Chain Interceptor

Let’s build three simple interceptors:

  1. Logging Interceptor: Prints the RPC method being called
  2. Auth Interceptor: Checks a simple key in the metadata
  3. Validation Interceptor: Ensures the request isn’t empty

1. Interceptor Example

go
 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:

go
 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

MERMAID
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

ScenarioLoggingAuthValidationHandlerOutput
Valid API key, req okYesYesYesYesSuccess (Hello resp.)
WRONG API keyYesError--Unauthorized error
NIL RequestYesYesError-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

  1. Reusable
    Each interceptor can be used across services without copy-pasting.

  2. Order Control
    The execution order is clear and easy to arrange (similar to the middleware stack in a web framework).

  3. Composable
    Easy to inject new features (audit, tracing, circuit breaker, etc.).

  4. 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:

go
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:

Happy coding! 🚀

Related Articles

💬 Comments