Build a gRPC-style RPC framework (14 scenes)
Scene 08 · Interceptors: the middleware onion
An interceptor wraps every call as one composable layer, so auth, metrics, tracing, and the retry policy are written once around the handler instead of per method.
Previously
We just built a retry policy — but it can't live inside each method any more than deadline-checking or auth can; all of these wrap every call, so we need the one structural pattern that lets them compose as layers.
Scene 08
Interceptors: the middleware onion
Diagram
Concentric rings wrap a central handler core that runs greet("Ada"). The request enters from the entry edge (your code on the client, the transport on the server) and travels INWARD, crossing each installed ring in turn; the response travels back OUTWARD through the same rings in reverse. Each ring is ONE interceptor — auth, trace, metrics, retry are example layers; the ordered stack of them is the middleware chain. A ring drawn dashed/ghosted is switched off: the call passes straight through it. The side toggle decides whether this onion wraps the client's outbound call or the server's inbound call.
Every call needs the same wrapping work: prove the caller is allowed (auth), open a span so it's traceable (trace), count and time it (metrics), and apply the retry budget you built last scene (retry). Copy-pasting that into all 40 methods of a service is madness. The fix is an *interceptor*: a single piece of code that wraps a call, runs its bit before handing the call along, and runs its bit again on the way back. Stack several of them and you get a *middleware chain* — an onion of layers around the real method. Watch greet("Ada") enter from outside and pass INWARD through auth → trace → metrics → retry to reach the handler, then watch the reply travel back OUTWARD through the same layers in reverse. Same layers, opposite order.
Implementation
Client.intercept
one client-side interceptor wraps the OUTBOUND call
1def intercept(ctx, method, req, invoker):2 # runs on the way IN, before the call leaves3 ctx = attach(ctx) # e.g. deadline header, token4 resp = invoker(ctx, method, req) # hand to next layer5 # runs on the way OUT, on the reply6 observe(resp)7 return resp
Server.intercept
one server-side interceptor wraps the INBOUND call
1def intercept(ctx, req, handler):2 # runs as the call ARRIVES, before the handler3 if not allowed(ctx): # auth lives here, not in the method4 return Unauthenticated5 resp = handler(ctx, req) # the real greet("Ada")6 # runs after the handler, before the reply leaves7 record(resp)8 return resp
Chain.compose
fold the enabled layers into one onion around the handler
1def compose(layers, handler):2 call = handler # the core3 for layer in reversed(layers):4 if not layer.enabled:5 continue # skip: call passes straight through6 call = wrap(layer, call) # outer now wraps inner7 return call # the chain, outermost-first
Not sure what to ask? Tap a question — the staff engineer answers in the chat panel.