Cross-cutting concerns belong in one place, not copied into every handler.
Interceptors are gRPC's middleware. They wrap calls on the client or server side so you can add behaviour once: logging, metrics, tracing, authentication, adding metadata, retries, or error mapping.
On the client in grpc-js, an interceptor is a function (options, nextCall) => InterceptingCall. You build the returned call with new grpc.InterceptingCall(nextCall(options), requester), where the requester can hook start (see and modify outgoing metadata, observe incoming metadata and status), sendMessage and halfClose. Pass interceptors per call ({ interceptors: [...] }) or for every call on a client via the interceptors option when constructing it.
On the server, grpc-js (1.10+) supports server interceptors passed in the Server constructor options: (methodDescriptor, call) => ServerInterceptingCall. Many teams instead wrap handler functions in a higher-order function, which is simpler for auth checks.
In other languages the API differs (Go's grpc.UnaryInterceptor, Java's ClientInterceptor), but the model is the same: an ordered chain where each link can inspect, modify, short-circuit or delegate.
Keep interceptors generic and cheap. Ready-made ones exist for OpenTelemetry tracing and Prometheus metrics, so you usually only write the auth and logging ones yourself.
Example
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";
const { hello } = grpc.loadPackageDefinition(protoLoader.loadSync("hello.proto"));
// Client interceptor: add a request id and log the final status
const requestId = (options, nextCall) =>
new grpc.InterceptingCall(nextCall(options), {
start(metadata, listener, next) {
metadata.set("x-request-id", "req-42");
next(metadata, {
onReceiveStatus(status, nextStatus) {
console.log(options.method_definition.path, grpc.status[status.code]);
nextStatus(status);
},
});
},
});
const server = new grpc.Server();
server.addService(hello.v1.Greeter.service, {
SayHello: (call, cb) => cb(null, { message: call.metadata.get("x-request-id")[0] }),
});
server.bindAsync("127.0.0.1:50056", grpc.ServerCredentials.createInsecure(), () => {
const client = new hello.v1.Greeter("127.0.0.1:50056", grpc.credentials.createInsecure(), { interceptors: [requestId] });
client.SayHello({ name: "Ann" }, (err, r) => { console.log(r.message); server.forceShutdown(); });
}); // prints: /hello.v1.Greeter/SayHello OK, then req-42Remember
- Interceptors are gRPC middleware on client and server
- Client interceptors can modify metadata and observe status
- Use them for auth, logging, metrics, tracing and retries
Watch out for
- Doing heavy synchronous work in an interceptor, which adds latency to every call.