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.
CLIENT INTERCEPTORS · wrap the OUTBOUND callyour codecalls greet()retrymetricstraceauthhandlergreet("Ada")request → inward to handlerCHAINauthontraceonmetricsonretryoneach layer wraps the outbound call — same chain, every method
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 leaves
3 ctx = attach(ctx) # e.g. deadline header, token
4 resp = invoker(ctx, method, req) # hand to next layer
5 # runs on the way OUT, on the reply
6 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 handler
3 if not allowed(ctx): # auth lives here, not in the method
4 return Unauthenticated
5 resp = handler(ctx, req) # the real greet("Ada")
6 # runs after the handler, before the reply leaves
7 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 core
3 for layer in reversed(layers):
4 if not layer.enabled:
5 continue # skip: call passes straight through
6 call = wrap(layer, call) # outer now wraps inner
7 return call # the chain, outermost-first
Not sure what to ask? Tap a question — the staff engineer answers in the chat panel.