Interceptors: the middleware onion
An interceptor wraps every call as one composable layer, so cross-cutting concerns — auth, tracing, metrics, the retry policy — are written once around the handler instead of copy-pasted into every method; client-side interceptors wrap the outbound call, server-side ones wrap the inbound call, and together they form a chain a request passes inward through and the response passes back outward.
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
- Watch
- Try it
- Predict
- Capture
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.
Highlighted lines are the ones running in the diagram right now.
def intercept(ctx, method, req, invoker):# runs on the way IN, before the call leavesctx = attach(ctx) # e.g. deadline header, tokenresp = invoker(ctx, method, req) # hand to next layer# runs on the way OUT, on the replyobserve(resp)return resp
def intercept(ctx, req, handler):# runs as the call ARRIVES, before the handlerif not allowed(ctx): # auth lives here, not in the methodreturn Unauthenticatedresp = handler(ctx, req) # the real greet("Ada")# runs after the handler, before the reply leavesrecord(resp)return resp
def compose(layers, handler):call = handler # the corefor layer in reversed(layers):if not layer.enabled:continue # skip: call passes straight throughcall = wrap(layer, call) # outer now wraps innerreturn call # the chain, outermost-first
Where this sits in Build a gRPC-style RPC framework
Scene 08 of 14, in the Reliability act — Deadlines, cancellation, retries, and the interceptor 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.
Up next. Our chain of layers decides WHAT happens to a call — but not WHERE it goes; the moment we ask which of many backends gets the call, HTTP/2's one-connection trick turns into a trap.
All 14 scenes in Build a gRPC-style RPC framework · Every curriculum