Interceptors
Interceptors are similar to the middleware or decorators you may be familiar
with from other frameworks: they’re the primary way of extending Connect and are
often used to add logging, metrics, tracing, retries, and other
functionality.
If you followed the getting started guide, you’ve already seen an interceptor in action:
the validate-go interceptor powers the Protovalidate integration that made sure every GreetRequest contained a valid name.
On this page you’ll learn how to build unary interceptors — more complex use cases are covered in the streaming documentation.
Take care when writing interceptors! They’re powerful, but overly complex interceptors can make debugging difficult.
Interceptors are functions
Section titled “Interceptors are functions”Unary interceptors are built on two interfaces: AnyRequest and AnyResponse
and provide access to the request and response data only as an any. With these
interfaces, we can model all unary RPCs as:
type UnaryFunc func(context.Context, AnyRequest) (AnyResponse, error)An interceptor wraps an RPC with some additional logic, so it’s transforming
one UnaryFunc into another:
type UnaryInterceptorFunc func(UnaryFunc) UnaryFuncMost unary interceptors are best implemented as a UnaryInterceptorFunc.
An example
Section titled “An example”That’s a little abstract, so let’s consider an example: we’d like to log every RPC. We could add logging to each method on our server, but it’s less error-prone to write an interceptor instead.
package example
import ( "context" "log/slog"
"connectrpc.com/connect")
func NewLoggingInterceptor() connect.UnaryInterceptorFunc { return func(next connect.UnaryFunc) connect.UnaryFunc { return func( ctx context.Context, req connect.AnyRequest, ) (connect.AnyResponse, error) { spec := req.Spec() slog.InfoContext(ctx, "rpc", "procedure", spec.Procedure, "is_client", spec.IsClient, ) return next(ctx, req) } }}To apply our new interceptor to handlers or clients, we can use
WithInterceptors:
// For handlers:interceptors := connect.WithInterceptors( NewLoggingInterceptor(), validate.NewInterceptor(),)mux := http.NewServeMux()mux.Handle(greetv1connect.NewGreetServiceHandler( &GreetServer{}, interceptors,))// For clients:client := greetv1connect.NewGreetServiceClient( http.DefaultClient, "http://localhost:8080", connect.WithInterceptors(NewLoggingInterceptor()),)Authentication
Section titled “Authentication”Don’t use interceptors to authenticate requests on the server. Handlers run unary interceptors after the request message has been read, decompressed, and unmarshaled. An interceptor-based check lets unauthenticated clients consume memory and CPU on your server.
Instead, use a request gate. Gates run once the request headers are available, before Connect reads any message and before the interceptor chain. Returning an error rejects the RPC immediately, so neither your interceptors nor your handler run.
type userKey struct{}
func authGate( ctx context.Context, _ connect.Spec, _ connect.Peer, header http.Header,) (context.Context, error) { // authenticate comes from your auth library. user, ok := authenticate(header.Get("Authorization")) if !ok { return nil, connect.NewError( connect.CodeUnauthenticated, errors.New("invalid credentials"), ) } // Pass the user along to interceptors and the handler. return context.WithValue(ctx, userKey{}, user), nil}Register the gate with
WithRequestGate:
mux.Handle(greetv1connect.NewGreetServiceHandler( &GreetServer{}, connect.WithRequestGate(authGate),))Rejected RPCs never reach the interceptor chain, so logging and metrics
interceptors don’t observe them. To turn requests away even earlier, use
standard net/http middleware. The
authn-go package provides
authentication middleware designed for Connect servers.