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

32 Writing Your Own Unary Interceptor

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

When building gRPC services in the Go ecosystem, we very often run into the need to plug in middleware functions: whether it’s logging requests, authentication validation, auditing, monitoring, or handling errors in a consistent pattern. In the REST world, this kind of thing is commonly done through middleware like in Gin or Echo. In gRPC, the equivalent concept is called an interceptor.

In this article, I’ll take a practical look at Unary Interceptors in gRPC: from understanding the concept and the architecture, to implementation and production tips. We’ll also build a custom unary interceptor from scratch, complete with code examples, a simulation, and flow diagrams.


What Is a Unary Interceptor?

An interceptor in gRPC is a function that “intercepts” the request flow from client to server before the method handler is executed. There are two main types of interceptors in gRPC:

  1. Unary interceptor – For unary methods (one request, one response).
  2. Stream interceptor – For streaming methods (client, server, or bidirectional).

In this article we’ll focus on the unary interceptor.

Why Use a Unary Interceptor?

Imagine you want to make sure all of your API endpoints are audited, or you want every request to be logged, or you want JWT validation to run before reaching your core logic. If you write that code over and over in every handler, the code becomes non-reusable and bug-prone. This is exactly where a unary interceptor becomes the solution.

Simply put, with a unary interceptor we have a single place where additional logic can “intercept” every request, either before or after the main handler runs.


Unary Interceptor Flow Diagram

Let’s visualize the process with a flowchart using Mermaid :

MERMAID
flowchart TD
    A(Client gRPC Request) --> B[Unary Interceptor]
    B --> C{Pre-Processing}
    C -->|Pass| D["Handler (Your Actual gRPC Service)"]
    D --> E{Post-Processing}
    E --> F[gRPC Response]
    C -->|Blocked/Error| G[Return Error to Client]

Anatomy of a Unary Interceptor in Go

Let’s look at the signature of a unary interceptor in Go:

go
1func(
2    ctx context.Context,
3    req interface{},
4    info *grpc.UnaryServerInfo,
5    handler grpc.UnaryHandler,
6) (resp interface{}, err error)

Parameter explanation:

  • ctx: The request context, which can be used to propagate custom values (e.g., a user claim).
  • req: The request payload.
  • info: Contains meta-information (the fully qualified method name, etc.).
  • handler: The function that executes the actual handler.

Typically, we run some pre-processing, then call handler(ctx, req), and add post-processing if needed.


Example Unary Interceptor Implementation: Logging

To get started, let’s build a simple interceptor that logs every incoming request, along with its execution time.

go
 1import (
 2    "context"
 3    "log"
 4    "time"
 5    "google.golang.org/grpc"
 6)
 7
 8func LoggingUnaryInterceptor(
 9    ctx context.Context,
10    req interface{},
11    info *grpc.UnaryServerInfo,
12    handler grpc.UnaryHandler,
13) (interface{}, error) {
14    start := time.Now()
15    log.Printf("gRPC method: %s - req: %+v", info.FullMethod, req)
16
17    resp, err := handler(ctx, req)
18
19    log.Printf(
20        "gRPC method: %s - resp: %+v - err: %v - duration: %s",
21        info.FullMethod, resp, err, time.Since(start),
22    )
23    return resp, err
24}

How to Register the Interceptor on the Server

Once the interceptor is ready, we register it when creating the gRPC server:

go
1server := grpc.NewServer(
2    grpc.UnaryInterceptor(LoggingUnaryInterceptor),
3)

You can also chain several unary interceptors at once using grpc_middleware :

go
1import "github.com/grpc-ecosystem/go-grpc-middleware"
2
3server := grpc.NewServer(
4    grpc_middleware.WithUnaryServerChain(
5        AuthUnaryInterceptor,
6        LoggingUnaryInterceptor,
7        MetricsUnaryInterceptor,
8    ),
9)

Simulation: Comparing Before & After the Interceptor

Here is a simulation of the request flow with and without a unary interceptor:

Without InterceptorWith Interceptor
1gRPC Request ArrivesgRPC Request Arrives
2Handler runsPre-processing by the Interceptor
3Handler runsHandler runs
4Response sent to clientPost-processing by the Interceptor
5Response sent to client


Case Study: Auth Interceptor with JWT

Let’s build an interceptor that validates a JWT token from the metadata before the handler is executed. If the token is invalid, return an error immediately; if it’s valid, propagate the user claim into the context.

Implementation:

go
 1import (
 2    "context"
 3    "errors"
 4    "strings"
 5    "google.golang.org/grpc"
 6    "google.golang.org/grpc/metadata"
 7)
 8
 9func AuthUnaryInterceptor(
10    ctx context.Context,
11    req interface{},
12    info *grpc.UnaryServerInfo,
13    handler grpc.UnaryHandler,
14) (interface{}, error) {
15    md, ok := metadata.FromIncomingContext(ctx)
16    if !ok {
17        return nil, errors.New("missing metadata")
18    }
19
20    // Get the Authorization header
21    tokens := md["authorization"]
22    if len(tokens) == 0 {
23        return nil, errors.New("missing authorization token")
24    }
25    
26    token := strings.TrimPrefix(tokens[0], "Bearer ")
27
28    // Validate the token here (pseudocode)
29    claims, err := ValidateJWT(token)
30    if err != nil {
31        return nil, errors.New("invalid token")
32    }
33
34    // Propagate the claim into the context
35    newCtx := context.WithValue(ctx, "user", claims)
36
37    // Forward to the main handler with the new context
38    return handler(newCtx, req)
39}

The Handler Can Access the User Claim

go
1user := ctx.Value("user").(UserClaims)
2// the user can now be accessed inside the handler

Best Practices for Writing Unary Interceptors

  1. Keep the logic simple: Make sure the work inside the interceptor isn’t too heavy, so latency doesn’t balloon.
  2. Handle errors explicitly: When an error occurs, make sure to return an informative error.
  3. Propagate context properly: Insert anything into the context clearly so the handler can access it.
  4. Separate pre and post processing concerns: Keep the logic before and after the handler runs distinct.
  5. Chain interceptors in the right order: For example: Auth → Logging → Metrics.

Understanding Interceptor Chaining and Composition

One of the advantages of interceptors is that they’re easy to compose. With chaining, the order of execution can be illustrated as follows:

MERMAID
sequenceDiagram
    participant Client
    participant Interceptor1
    participant Interceptor2
    participant Handler
    
    Client->>Interceptor1: Request
    Interceptor1->>Interceptor2: Pre-processing
    Interceptor2->>Handler: Pre-processing
    Handler-->>Interceptor2: Response
    Interceptor2-->>Interceptor1: Post-processing
    Interceptor1-->>Client: Return Response

Conclusion

A unary interceptor is an essential feature for building gRPC services that are robust, concise, and maintainable. By understanding its architecture and implementation patterns, we can easily add functionality such as logging, authentication, rate-limiting, and observability without duplicating code in every handler.

With the examples and case study above, you are now ready to write your own unary interceptor to fit your application’s needs! And remember, design your interceptors with good efficiency and clarity to produce services and products that are reliable and scalable.


References:

Happy refactoring!

Related Articles

💬 Comments