StackPractices
intermediate Por Mathias Paulenko

Middleware: Interceptores HTTP

Cómo implementar middleware de request/response para logging, auth y manejo de errores en Python, JavaScript y Java.

Temas: api

Visión general

Cada request a tu API necesita el mismo trabajo previo: loguearlo, verificar el token de auth, añadir headers CORS, medir cuánto tarda, capturar lo que explote. El middleware es la capa que hace ese trabajo — funciones que se ubican entre el request entrante y tu route handler, ejecutándose en un pipeline antes (y a veces después) del handler.

Cada middleware de la cadena puede inspeccionar o modificar el request, abortarlo temprano (un 401 de un chequeo de auth), o pasar el control a la siguiente capa con next(). El orden en que los registras es el orden en que se ejecutan — por eso el bug clásico es poner el error handler antes de las rutas.

Cuándo usarlo

Usa esta recipe cuando:

  • Aplicar autenticación antes de que los route handlers se ejecuten. Consulta JWT Authentication para patrones de auth.
  • Loguear todos los requests entrantes y tiempos de respuesta. Consulta Logging para logging estructurado.
  • Añadir headers CORS o de seguridad a cada response. Consulta Manejo de CORS para la configuración de CORS.
  • Validar cuerpos de request o parámetros de query en un solo lugar en vez de dentro de cada handler.
  • Implementar rate limiting o throttling de requests. Consulta Rate Limiting con Redis para rate limiting basado en Redis.

Esta recipe muestra la forma del middleware agnóstica a framework — las cadenas de Express y FastAPI de abajo son ejecutables en el repositorio companion. Para composiciones específicas de Express — wrappers async, scoping por path de montaje, error boundaries — consulta patrones de middleware en Express.

Solución

Python (FastAPI)

from fastapi import Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
import time

# Middleware personalizado
@app.middleware("http")
async def log_requests(request: Request, call_next):
    start = time.time()
    response = await call_next(request)
    duration = time.time() - start
    print(f"{request.method} {request.url.path} - {response.status_code} ({duration:.3f}s)")
    return response

# Middleware CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://example.com"],
    allow_methods=["GET", "POST"],
)

# Dependencia de auth (usada como middleware en rutas)
async def verify_token(request: Request):
    token = request.headers.get("Authorization")
    if not token or not token.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Missing token")
    return token

@app.get("/protected", dependencies=[Depends(verify_token)])
async def protected():
    return {"message": "Secret data"}

JavaScript (Express)

const express = require('express');
const app = express();

// Middleware de logging
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
  });
  next();
});

// Middleware de auth
function authMiddleware(req, res, next) {
  const token = req.headers.authorization;
  if (!token || !token.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  req.user = verifyToken(token); // tu lógica de verificación de token
  next();
}

app.use('/api/protected', authMiddleware);

// Middleware de manejo de errores (debe ir al final)
app.use((err, req, res, next) => {
  console.error(err);
  res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' });
});

Java (Spring Boot)

@Component
public class LoggingInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
        req.setAttribute("startTime", System.currentTimeMillis());
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception ex) {
        long start = (Long) req.getAttribute("startTime");
        long duration = System.currentTimeMillis() - start;
        System.out.println(req.getMethod() + " " + req.getRequestURI() + " - " + res.getStatus() + " (" + duration + "ms)");
    }
}

// Registrar interceptor
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoggingInterceptor loggingInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loggingInterceptor).addPathPatterns("/api/**");
    }
}

Explicación

El pipeline es la idea central. Un request entra por la capa más externa y viaja hacia adentro a través de cada middleware hasta llegar al handler; el response luego vuelve hacia afuera por las mismas capas en orden inverso:

sequenceDiagram diagram: participant Client

Cuando auth rechaza el request, next() nunca se llama y el handler nunca se ejecuta — ese comportamiento de “fail fast” es lo que hace del middleware el hogar correcto para auth, validación y rate limiting.

Cada framework expone hooks en distintos puntos del pipeline:

  • Antes del handler (Express app.use, FastAPI call_next, Spring preHandle): inspeccionar, modificar o bloquear el request.
  • De camino a la salida (Express res.on('finish'), Spring postHandle): añadir headers, loguear, registrar métricas.
  • Después de enviar el response (Spring afterCompletion): limpieza, logging asíncrono.
  • Middleware de errores (la firma de cuatro argumentos (err, req, res, next) de Express): captura lo que se lance en cualquier capa posterior. Express lo identifica solo por el número de argumentos — una función de tres nunca se llama para errores. Debe registrarse en último lugar.

Middleware vs decoradores y guards: el middleware corre para todo request que coincida con su path de montaje; un decorador o dependencia (Depends de FastAPI, un guard de NestJS, @PreAuthorize de Spring) se adhiere a una ruta concreta. Usa middleware cuando el problema es verdaderamente transversal — logging, CORS, correlation IDs — y para reglas por endpoint usa una dependencia o un guard. Un chequeo de auth por ruta hecho como middleware global te obliga a mantener una lista de excepciones, y eso es lo que todo el mundo olvida.

