Lesson 11 · Interceptors, middleware & auth

Interceptors: unary & stream

gRPC's middleware — the code that wraps every RPC.

Your win: explain what a gRPC interceptor is, read the unary and stream signatures, and recognise the enrich-ctx-then-call-handler pattern that runs on every request in your services.

An interceptor wraps every RPC

An interceptor runs code before and after a handler, for every call — it's gRPC's equivalent of HTTP middleware.1 It's where cross-cutting concerns live — auth, logging, tracing, metrics, panic-recovery — so your handlers stay pure business logic.

Two kinds, two sides Unary interceptors wrap single request/response calls; stream interceptors wrap streaming calls (Part 2). And each exists on both the server and the client side. Four combinations, one idea.

The unary server signature

func(ctx context.Context, req interface{},
     info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error)

You get the ctx and req, plus info (which method is being called) and handler (the next thing in line — the real handler, or the next interceptor). The pattern: do work, optionally enrich ctx, then return handler(ctx, req) — and you can inspect the result after.

internal/usermgmt/pkg/interceptors/auth.go:79
func (a *Auth) UnaryServerInterceptor(ctx, req, info, handler) (interface{}, error) {
    // …verify JWT from metadata (Lesson 10)…
    ctx = glInterceptors.ContextWithJWTClaims(ctx, claims)  // enrich ctx
    return handler(ctx, req)                              // call down the chain
}

The stream server signature

func(srv interface{}, ss grpc.ServerStream,
     info *grpc.StreamServerInfo, handler grpc.StreamHandler) error

Same idea, but it wraps the stream (ss) instead of a request/response — used by the recovery and logging interceptors on tom's streaming RPCs.

Anchor Our auth is exactly this pattern: read the token from metadata → verify → inject claims → handler(ctx, req). Because it's an interceptor, every RPC in the service is authenticated without a single line in any handler. That's the payoff — and it's why the handlers you write are pure business logic.
Mental model An interceptor is a decorator around the handler function — Go's function values (your Go course) applied to RPCs. handler is just the next func; you choose whether/when to call it, and what to do with its result.
Read this next

gRPC — Interceptors guide + grpc-go interceptor example

The official concept (server vs client, unary vs stream) and a runnable Go example of each signature.

grpc.io/docs/guides/interceptors
grpc-go — interceptor example

Check yourself (from memory)

Q1. A gRPC interceptor is best described as…

Middleware — cross-cutting code before/after the handler, on every call. Keeps handlers pure.

Q2. A unary server interceptor ends by…

It calls the next thing in line (handler) — after enriching ctx — and can inspect the result afterward.

Q3. gRPC interceptors come in which two kinds?

Unary (single call) and stream (streaming call) — each on both server and client sides.
What is a gRPC interceptor, its unary signature, and the core pattern?
recall, then click to reveal
Middleware that wraps every RPC to run cross-cutting logic (auth/log/trace/recovery) before/after the handler — gRPC's version of HTTP middleware. Two kinds (unary, stream), two sides (server, client). Unary server signature: func(ctx, req, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp, err). Core pattern: do work / enrich ctx, then return handler(ctx, req) — with optional before-and-after logic. Keeps handlers pure business logic.
Want to write a tiny logging interceptor yourself, or see the client-side signature? Ask me.

1. gRPC — Interceptors.