Skip to content
Created by

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.

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) UnaryFunc

Most unary interceptors are best implemented as a UnaryInterceptorFunc.

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()),
)

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, authenticate at the HTTP layer with standard net/http middleware, which runs before Connect reads the request body. The authn-go package provides authentication middleware designed for Connect servers.