Middleware en clientes HTTP con RoundTripper

En Go, cuando necesitas interceptar, modificar o registrar cada petición saliente de tu aplicación, no debes manipular el http.Client directamente. La forma correcta de implementar lógica transversal (logging, métricas, autenticación, retries) es mediante la implementación personalizada de la interfaz http.RoundTripper.

A diferencia de los middlewares de servidor, que implementan http.Handler para procesar peticiones entrantes, un RoundTripper actúa como un decorador sobre el transporte de red. La interfaz se reduce a un único método: RoundTrip(*http.Request) (*http.Response, error). Su función es ejecutar un ciclo completo de petición-respuesta. Al envolver el transporte base (como http.DefaultTransport), puedes interceptar la petición antes de que viaje por el wire y analizar la respuesta cuando regresa.

Este enfoque es preferible porque el http.Client es una estructura de alto nivel que gestiona reintentos de redirección, manejo de cookies y timeouts. Si intentas añadir lógica en el cliente, estarás operando en una capa demasiado alta. Al usar un RoundTripper, te aseguras de que tu lógica se ejecute exactamente en el punto donde la petición se convierte en una operación de red, permitiendo que el cliente siga gestionando su lógica de alto nivel de forma transparente.

Debes usar este patrón cuando la lógica de negocio no debe preocuparse por la infraestructura (ej. inyectar un token JWT o medir la latencia de una API externa). Si lo implementas mal, específicamente si mutas el puntero *http.Request original en lugar de crear una copia, causarás condiciones de carrera (race conditions) impredecibles, ya que estarás alterando un objeto que otros goroutines podrían estar leyendo simultáneamente.

package main

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

// TelemetryTransport es un middleware para el cliente HTTP que
// añade un ID de rastreo y mide el tiempo de respuesta.
type TelemetryTransport struct {
	// Base es el transporte real (ej. http.DefaultTransport).
	Base http.RoundTripper
}

// RoundTrip implementa la interfaz http.RoundTripper.
func (t *TelemetryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	start := time.Now()

	// IMPORTANTE: Debemos clonar la petición. req es un puntero.
	// Si modificamos req directamente, afectamos al llamador y causamos race conditions.
	// req.Clone crea una copia superficial segura para la concurrencia.
	reqCopy := req.Clone(req.Context())
	
	// Inyectamos un header para trazabilidad en el servidor.
	reqCopy.Header.Set("X-Request-ID", "trace-12345")

	// Ejecutamos la petición usando el transporte base.
	resp, err := t.Base.RoundTrip(reqCopy)

	// Calculamos la latencia después de que la petición termine.
	duration := time.Since(start)

	if err != nil {
		log.Printf("[Telemetry] Petición falló tras %v: %v", duration, err)
		return nil, err
	}

	log.Printf("[Telemetry] Status: %d | Latencia: %v", resp.StatusCode, duration)
	
	return resp, nil
}

func main() {
	// Configuramos un servidor de prueba para la demostración.
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Printf("Servidor recibió Request-ID: %s\n", r.Header.Get("X-Request-ID"))
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("OK"))
	}))
	defer server.Close()

	// Configuramos el cliente con nuestro middleware personalizado.
	client := &http.Client{
		Timeout: 5 * time.Second,
		// Envolvemos el transporte por defecto con nuestra lógica de telemetría.
		Transport: &TelemetryTransport{
			Base: http.DefaultTransport,
		},
	}

	// Realizamos una petición al servidor de prueba.
	resp, err := client.Get(server.URL)
	if err != nil {
		log.Fatalf("Error en la petición: %v", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Printf("Respuesta final: %s\n", string(body))
}

Análisis del funcionamiento

En el ejemplo, TelemetryTransport actúa como un envoltorio sobre http.DefaultTransport.

  1. Clonación de la petición: La línea req.Clone(req.Context()) es la más crítica. En Go, los objetos http.Request se pasan por puntero. Si hiciéramos req.Header.Set(...) directamente, estaríamos modificando el objeto original que el resto de la aplicación está usando. Si dos goroutines usaran el mismo cliente simultáneamente, habría una colisión de memoria. Clone asegura que la modificación de headers solo afecte a esta ejecución específica.
  2. Delegación: La llamada t.Base.RoundTrip(reqCopy) es lo que realmente realiza la magia. No estamos implementando el protocolo HTTP desde cero; simplemente estamos pasando la petición modificada al “motor” real (el DefaultTransport), que se encarga de la gestión de conexiones TCP y TLS.
  3. Observabilidad: Al rodear la llamada a t.Base.RoundTrip con el cálculo de time.Since(start), obtenemos una métrica exacta de la latencia de red, incluyendo el tiempo que el servidor tardó en procesar la solicitud, algo que sería imposible medir desde la capa del http.Client.

El error frecuente

El error más peligroso al implementar RoundTrip es la mutación de campos compartidos. Un error común es intentar modificar el cuerpo de la petición (req.Body) sin tener en cuenta que es un io.ReadCloser que se consume al leerse.

// ERROR: Esto romperá la petición o causará errores de "body already read"
func (t *BadTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    // Error: Modificar el header original causa race conditions
    req.Header.Set("X-Bad", "value") 
    
    // Error: Si lees req.Body para loguearlo, el transport real recibirá un Body vacío
    body, _ := io.ReadAll(req.Body)
    log.Printf("Body: %s", body)
    
    // No podemos simplemente reasignar el Body porque el original ya se leyó
    // req.Body = io.NopCloser(bytes.NewBuffer(body)) // Esto es un parche, no una solución limpia
    
    return t.Base.RoundTrip(req)
}

Si necesitas leer o modificar el cuerpo de la petición en un middleware, debes clonar el cuerpo utilizando io.NopCloser y un bytes.Buffer para asegurar que el flujo de datos esté disponible para el siguiente componente en la cadena.

188

Dejar un comentario

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

Scroll al inicio