Skip to main content

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. They can modify the context, the request, the response, and any errors. Interceptors are often used to add logging, metrics, tracing, retries, and other functionality. This document covers 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

Unary interceptors are built on two interfaces: AnyRequest and AnyResponse. They're nearly identical to Request and Response, but provide access to the request and response data only as an any. With those 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.

An example

That's a little abstract, so let's consider an example: we'd like to apply a simple header-based authentication scheme to our RPCs. We could add this logic to each method on our server, but it's less error-prone to write an interceptor instead.

package example

import (
"errors"

"github.com/bufbuild/connect-go"
)

const tokenHeader = "Acme-Token"

func NewAuthInterceptor() connect.UnaryInterceptorFunc {
interceptor := func(next connect.UnaryFunc) connect.UnaryFunc {
return connect.UnaryFunc(func(
ctx context.Context,
req connect.AnyRequest,
) (connect.AnyResponse, error) {
if req.Spec().IsClient {
// Send a token with client requests.
req.Header().Set(tokenHeader, "sample")
} else if req.Header().Get(tokenHeader) == "" {
// Check token in handlers.
return nil, connect.NewError(
connect.CodeUnauthenticated,
errors.New("no token provided"),
)
}
return next(ctx, req)
})
}
return connect.UnaryInterceptorFunc(interceptor)
}

To apply our new interceptor to handlers or clients, we can use WithInterceptors:

interceptors := connect.WithInterceptors(NewAuthInterceptor())
// For handlers:
mux := http.NewServeMux()
mux.Handle(greetv1connect.NewGreetServiceHandler(
&greetServer{},
interceptors,
))
// For clients:
client := greetv1connect.NewGreetServiceClient(
http.DefaultClient,
"https://api.acme.com",
interceptors,
)