Interceptors

Complete guide to interceptors and middleware in Goa - type-safe Goa interceptors, HTTP middleware, and gRPC interceptors.

Goa proporciona una solución completa para el procesamiento de peticiones que combina interceptores de tipo seguro con patrones de middleware tradicionales. Esta guía cubre los tres enfoques.

Visión general

Cuando se procesan peticiones en un servicio Goa, se dispone de tres herramientas complementarias:

  1. Interceptores Goa: Acceso comprobado en tiempo de compilación a los tipos de dominio de tu servicio
  2. Middleware HTTP: Patrón estándar http.Handler para problemas específicos de HTTP
  3. Interceptores gRPC: Patrones gRPC estándar para necesidades específicas de RPC

Cuándo usar cada uno

PreocupaciónHerramienta
Validación de lógica de negocioInterceptores Goa
Transformación de datosInterceptores Goa
Enriquecimiento de petición/respuestaInterceptores Goa
Logging, trazasMiddleware HTTP/gRPC
Compresión, CORSMiddleware HTTP
Gestión de metadatosInterceptores gRPC
Limitación de velocidadMiddleware HTTP/gRPC

Interceptores Goa

Los interceptores Goa proporcionan acceso seguro a los tipos de dominio de su servicio, con comprobaciones en tiempo de compilación y métodos de ayuda generados.

Modelo de ejecución (código generado)

Los interceptores no son “ganchos mágicos” del runtime. En Goa son wrappers de endpoints generados. El DSL indica qué campos puede leer/escribir un interceptor y la generación produce:

  • Contrato del lado del servicio en gen/<service>/service_interceptors.go
    • Interfaz ServerInterceptors: un método por interceptor
    • Estructuras *<Interceptor>Info: metadatos de servicio/método + accesores
    • Interfaces de acceso *Payload / *Result: solo los campos que declaraste como legibles/escribibles
  • Contrato del lado del cliente en gen/<service>/client_interceptors.go
    • Interfaz ClientInterceptors y sus tipos *Info + accesores
  • Cadena de wrappers en gen/<service>/interceptor_wrappers.go
    • Wrap<Method>Endpoint y Wrap<Method>ClientEndpoint por método
    • En streaming, wrappers de stream que interceptan SendWithContext / RecvWithContext
  • Cableado en gen/<service>/endpoints.go y gen/<service>/client.go
    • NewEndpoints aplica wrappers del lado servidor alrededor de los endpoints del servicio
    • NewClient aplica wrappers del lado cliente alrededor de los endpoints del transporte

La consecuencia importante es: los interceptores del servidor se ejecutan después del decode del transporte y antes del método del servicio, y los interceptores del cliente se ejecutan antes del encode del transporte y después del decode de la respuesta (porque envuelven el mismo endpoint tipado que llama tu código cliente).

Contrato del interceptor del servidor

Goa genera una interfaz por servicio. Cada método del interceptor debe llamar a next exactamente una vez para continuar (o devolver un error/respuesta antes):

type ServerInterceptors interface {
    RequestAudit(ctx context.Context, info *RequestAuditInfo, next goa.Endpoint) (any, error)
}

En tiempo de ejecución, normalmente:

  1. Usas info.Payload() / info.Result(res) para acceso type-safe (recomendado).
  2. Usas info.Service(), info.Method() y info.CallType() para etiquetar logs/métricas con identificadores estables.
  3. Llamas a next(ctx, info.RawPayload()) para continuar la cadena.
  4. Opcionalmente mutas el payload antes de next, o mutas el result después.

Ejemplo (enriquecimiento del result + timing):

type Interceptors struct{}

func (i *Interceptors) RequestAudit(ctx context.Context, info *RequestAuditInfo, next goa.Endpoint) (any, error) {
    start := time.Now()

    res, err := next(ctx, info.RawPayload())
    if err != nil {
        return nil, err
    }

    r := info.Result(res)
    r.SetProcessedAt(time.Now().UTC().Format(time.RFC3339Nano))
    r.SetDuration(int(time.Since(start).Milliseconds()))

    return res, nil
}

Por qué importan las interfaces de acceso:

  • Si declaras ReadPayload(Attribute("recordID")), Goa genera RecordID() <type>.
  • Si declaras WriteResult(Attribute("cachedAt")), Goa genera SetCachedAt(<type>).
  • El interceptor no puede tocar campos que no declaraste; ese es el contrato en tiempo de compilación.

Contrato del interceptor del cliente

Los interceptores de cliente son lo mismo, pero del lado cliente: envuelven el endpoint de transporte que pasas a gen/<service>.NewClient(...).

En la práctica eso significa:

  • info.RawPayload() es el payload tipado del método (por ejemplo, *GetPayload), no un *http.Request.
  • Si “escribes” en el payload en el interceptor, el endpoint de transporte codificará esos cambios (headers/body/etc.) según los mapeos del transporte.
  • Puedes usar info.Result(res) para leer/escribir campos del result tras decodificar la respuesta.

Orden (qué corre primero de verdad)