Un ejemplo concreto de por qué importa el orden: los navegadores envían un preflight OPTIONS antes de cualquier request cross-origin — y los preflights no llevan credenciales. Registra auth antes de CORS y cada preflight muere en un 401 antes de que se adjunten los headers CORS; el navegador reporta un “error de CORS” que no es realmente un problema de CORS. Pon CORS primero y el preflight obtiene sus headers y hace short-circuit, mientras que los requests reales siguen pasando por auth. La mayoría de bugs “misteriosos” de middleware vienen de una asunción de orden como esta.

Lo que funciona

  • Registra en el orden en que debe fluir el request: logging y correlation IDs primero (para que todo lo posterior quede trazado), luego CORS y headers de seguridad, luego auth, luego las rutas — el error handler en último lugar.
  • Fall fast: un middleware de auth que devuelve 401 evita que el handler se ejecute. Rechazar barato siempre gana a procesar caro.
  • No te tragues errores: en Express, envuelve el middleware async en try/catch y reenvía con next(err) — un rechazo async sin manejar salta completamente el error handler.
  • Sé stateless: datos de scope-request en variables a nivel de módulo se mezclan entre requests concurrentes. Adjunta a req (Express), request.state (FastAPI) o atributos del request (Spring).
  • Usa los middlewares del framework para lo aburrido: helmet y morgan en Express, CORSMiddleware y GZipMiddleware en FastAPI, los hooks de WebMvcConfigurer en Spring están todos probados — escribe middleware propio solo donde ellos se quedan cortos.
  • Inyecta, no construyas: en Spring y FastAPI, inyecta servicios en el interceptor en lugar de instanciarlos dentro — más barato y testeable.

Errores comunes

  • Olvidar next(): el request se queda colgado para siempre — sin response, sin error, solo un timeout. Sigue siendo el bug de middleware de Express más común con diferencia.
  • Error handler no está al final: si registras el middleware de error de cuatro argumentos antes de las rutas, simplemente nunca se dispara.
  • Mutar req con datos no confiables: asignar req.user desde query params o headers sin validar sobrescribe estado confiable con input del atacante.
  • Trabajo síncrono pesado en la cadena: el middleware corre en cada request — un pico de CPU bloqueante frena todos los demás requests del proceso.
  • Llamar next() dos veces: normalmente por una rama que olvida hacer return tras llamarlo una vez. En Express viejo crashea el proceso; en versiones nuevas produce responses corruptos.

Solución de Problemas

  • Los requests se cuelgan en Express: algún middleware aguas arriba no llama a next(). Bisecciona comentando líneas app.use, o añade un console.log de rastro por capa y mira dónde se corta.
  • Errores de CORS en el navegador: el preflight OPTIONS también necesita los headers — registra el middleware CORS antes del auth, o el preflight muere en un 401.
  • Auth corre pero la ruta nunca: te falta next() tras la verificación exitosa.
  • Middleware silencioso en algunas rutas: revisa el path de montaje — app.use('/api', mw) salta todo lo que esté fuera de /api.
  • El handler lanza error pero recibes una página de error HTML: el error nunca llegó a tu middleware — le falta el primer parámetro err, y las funciones de tres argumentos se omiten para errores.

Lecturas Recomendadas

Preguntas frecuentes

¿Qué hace exactamente next() en middleware de Express?

Pasa el control a la siguiente función de la cadena — el siguiente middleware registrado o el route handler. Si lo omites, el request se queda colgado para siempre. Si lo llamas con un argumento (next(err)), Express salta directamente al middleware de errores, ignorando todo lo que hay en medio.

¿Puede el middleware modificar el body del response?

En Express, no de forma limpia — el body puede estar ya transmitido para cuando el middleware lo ve. Puedes envolver res.send/res.json, pero es delicado. FastAPI y Spring facilitan el post-procesamiento; en cualquier caso, los headers y status codes son lo seguro de tocar.

¿Cuántas capas de middleware son demasiadas?

No hay un límite fijo, pero cada capa añade latencia a cada request. Si tu cadena supera la docena de elementos, algunos probablemente pertenecen al nivel de ruta. Profilea antes de cortar — un middleware de logging cuesta microsegundos; una verificación de token síncrona en cada request es lo caro.

¿La validación va en el middleware o en el handler?

Ambas funcionan; lo que importa es dónde aplica la regla. El middleware es correcto cuando la misma validación cubre muchas rutas ("todo body POST menor de 1MB, JSON válido"). La validación por ruta — "el user_id debe existir" — pertenece a una dependencia, un guard o el propio handler.

¿Por qué no se llama a mi error handler de Express?

Dos sospechosos habituales: está registrado antes de las rutas (debe ir al final), o le falta el parámetro err — Express reconoce el middleware de errores solo por su firma de cuatro argumentos.