11 - Interceptors
📋 Jump to TakeawaysInterceptors are gRPC's version of middleware. They sit between the client/server and the RPC handler, letting you add logging, authentication, metrics, or any cross-cutting concern without touching your business logic.
Unary Server Interceptor
A unary server interceptor wraps every unary RPC call. Add "time" to your imports in main.go:
func loggingInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
start := time.Now()
// Call the actual handler
resp, err := handler(ctx, req)
log.Printf("method=%s duration=%s err=%v",
info.FullMethod, time.Since(start), err)
return resp, err
}The signature looks intimidating but it's simple: you get the context, the request, info about which method was called, and the next handler in the chain. Call handler(ctx, req) to proceed, or return early to short-circuit.
Register it when creating the server:
srv := grpc.NewServer(
grpc.UnaryInterceptor(loggingInterceptor),
)Stream Server Interceptor
Streaming RPCs have their own interceptor type:
func streamLoggingInterceptor(
srv interface{},
ss grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) error {
start := time.Now()
err := handler(srv, ss)
log.Printf("stream method=%s duration=%s err=%v",
info.FullMethod, time.Since(start), err)
return err
}Register it separately:
srv := grpc.NewServer(
grpc.UnaryInterceptor(loggingInterceptor),
grpc.StreamInterceptor(streamLoggingInterceptor),
)Chaining Multiple Interceptors
You often need more than one interceptor. gRPC supports chaining:
srv := grpc.NewServer(
grpc.ChainUnaryInterceptor(
loggingInterceptor,
authInterceptor,
recoveryInterceptor,
),
grpc.ChainStreamInterceptor(
streamLoggingInterceptor,
streamAuthInterceptor,
),
)Interceptors run in order. The first one in the list runs first, calls the next, and so on. Like HTTP middleware, the order matters.
Authentication Interceptor
Here's a practical example. Check for an API key in the metadata (gRPC's equivalent of HTTP headers). Metadata is a set of key-value string pairs sent alongside every RPC call. We'll cover it in depth in lesson 13; for now, just know it's how you pass auth tokens, request IDs, and other context.
func authInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// Skip auth for health checks
if info.FullMethod == "/grpc.health.v1.Health/Check" {
return handler(ctx, req)
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
keys := md.Get("authorization")
if len(keys) == 0 || keys[0] != "Bearer my-secret-key" {
return nil, status.Error(codes.Unauthenticated, "invalid api key")
}
return handler(ctx, req)
}The interceptor extracts metadata from the context, checks the authorization value, and either proceeds or returns an error. The client never reaches your handler if auth fails.
Add these imports to main.go:
"google.golang.org/grpc/metadata"You should already have codes and status from earlier lessons.
Recovery Interceptor
Catch panics so one bad request doesn't crash the whole server:
func recoveryInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{}, err error) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic recovered: %v", r)
err = status.Errorf(codes.Internal, "internal error")
}
}()
return handler(ctx, req)
}Client Interceptors
Clients have interceptors too. Useful for adding auth headers, logging outgoing calls, or retrying failed requests:
func clientLoggingInterceptor(
ctx context.Context,
method string,
req, reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
log.Printf("client call=%s duration=%s err=%v",
method, time.Since(start), err)
return err
}
// Register on the client connection
conn, err := grpc.NewClient("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(clientLoggingInterceptor),
)Client Auth Interceptor
Automatically attach credentials to every outgoing call:
func clientAuthInterceptor(
ctx context.Context,
method string,
req, reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer my-secret-key")
return invoker(ctx, method, req, reply, cc, opts...)
}This pairs with the server auth interceptor from earlier. The client adds the key, the server checks it.
Key Takeaways
- Interceptors are gRPC middleware for cross-cutting concerns
- Unary and stream interceptors have different signatures
- Use
ChainUnaryInterceptorandChainStreamInterceptorfor multiple interceptors - Order matters: first interceptor runs first
- Common interceptors: logging, auth, recovery, metrics
- Client interceptors work the same way, useful for adding auth headers automatically