Los interceptores se aplican generando una cadena de wrappers. El Wrap<Method>Endpoint generado es la fuente de verdad del orden.

Conceptualmente, la generación hace algo así:

func WrapGetEndpoint(endpoint goa.Endpoint, i ServerInterceptors) goa.Endpoint {
    endpoint = wrapGetCache(endpoint, i)
    endpoint = wrapGetJWTAuth(endpoint, i)
    endpoint = wrapGetRequestAudit(endpoint, i)
    endpoint = wrapGetSetDeadline(endpoint, i)
    endpoint = wrapGetTraceRequest(endpoint, i)
    return endpoint
}

Cada wrap... devuelve un nuevo endpoint que llama al interceptor con next apuntando al endpoint anterior. Por eso:

  • En la entrada (petición): el último wrapper se ejecuta primero.
  • En la salida (respuesta): el primer wrapper se ejecuta primero.

Si el orden importa, usa este modelo mental en vez de una regla genérica: mira el wrap generado del método.

Interceptores de streaming (Send/Recv)

En streaming bidireccional, la generación envuelve el stream para que cada envío/recepción sea interceptado. Un mismo interceptor puede invocarse con distintos tipos de llamada:

  • goa.InterceptorUnary: interceptación única de la llamada al endpoint del stream
  • goa.InterceptorStreamingSend: interceptación de cada SendWithContext
  • goa.InterceptorStreamingRecv: interceptación de cada RecvWithContext

Usa info.CallType() si necesitas ramificar. En send, info.RawPayload() es el mensaje. En recv, el “payload” lo produce next (tu interceptor lo ve como valor de retorno).

Definición de Interceptores

var RequestLogger = Interceptor("RequestLogger", func() {
    Description("Logs incoming requests and their timing")
    
    // Read status from result
    ReadResult(func() {
        Attribute("status")
    })
    
    // Add timing information to result
    WriteResult(func() {
        Attribute("processedAt")
        Attribute("duration")
    })
})

Aplicación de interceptores

Aplicar a nivel de servicio o método:

var _ = Service("calculator", func() {
    // Apply to all methods
    ServerInterceptor(RequestLogger)
    
    Method("add", func() {
        // Method-specific interceptor
        ServerInterceptor(ValidateNumbers)
        
        Payload(func() {
            Attribute("a", Int)
            Attribute("b", Int)
        })
        Result(func() {
            Attribute("sum", Int)
            Attribute("status", Int)
            Attribute("processedAt", String)
            Attribute("duration", Int)
        })
    })
})

Implementación de interceptores

func (i *Interceptors) RequestLogger(ctx context.Context, info *RequestLoggerInfo, next goa.Endpoint) (any, error) {
    start := time.Now()
    
    // Call next interceptor or endpoint
    res, err := next(ctx, info.RawPayload())
    if err != nil {
        return nil, err
    }
    
    // Access result through type-safe interface
    r := info.Result(res)
    
    // Add timing information
    r.SetProcessedAt(time.Now().Format(time.RFC3339))
    r.SetDuration(int(time.Since(start).Milliseconds()))
    
    return res, nil
}

Patrones de acceso

Acceso de sólo lectura

var Monitor = Interceptor("Monitor", func() {
    Description("Collects metrics without modifying data")
    
    ReadPayload(func() {
        Attribute("size")
    })
    
    ReadResult(func() {
        Attribute("status")
    })
})

Acceso de escritura

var Enricher = Interceptor("Enricher", func() {
    Description("Adds context information")
    
    WritePayload(func() {
        Attribute("requestID")
    })
    
    WriteResult(func() {
        Attribute("processedAt")
    })
})

Acceso combinado

var DataProcessor = Interceptor("DataProcessor", func() {
    Description("Processes both requests and responses")
    
    ReadPayload(func() {
        Attribute("rawData")
    })
    WritePayload(func() {
        Attribute("processed")
    })
    
    ReadResult(func() {
        Attribute("status")
    })
    WriteResult(func() {
        Attribute("enriched")
    })
})

Interceptores del lado del cliente

var ClientContext = Interceptor("ClientContext", func() {
    Description("Enriches requests with client context")
    
    WritePayload(func() {
        Attribute("clientVersion")
        Attribute("clientID")
    })
    
    ReadResult(func() {
        Attribute("rateLimit")
        Attribute("rateLimitRemaining")
    })
})

var _ = Service("inventory", func() {
    ClientInterceptor(ClientContext)
    // ...
})

Interceptores de flujo

Para los métodos de streaming, utilice variantes de streaming:

var ServerProgressTracker = Interceptor("ServerProgressTracker", func() {
    Description("Adds progress to server stream responses")
    
    WriteStreamingResult(func() {
        Attribute("percentComplete")
        Attribute("itemsProcessed")
    })
})

var ClientMetadataEnricher = Interceptor("ClientMetadataEnricher", func() {
    Description("Enriches outgoing client stream messages")
    
    WriteStreamingPayload(func() {
        Attribute("clientTimestamp")
    })
})

Orden de ejecución

