Patrón Middleware en Go: Composición de Handlers

El patrón middleware en Go es una implementación funcional del patrón decorator. En términos técnicos, consiste en una función de orden superior que recibe un http.Handler como argumento y retorna un nuevo http.Handler. Este nuevo componente envuelve la lógica del original, permitiéndote interceptar la ejecución para realizar tareas antes o después de que la solicitud llegue al controlador final.

Este mecanismo funciona gracias a la simplicidad de la interfaz http.Handler [disponible desde Go 1.0], la cual solo requiere la implementación de un método ServeHTTP(ResponseWriter, *Request). Al retornar un http.HandlerFunc (que es un tipo que implementa la interfaz http.Handler), estamos construyendo una estructura de datos anidada, similar a una cebolla, donde cada capa tiene acceso a la solicitud y a la capacidad de decidir si la ejecución continúa hacia el siguiente eslabón de la cadena.

Deberías utilizar este patrón cuando necesites implementar cross-cutting concerns (preocupaciones transversales): autenticación, registro de logs, trazabilidad mediante IDs de solicitud, limitación de tasa (rate limiting) o recuperación ante pánicos (panic recovery).

Si implementas mal la cadena —por ejemplo, si olvidas llamar a next.ServeHTTP en una de las capas— la solicitud quedará colgada o se cortará abruptamente sin responder, ya que habrás roto el flujo de ejecución hacia el manejador final.

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"time"
)

// contextKey es un tipo personalizado para evitar colisiones de claves en el context.
// Usar un tipo de struct vacío en lugar de un string es una práctica esencial para
// evitar que otros paquetes sobrescriban accidentalmente nuestros valores.
type contextKey struct {
	name string
}

var (
	keyRequestID = contextKey{"request_id"}
	keyUser      = contextKey{"user"}
)

// TraceMiddleware inyecta un ID de seguimiento en el contexto de la solicitud.
func TraceMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requestID := fmt.Sprintf("req-%d", time.Now().UnixNano())
		// Creamos un nuevo contexto derivado con el valor inyectado.
		ctx := context.WithValue(r.Context(), keyRequestID, requestID)
		// Pasamos la solicitud con el nuevo contexto al siguiente handler.
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// AuthMiddleware simula la autenticación extrayendo un usuario del header.
func AuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		user := r.Header.Get("X-User")
		if user == "" {
			http.Error(w, "No autorizado", http.StatusUnauthorized)
			return // Importante: detenemos la cadena si la condición no se cumple.
		}
		ctx := context.WithValue(r.Context(), keyUser, user)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// LoggingMiddleware registra la actividad de la solicitud.
func LoggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		
		// Ejecutamos el siguiente handler en la cadena.
		next.ServeHTTP(w, r)

		// Recuperamos valores del contexto que fueron inyectados por middlewares previos.
		reqID, _ := r.Context().Value(keyRequestID).(string)
		user, _ := r.Context().Value(keyUser).(string)

		log.Printf("[%s] %s %s | User: %s | Duration: %v\n",
			reqID, r.Method, r.URL.Path, user, time.Since(start))
	})
}

// mainHandler es el controlador final de nuestra aplicación.
func mainHandler(w http.ResponseWriter, r *http.Request) {
	user := r.Context().Value(keyUser)
	fmt.Fprintf(w, "Hola, %v! Bienvenido al sistema.", user)
}

func main() {
	// El orden de composición es crítico: el último en envolver es el primero en ejecutarse
	// en la fase de entrada (request), pero el último en terminar en la fase de salida (response).
	chain := LoggingMiddleware(AuthMiddleware(TraceMiddleware(http.HandlerFunc(mainHandler))))

	// Simulamos una petición válida
	req := httptest.NewRequest("GET", "/api/data", nil)
	req.Header.Set("X-User", "gopher_pro")
	rr := httptest.NewRecorder()

	chain.ServeHTTP(rr, req)

	fmt.Printf("Status: %d\nCuerpo: %s\n", rr.Code, rr.Body.String())

	// Simulamos una petición sin autenticación
	reqUnauth := httptest.NewRequest("GET", "/api/data", nil)
	rrUnauth := httptest.NewRecorder()

	chain.ServeHTTP(rrUnauth, reqUnauth)
	fmt.Printf("Status Unauth: %d\nCuerpo Unauth: %s\n", rrUnauth.Code, rrUnauth.Body.String())
}

Análisis de la implementación

En el código anterior, la variable chain define la estructura de la cebolla. Cuando llega una petición, el flujo sigue este orden:

  1. LoggingMiddleware recibe la petición primero. Registra el tiempo de inicio y llama a next.ServeHTTP.
  2. AuthMiddleware recibe la petición de LoggingMiddleware. Si el header X-User es válido, inyecta el usuario en el context mediante r.WithContext(ctx) y llama a next.ServeHTTP.
  3. TraceMiddleware recibe la petición de AuthMiddleware, genera un requestID y lo inyecta en el contexto.
  4. mainHandler (el núcleo) ejecuta la lógica de negocio usando los datos recuperados del contexto.

Es vital notar que r.WithContext(ctx) no modifica la solicitud original, sino que devuelve una copia de la misma con el nuevo contexto. Esto es crucial para la seguridad en entornos concurrentes, asegurando que los cambios en el contexto no afecten a otras goroutines que podrían estar procesando la misma solicitud original (aunque en el modelo de net/http estándar, cada petición corre en su propia goroutine).

El uso de http.HandlerFunc(func(...)) dentro de los middlewares es un “type conversion” necesario para que una función anónima cumpla con la interfaz http.Handler. Sin este paso, el compilador no permitiría retornar la función donde se espera un http.Handler.

El error frecuente

Un error sutil pero devastador en producción ocurre cuando se intenta modificar la respuesta en un middleware y luego se llama a next.ServeHTTP(w, r).

// ERROR: Este middleware causará problemas de integridad en la respuesta
func BrokenMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK) // Escribimos la cabecera aquí
        w.Write([]byte("Error temprano")) 
        
        next.ServeHTTP(w, r) // El siguiente handler intentará escribir de nuevo
    })
}

En Go, una vez que se llama a w.WriteHeader o w.Write, las cabeceras HTTP se envían al cliente. Cualquier intento posterior de modificar las cabeceras (por ejemplo, cambiar el status code o añadir un header en el mainHandler) será ignorado por el servidor y, en muchos casos, el ResponseWriter simplemente escribirá los datos adicionales en el cuerpo de la respuesta, resultando en un payload corrupto o inesperado. Si un middleware decide que la petición no debe continuar, debe responder y no llamar a next.ServeHTTP.

191

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio