Combining Middleware and Interceptors
Learn powerful patterns for combining HTTP middleware with Goa interceptors to create robust and maintainable services.
HTTP middleware in Goa services handles protocol-level concerns like logging, metrics, tracing, and request context management. This guide shows you how to effectively use middleware in your Goa services.
HTTP middleware wraps HTTP handlers to form a processing chain. Each middleware can perform actions before and after the request is handled:
func ExampleMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Pre-processing
// e.g., logging, metrics, tracing
next.ServeHTTP(w, r)
// Post-processing
// e.g., response logging, cleanup
})
}
A typical Goa service uses the following middleware stack:
// Create base HTTP handler
handler := mux
// Add standard middleware chain
handler = debug.HTTP()(handler) // Debug logging control
handler = otelhttp.NewHandler(handler, "service") // OpenTelemetry instrumentation
handler = log.HTTP(ctx)(handler) // Request logging
handler = goahttpmiddleware.RequestID()(handler) // Request ID generation
handler = goahttpmiddleware.PopulateRequestContext()(handler) // Goa context population
Handles logging, metrics, and tracing:
// Logging middleware with path filtering
handler = log.HTTP(ctx,
log.WithPathFilter(regexp.MustCompile(`^/(healthz|metrics)$`)))(handler)
// OpenTelemetry tracing middleware
handler = otelhttp.NewHandler(handler, "service-name",
otelhttp.WithMessageEvents(otelhttp.ReadEvents, otelhttp.WriteEvents))
Enriches the request context with useful information:
func ContextEnrichmentMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Add request-scoped values
ctx := r.Context()
ctx = context.WithValue(ctx, "request.start", time.Now())
ctx = context.WithValue(ctx, "request.id", r.Header.Get("X-Request-ID"))
// Continue with enriched context
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Handles authentication and request validation:
func SecurityHeadersMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set security headers
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)
})
}
Order your middleware carefully - typically from outermost to innermost:
Optimize middleware for performance:
func OptimizedMiddleware(next http.Handler) http.Handler {
// Pre-compile expensive objects
pathRegex := regexp.MustCompile(`^/api/v\d+/`)
// Use sync.Pool for frequently allocated objects
bufPool := sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Skip middleware for non-matching paths
if !pathRegex.MatchString(r.URL.Path) {
next.ServeHTTP(w, r)
return
}
// Use pooled resources
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
next.ServeHTTP(w, r)
})
}
Handle errors consistently:
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 error with request context
log.Printf("panic recovered: %v", err)
// Return 500 response
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
When integrating middleware with a Goa service:
func main() {
// 1. Create base muxer
mux := goahttp.NewMuxer()
// 2. Create and mount Goa server
server := genhttp.New(endpoints, mux, decoder, encoder, eh, eh)
genhttp.Mount(mux, server)
// 3. Add middleware stack
handler := mux
handler = debug.HTTP()(handler) // Debug logging
handler = otelhttp.NewHandler(handler, "svc") // Tracing
handler = log.HTTP(ctx)(handler) // Request logging
handler = goahttpmiddleware.RequestID()(handler) // Request ID
// 4. Create HTTP server with timeouts
httpServer := &http.Server{
Addr: ":8080",
Handler: handler,
ReadHeaderTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
}
Test middleware in isolation and as part of the chain:
func TestMiddleware(t *testing.T) {
// Create test handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
// Add middleware
handler = YourMiddleware(handler)
// Create test request
req := httptest.NewRequest("GET", "/test", nil)
rec := httptest.NewRecorder()
// Test
handler.ServeHTTP(rec, req)
// Assert results
if rec.Code != http.StatusOK {
t.Errorf("got status %d, want %d", rec.Code, http.StatusOK)
}
}
HTTP middleware is a powerful tool for handling protocol-specific concerns in your Goa services. By following these patterns and best practices, you can create clean, maintainable, and efficient HTTP processing pipelines.