Goa aplica interceptores construyendo una cadena de wrappers alrededor del endpoint de cada método. La forma más sencilla de entender el orden exacto (sobre todo cuando mezclas interceptores a nivel de servicio y de método) es mirar el Wrap<Method>Endpoint generado y recordar:

  • El último wrapper se ejecuta primero en la ruta de petición.
  • El primer wrapper se ejecuta primero en la ruta de respuesta.

Si necesitas un contrato estable en tu propio código, trata el wrap generado como la especificación canónica del orden para ese método.


Middleware HTTP

El middleware HTTP gestiona los problemas a nivel de protocolo utilizando el patrón estándar http.Handler.

Pila de middleware común

mux := goahttp.NewMuxer()

// Add middleware (outermost to innermost)
mux.Use(debug.HTTP())                               // Debug logging
mux.Use(otelhttp.NewMiddleware("service"))          // OpenTelemetry
mux.Use(log.HTTP(ctx))                              // Request logging
mux.Use(goahttpmiddleware.RequestID())              // Request ID
mux.Use(goahttpmiddleware.PopulateRequestContext()) // Goa context

Creación de middleware personalizado

func ExampleMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Pre-processing
        start := time.Now()
        
        next.ServeHTTP(w, r)
        
        // Post-processing
        log.Printf("Request took %v", time.Since(start))
    })
}

Middleware de cabeceras de seguridad

func SecurityHeadersMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("X-Frame-Options", "DENY")
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("X-XSS-Protection", "1; mode=block")
        
        next.ServeHTTP(w, r)
    })
}

Middleware de enriquecimiento del contexto

func ContextEnrichmentMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        ctx = context.WithValue(ctx, "request.start", time.Now())
        ctx = context.WithValue(ctx, "request.id", r.Header.Get("X-Request-ID"))
        
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Middleware de gestión de errores

func ErrorHandlingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic recovered: %v", err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        
        next.ServeHTTP(w, r)
    })
}

Interceptores gRPC

los interceptores gRPC gestionan los problemas a nivel de protocolo de las llamadas RPC.

Interceptores Unarios

func LoggingInterceptor() grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        start := time.Now()
        
        resp, err := handler(ctx, req)
        
        log.Printf("Method: %s, Duration: %v, Error: %v",
            info.FullMethod, time.Since(start), err)
        
        return resp, err
    }
}

Interceptores de flujo

func StreamLoggingInterceptor() grpc.StreamServerInterceptor {
    return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
        start := time.Now()
        
        err := handler(srv, ss)
        
        log.Printf("Stream: %s, Duration: %v, Error: %v",
            info.FullMethod, time.Since(start), err)
        
        return err
    }
}

Integración con Goa

func main() {
    srv := grpc.NewServer(
        grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
            MetadataInterceptor(),
            LoggingInterceptor(),
            MonitoringInterceptor(),
        )),
        grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
            StreamMetadataInterceptor(),
            StreamLoggingInterceptor(),
        )),
    )

    pb.RegisterServiceServer(srv, server)
}

Combinando los tres

He aquí cómo funcionan los tres enfoques juntos:

func main() {
    // 1. Create service with Goa interceptors
    svc := NewService()
    interceptors := NewInterceptors(log.Default())
    endpoints := NewEndpoints(svc, interceptors)
    
    // 2. Set up HTTP with middleware
    mux := goahttp.NewMuxer()
    mux.Use(otelhttp.NewMiddleware("payment-svc"))
    mux.Use(debug.HTTP())
    mux.Use(log.HTTP(ctx))
    
    httpServer := genhttp.New(endpoints, mux, dec, enc, eh, eh)
    genhttp.Mount(mux, httpServer)
    
    // 3. Set up gRPC with interceptors
    grpcServer := grpc.NewServer(
        grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
            grpc_recovery.UnaryServerInterceptor(),
            grpc_prometheus.UnaryServerInterceptor,
        )),
    )
    
    grpcSvr := gengrpc.New(endpoints, nil)
    genpb.RegisterPaymentServer(grpcServer, grpcSvr)
}

Flujo de ejecución

Request Processing:
─────────────────────────────────────────────────────────────────>
HTTP/gRPC Middleware → Goa Interceptors → Service Method

Response Processing:
<─────────────────────────────────────────────────────────────────
Service Method → Goa Interceptors → HTTP/gRPC Middleware

Mejores prácticas

Interceptores Goa

  • Usar para validación de lógica de negocio y transformación de datos
  • Mantener los interceptores centrados en responsabilidades únicas
  • Utilizar patrones de acceso seguros

Middleware HTTP

  • Ordenar cuidadosamente el middleware (primero la recuperación de pánico, luego el logging, etc.)
  • Precompile objetos costosos (regex, etc.)
  • Utilice sync.Pool para los objetos asignados con frecuencia

Interceptores gRPC

  • Centrarse en las preocupaciones a nivel de protocolo
  • Manejar adecuadamente la cancelación de contexto
  • Utilizar códigos de estado apropiados

General

  • Probar los interceptores y el middleware de forma aislada
  • Considerar el impacto en el rendimiento
  • Documente la finalidad de cada interceptor

Véase también