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:
- Interceptores Goa: Acceso comprobado en tiempo de compilación a los tipos de dominio de tu servicio
- Middleware HTTP: Patrón estándar
http.Handlerpara problemas específicos de HTTP - Interceptores gRPC: Patrones gRPC estándar para necesidades específicas de RPC
Cuándo usar cada uno
| Preocupación | Herramienta |
|---|---|
| Validación de lógica de negocio | Interceptores Goa |
| Transformación de datos | Interceptores Goa |
| Enriquecimiento de petición/respuesta | Interceptores Goa |
| Logging, trazas | Middleware HTTP/gRPC |
| Compresión, CORS | Middleware HTTP |
| Gestión de metadatos | Interceptores gRPC |
| Limitación de velocidad | Middleware 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
- Interfaz
- Contrato del lado del cliente en
gen/<service>/client_interceptors.go- Interfaz
ClientInterceptorsy sus tipos*Info+ accesores
- Interfaz
- Cadena de wrappers en
gen/<service>/interceptor_wrappers.goWrap<Method>EndpointyWrap<Method>ClientEndpointpor método- En streaming, wrappers de stream que interceptan
SendWithContext/RecvWithContext
- Cableado en
gen/<service>/endpoints.goygen/<service>/client.goNewEndpointsaplica wrappers del lado servidor alrededor de los endpoints del servicioNewClientaplica 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:
- Usas
info.Payload()/info.Result(res)para acceso type-safe (recomendado). - Usas
info.Service(),info.Method()yinfo.CallType()para etiquetar logs/métricas con identificadores estables. - Llamas a
next(ctx, info.RawPayload())para continuar la cadena. - 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 generaRecordID() <type>. - Si declaras
WriteResult(Attribute("cachedAt")), Goa generaSetCachedAt(<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 streamgoa.InterceptorStreamingSend: interceptación de cadaSendWithContextgoa.InterceptorStreamingRecv: interceptación de cadaRecvWithContext
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
- Referencia DSL - Definiciones DSL del interceptor
- Guía HTTP - Patrones de middleware específicos de HTTP
- Guía gRPC - Patrones de interceptores gRPC
- Documentación de Clue - Interceptores y middleware de observabilidad