19 Adding a Unary Interceptor on the Server
When building a gRPC-based backend application, one of the powerful concepts you absolutely need to master is the interceptor. On the server side in particular, the unary interceptor plays a crucial role—covering everything from logging and authentication to metrics. In this article, we’ll thoroughly explore how to add a unary interceptor to a gRPC server, complete with real-world scenarios, code examples, and even a flow diagram so you can easily put it into practice.
What Is a gRPC Interceptor?
By analogy, an interceptor in gRPC is similar to “middleware” in ExpressJS or the HTTP pipeline in ASP.NET Core. An interceptor in gRPC lets us “short-circuit” or “extend” the process before or after an RPC executes in a generic way.
Why Do You Need a Unary Interceptor?
A unary interceptor is a great fit for several use cases:
- Logging: Record every request, response, and error.
- Authentication & Authorization: Validate a token/JWT before the handler executes.
- Monitoring: Send metrics to Prometheus, DataDog, and so on.
- Request/Response Transformation: Modify data before or after the handler.
This way, your business code doesn’t get mixed up with other concerns—keeping it clean, DRY, and testable.
Unary Interceptor Flow Diagram
To clarify how data flows when a unary interceptor runs, take a look at the diagram below:
flowchart LR
A(Client) --> B(gRPC Server)
B --> C(Interceptor)
C --> D(Service Handler)
D --> C
C --> B
B --> A
Example Scenario: A Logging Interceptor on the Server
Suppose our application has a simple service: a HelloService with a SayHello method.
1. Proto Definition (hello.proto)
1syntax = "proto3";
2
3service HelloService {
4 rpc SayHello (HelloRequest) returns (HelloReply) {}
5}
6
7message HelloRequest {
8 string name = 1;
9}
10
11message HelloReply {
12 string message = 1;
13}Next step: generate the stub code for the server.
2. gRPC Server Implementation + Unary Interceptor
We’ll assume you’re using Go and the google.golang.org/grpc package, since its documentation and community are very extensive.
a. Creating the Logging Interceptor
1import (
2 "context"
3 "log"
4
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) (resp interface{}, err error) {
14 log.Printf("Received request for method: %s", info.FullMethod)
15 resp, err = handler(ctx, req) // Run the actual handler
16 if err != nil {
17 log.Printf("Error executing %s: %v", info.FullMethod, err)
18 } else {
19 log.Printf("Response: %+v", resp)
20 }
21 return resp, err
22}b. Registering the Interceptor on the Server
1import (
2 "google.golang.org/grpc"
3 pb "path/to/generated/hello" // adjust the path
4)
5
6func main() {
7 server := grpc.NewServer(
8 grpc.UnaryInterceptor(LoggingUnaryInterceptor), // Attach the interceptor here
9 )
10 pb.RegisterHelloServiceServer(server, &HelloService{})
11 // ... bind the listener, etc.
12}c. Implementing the Main Handler
1type HelloService struct {
2 pb.UnimplementedHelloServiceServer
3}
4
5func (s *HelloService) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
6 return &pb.HelloReply{Message: "Hello, " + req.Name + "!"}, nil
7}Simulation: Logging Output
The table below shows a simulation of the interceptor’s output when a client sends a request:
| Method | Request | Response | Error |
|---|---|---|---|
| /HelloService/SayHello | { “name”: “Kiki” } | { “message”: “Hello, Kiki!” } | - |
Log output (stdout):
1Received request for method: /HelloService/SayHello
2Response: {Message:"Hello, Kiki!"}Combining Multiple Interceptors
What if we want to stack several interceptors (for example: authentication + logging)? In Go, use grpc.ChainUnaryInterceptor:
1server := grpc.NewServer(
2 grpc.ChainUnaryInterceptor(
3 AuthenticationInterceptor,
4 LoggingUnaryInterceptor,
5 // and so on.
6 ),
7)The chain runs in order; the final handler always stays at the very end.
Another Case Study: Authentication Interceptor
As a complement, here’s a simple interceptor for validating the authorization header.
1func AuthUnaryInterceptor(
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.Error(codes.Unauthenticated, "missing auth token")
10 }
11 token := md["authorization"][0]
12 if token != "mysecrettoken" {
13 return nil, status.Error(codes.Unauthenticated, "invalid auth token")
14 }
15 return handler(ctx, req)
16}If the client doesn’t include the authorization header, the request is automatically rejected without the main handler ever being executed.
Best Practice: When Should You Use an Interceptor?
| Middleware Need | Good Fit for an Interceptor? |
|---|---|
| Access Logging | ✅ |
| Rate Limiting | ✅ |
| Caching a Single Endpoint | ✗ (better handled inline in the handler) |
| Global Authentication | ✅ |
| Service-level Validation | ✅ |
| Simple Data Enrichment | ✗ (prefer the handler) |
Conclusion
By adding a unary interceptor to your gRPC server, you can apply elegant, powerful, enterprise-grade middleware while keeping your code clean. The setup is straightforward, yet the impact is massive for the maintainability and scalability of your backend microservices.
If you come across an interesting use case, feel free to experiment by adding your own interceptors—and don’t be afraid to combine several at once. This is one of the modern backend service best practices that today’s engineers absolutely need to master!
Further Reading:
Hope this article helps! Don’t hesitate to ask in the comments or share your experience using interceptors in production.