Interceptors
Middleware for RPC
Section titled “Middleware for RPC”An interceptor is gRPC’s version of middleware: a function that wraps a call so you can run code before and after the actual handler. If you have used HTTP middleware, this is the same idea, applied to RPCs instead of routes.
The point is to keep cross-cutting logic — logging, authentication, metrics, tracing, retries — out of your business handlers. You write the concern once as an interceptor and it applies to every method automatically.
The chain wraps the handler
Section titled “The chain wraps the handler”Interceptors compose into a chain. Each one gets the call, does its work, and calls the next link — until the innermost link is your handler. On the way back out, each interceptor can inspect the response or error.
flowchart LR call["incoming call"] --> log["logging interceptor"] log --> auth["auth interceptor"] auth --> metrics["metrics interceptor"] metrics --> handler["your handler"] handler -->|response / error| metrics metrics --> auth auth --> log log -->|response| call
The order matters: put logging outermost so it records everything, auth next so unauthenticated calls are rejected before they reach expensive work, and metrics close to the handler so timings reflect real work.
Two flavors: unary and streaming
Section titled “Two flavors: unary and streaming”Because gRPC has unary and streaming calls, it has two interceptor types:
- A unary interceptor wraps a single request/response call. It sees the request, calls the handler, and sees the response — one shot.
- A stream interceptor wraps a streaming call. It doesn’t see individual messages by default; it wraps the stream object, so to observe each message you wrap the stream’s send/receive methods.
That asymmetry trips people up: a logging interceptor written for unary calls won’t automatically log every streamed message — you have to wrap the stream.
A minimal logging interceptor
Section titled “A minimal logging interceptor”Here is server-side logging that records the method and elapsed time for every unary call.
func loggingUnary( ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,) (any, error) { start := time.Now() resp, err := handler(ctx, req) // call the next link (eventually your handler) log.Printf("method=%s took=%s err=%v", info.FullMethod, time.Since(start), err) return resp, err}
server := grpc.NewServer(grpc.ChainUnaryInterceptor(loggingUnary))class LoggingInterceptor(grpc.ServerInterceptor): def intercept_service(self, continuation, handler_call_details): method = handler_call_details.method start = time.time() handler = continuation(handler_call_details) # the next link logging.info("method=%s registered", method) return handler # wrap handler.unary_unary to time each call in practice
server = grpc.server(executor, interceptors=[LoggingInterceptor()])// @grpc/grpc-js exposes interceptors on the client; on the server,// wrap handlers or use a middleware library such as nice-grpc.const loggingUnary: ServerMiddleware = async function* (call, ctx) { const start = Date.now(); try { return yield* call.next(call.request, ctx); } finally { console.log(`method=${ctx.path} took=${Date.now() - start}ms`); }};Interceptors run on both sides: server interceptors enforce auth and record metrics; client interceptors attach tokens, add trace ids, and implement retries with backoff. The same chain idea applies in both directions.
Common uses
Section titled “Common uses”- Logging & tracing — one place to record method, latency, status, and a trace id.
- Auth — validate a token from metadata and reject early with
UNAUTHENTICATED. - Metrics — count calls and observe latency per method for dashboards.
- Retries — a client interceptor can transparently retry idempotent calls on transient failures.
- Recovery — catch a panic/exception in a handler and turn it into a clean
INTERNALstatus instead of crashing the connection.