Skip to content

Interceptors & Headers

Interceptors provide middleware for cross-cutting concerns like logging, authentication, metrics, and tracing. Headers enable bidirectional metadata propagation.

Server Interceptors

Interceptors wrap every handler call. They see the full request context and can modify the response.

go
func loggingInterceptor(
    ctx context.Context,
    req interface{},
    info *productv1.UnaryServerInfo,
    handler productv1.UnaryHandler,
) (interface{}, error) {
    start := time.Now()
    log.Printf("→ %s.%s", info.Service, info.Method)

    resp, err := handler(ctx, req)

    log.Printf("← %s.%s (%v)", info.Service, info.Method, time.Since(start))
    return resp, err
}

Registration

go
svc, err := RegisterProductServiceHandlers(nc, impl,
    WithServerInterceptor(loggingInterceptor),
    WithServerInterceptor(metricsInterceptor),
    WithServerInterceptor(authInterceptor),
)

Interceptors execute in order: logging → metrics → auth → handler → auth → metrics → logging

Client Interceptors

Same pattern on the client side:

go
func clientTraceInterceptor(
    ctx context.Context,
    method string,
    req, reply interface{},
    invoker UnaryInvoker,
) error {
    // Add trace header before sending
    ctx = WithOutgoingHeaders(ctx, nats.Header{
        "X-Trace-Id": []string{uuid.New().String()},
    })
    return invoker(ctx, method, req, reply)
}

Registration

go
client := NewProductServiceNatsClient(nc,
    WithClientInterceptor(clientTraceInterceptor),
)

Headers

Reading Request Headers (Server)

go
func myHandler(ctx context.Context, req *MyRequest) (*MyResponse, error) {
    headers := IncomingHeaders(ctx)
    traceID := headers.Get("X-Trace-Id")
    // ...
}

Setting Response Headers (Server)

go
func myInterceptor(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (interface{}, error) {
    responseHeaders := nats.Header{}
    responseHeaders.Set("X-Server-Version", "1.0.0")
    SetResponseHeaders(ctx, responseHeaders)

    return handler(ctx, req)
}

Setting Request Headers (Client)

go
ctx := WithOutgoingHeaders(context.Background(), nats.Header{
    "X-Client-Version": []string{"2.0.0"},
    "Authorization":    []string{"Bearer " + token},
})
resp, err := client.GetProduct(ctx, req)

Reading Response Headers (Client)

go
ctx := context.Background()
resp, err := client.GetProduct(ctx, req)
// Response headers are available via the context after the call
headers := ResponseHeaders(ctx)
serverVersion := headers.Get("X-Server-Version")

Common Patterns

Authentication

go
func authInterceptor(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (interface{}, error) {
    headers := IncomingHeaders(ctx)
    token := headers.Get("Authorization")
    if token == "" {
        return nil, NewMyServiceUnauthenticatedError(info.Method, "missing auth token")
    }
    // Verify token...
    return handler(ctx, req)
}

Request Timing

go
func timingInterceptor(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (interface{}, error) {
    start := time.Now()
    resp, err := handler(ctx, req)
    duration := time.Since(start)

    responseHeaders := nats.Header{}
    responseHeaders.Set("X-Duration-Ms", fmt.Sprintf("%d", duration.Milliseconds()))
    SetResponseHeaders(ctx, responseHeaders)

    return resp, err
}

Released under the MIT License.