Chain of Responsibility en Middleware de Peticiones
Pasa peticiones a lo largo de una cadena de handlers donde cada uno decide si procesa la petición o la delega al siguiente eslabón de la pipeline
El patrón Chain of Responsibility pasa una petición a lo largo de una cadena de handlers, donde cada uno decide si actúa sobre ella o la reenvía. Aplicado al procesamiento de peticiones se convierte en la pipeline de middleware: autenticación, rate limiting, validación, logging — cada paso es un eslabón, y la petición los recorre en orden hasta llegar al handler del endpoint o hasta que un eslabón la cortocircuita.
Cuándo Usarlo
- Una petición HTTP debe cruzar varias preocupaciones (auth, rate limits, validación, logging) y quieres cada una aislada y reordenable
- El conjunto de comprobaciones cambia por ruta o entorno — un endpoint público omite auth, un webhook omite la validación de esquema — así que la cadena debe componerse dinámicamente
- Los pasos necesitan poder detener el procesamiento antes de tiempo: rechazar un token inválido, servir una respuesta en caché, abortar ante un body malformado
- Quieres añadir o quitar pasos sin tocar el código que invoca la pipeline
No uses una cadena cuando la pipeline tiene uno o dos pasos fijos, cuando un if/else simple se lee más claro, o cuando los pasos deben ejecutarse en paralelo — una cadena es secuencial por definición.
Problema
Una petición HTTP necesita autenticación, rate limiting, validación de entrada y logging. Si hardcodeas esa secuencia dentro del router, cada ruta hereda un guion rígido: salta una comprobación y obtienes una inconsistencia que nadie detecta hasta producción. La cadena convierte cada preocupación en un handler de responsabilidad única que puede insertarse, eliminarse o reordenarse sin reescribir la pipeline.
Solución
// chain/Handler.ts
interface RequestContext {
headers: Record<string, string>;
body: unknown;
path: string;
method: string;
user?: { id: string; roles: string[] };
}
type NextFunction = () => void;
abstract class MiddlewareHandler {
protected next: MiddlewareHandler | null = null;
setNext(handler: MiddlewareHandler): MiddlewareHandler {
this.next = handler;
return handler;
}
handle(req: RequestContext, next: NextFunction): void {
if (this.canHandle(req)) {
this.process(req, () => {
if (this.next) {
this.next.handle(req, next);
} else {
next();
}
});
} else if (this.next) {
this.next.handle(req, next);
} else {
next();
}
}
protected abstract canHandle(req: RequestContext): boolean;
protected abstract process(req: RequestContext, next: NextFunction): void;
}
// Handlers concretos
class AuthMiddleware extends MiddlewareHandler {
protected canHandle(): boolean {
return true; // Siempre comprobar auth
}
protected process(req: RequestContext, next: NextFunction): void {
const token = req.headers['authorization']?.replace('Bearer ', '');
if (!token) {
throw new Error('Unauthorized');
}
// Verificar token
req.user = { id: 'user123', roles: ['user'] };
next();
}
}
class RateLimitMiddleware extends MiddlewareHandler {
private requests = new Map<string, number[]>();
private readonly windowMs = 60000;
private readonly maxRequests = 100;
protected canHandle(): boolean {
return true;
}
protected process(req: RequestContext, next: NextFunction): void {
const clientId = req.headers['x-client-id'] || req.user?.id || 'anonymous';
const now = Date.now();
const window = this.requests.get(clientId) || [];
const recent = window.filter(t => now - t < this.windowMs);
if (recent.length >= this.maxRequests) {
throw new Error('Rate limit exceeded');
}
recent.push(now);
this.requests.set(clientId, recent);
next();
}
}
class ValidationMiddleware extends MiddlewareHandler {
protected canHandle(req: RequestContext): boolean {
return req.method === 'POST' || req.method === 'PUT';
}
protected process(req: RequestContext, next: NextFunction): void {
if (!req.body || typeof req.body !== 'object') {
throw new Error('Invalid request body');
}
next();
}
}
class LoggingMiddleware extends MiddlewareHandler {
protected canHandle(): boolean {
return true;
}
protected process(req: RequestContext, next: NextFunction): void {
console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
next();
}
}
// Construir la cadena
const auth = new AuthMiddleware();
const rateLimit = new RateLimitMiddleware();
const validation = new ValidationMiddleware();
const logging = new LoggingMiddleware();
auth.setNext(rateLimit).setNext(validation).setNext(logging);
// Uso
function handleRequest(req: RequestContext): void {
auth.handle(req, () => {
console.log('La petición llegó al handler final');
});
}
Cómo Funciona
- MiddlewareHandler declara el contrato:
canHandledecide si el eslabón aplica a esta petición,processhace el trabajo,setNextconecta la cadena. - Los handlers concretos (auth, rate limit, validación, logging) ejecutan su lógica y llaman a
next(), o saltan directamente al eslabón siguiente. - Cualquier eslabón puede cortocircuitar — lanzar un error, escribir la respuesta, o simplemente no llamar a
next()— y nada de lo que está aguas abajo se ejecuta.
El orden es el contrato, y se queda implícito. auth.setNext(rateLimit).setNext(validation) significa que auth corre primero; intercambia dos eslabones y podrías validar un body antes de autenticar al llamante. Documenta el orden previsto o construye la cadena en un único lugar evidente.
Variación: compose() al estilo Express
// Estilo Express con funciones en lugar de clases
type Middleware = (req: RequestContext, next: NextFunction) => void;
function compose(middlewares: Middleware[]): Middleware {
return (req, finalNext) => {
let index = -1;
function dispatch(i: number): void {
if (i <= index) throw new Error('next() llamado varias veces');
index = i;
const fn = i < middlewares.length ? middlewares[i] : finalNext;
if (!fn) return;
fn(req, () => dispatch(i + 1));
}
dispatch(0);
};
}
const pipeline = compose([
(req, next) => { console.log('Auth'); next(); },
(req, next) => { console.log('Rate limit'); next(); },
(req, next) => { console.log('Log'); next(); },
]);
Este compose es esencialmente lo que hace koa-compose de Koa: dispatch(i) ejecuta el middleware i, y cada next() reanuda el índice siguiente. La guarda i <= index lanza error cuando un middleware llama a next() dos veces — el clásico bug del “double next”.
- Código complementario: el repo companion de chain-of-responsibility-middleware empaqueta estos ejemplos como archivos TypeScript y JavaScript ejecutables.
Variación: Servlet Filter (Java)
La misma forma viene de serie en la API Servlet de Java — FilterChain.doFilter() es literalmente el next() del patrón, y no llamarlo cortocircuita la cadena:
public class AuthFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest http = (HttpServletRequest) req;
String token = http.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
((HttpServletResponse) res).sendError(401); // cortocircuito
return;
}
req.setAttribute("user", token.substring(7)); // estado acotado a la petición
chain.doFilter(req, res); // next()
}
}
Los filtros se declaran en web.xml o con @WebFilter y el contenedor los ordena — el contrato de orden vive en configuración, no en el código, igual que en la cadena de clases de arriba.
Middleware en Frameworks Reales
El patrón ya vive dentro de la mayoría de los stacks HTTP; normalmente configuras una cadena en lugar de construirla:
- Express / Connect:
app.use(fn)añade eslabones a una cadena lineal;next(err)salta directo al middleware de errores al final. - Koa: el modelo “cebolla” —
koa-composepermite ejecutar código antes y después deawait next(), así que un solo middleware puede medir toda la cadena aguas abajo. - ASP.NET Core:
Usepuede llamar anext,Runtermina la pipeline, yMapla ramifica por path — cadena de responsabilidad más routing. - Django: hooks
process_request/process_responsealrededor de la vista, declarados una vez en el ajusteMIDDLEWARE. - Java Servlets:
FilterChain.doFilter()es literalmente elnext()del patrón.
Las diferencias importan: Express reenvía los errores a un canal dedicado en lugar de lanzarlos, Koa espera a next() de modo que la cadena se desenrolla de vuelta por las mismas funciones, y ASP.NET expone ramificación en lugar de una sola línea.
Consecuencias y Trade-offs
- Pasos desacoplados, orden acoplado. Cada eslabón es independiente, pero el comportamiento de la cadena depende de la secuencia — auth antes que rate limiting solo cuenta tráfico autenticado; logging primero captura también las peticiones rechazadas. Elige el orden deliberadamente.
- Depurar una cadena larga es más difícil que depurar una función. Una petición que “desaparece” suele significar que algún middleware olvidó
next(). Los IDs de correlación y un middleware de timing por eslabón se pagan solos enseguida. - La semántica de fallo cambia por eslabón. Una excepción a mitad de cadena deja la petición en estado indefinido; decide si un rate limiter caído debe fallar cerrado (rechazar) o fallar abierto (registrar y continuar).
- El coste secuencial se acumula. Diez middleware de 1 ms cada uno son 10 ms de latencia extra antes de que corra tu handler. Mantén baratos los eslabones del camino caliente y empuja el trabajo pesado detrás de cortocircuitos o cachés.
- Fácil de testear por unidad, débil de extremo a extremo. Cada eslabón se prueba bien aislado, pero los bugs de orden solo aparecen cuando corre la cadena real — conserva al menos un test de integración sobre la pipeline completa.
Consideraciones de Producción
- Mantén los handlers sin estado o guarda el estado en el objeto de petición/contexto. El rate limiter con
Mapen memoria del ejemplo funciona en un proceso, pero reparte el presupuesto entre réplicas — muévelo a Redis cuando escales horizontalmente. - Mezclar middleware síncrono y asíncrono es la primera causa de peticiones colgadas: un handler
asyncque nunca llama anext()y nunca lanza deja el socket abierto hasta que un middleware de timeout, o el cliente, se rinde. - Falla en voz alta en los eslabones de seguridad (auth, verificación de firma), falla en silencio en los de observabilidad (métricas, logging) — un endpoint de métricas muerto nunca debería devolver un 500 en tu API.
- Instrumenta la propia cadena: emite timing por middleware para ver qué eslabón se queda con la latencia.
Errores Comunes
- Olvidar
next()en un handler async, así que la petición se cuelga en lugar de fallar - Construir una cadena circular que hace bucles infinitos entre dos handlers
- Guardar estado por petición en campos compartidos del middleware que se filtran entre peticiones concurrentes
- Tragar errores dentro de un eslabón en lugar de reenviarlos al handler de errores del final
- Ejecutar trabajo costoso (consultas a la DB, llamadas externas) al inicio de la cadena sin caché ni cortocircuito
- Terminar la cadena sin handler por defecto, así que las peticiones no encajadas caen en silencio
- Dejar que un middleware absorba varias preocupaciones — auth y validación y logging en un solo eslabón
- Poner la validación antes que la autenticación y filtrar a llamantes anónimos qué endpoints existen
- Usar una cadena donde bastaría un
ifde dos líneas
Buenas Prácticas
- Una preocupación por eslabón. Si un middleware hace dos cosas, divídelo — quieres poder reordenar, omitir y probar cada preocupación de forma independiente.
- Resuelve siempre la rama. Todo camino a través de un middleware debe llamar a
next(), escribir una respuesta o reenviar un error. Una revisión de “¿este handler siempre resuelve la petición?” bien vale una regla de lint. - Pon los rechazos baratos primero. Las comprobaciones de firma y tamaño van antes que auth y las consultas a la DB — rechaza la basura antes de que te cueste una query.
- Trata el orden como configuración. Construye la cadena en un solo archivo, comenta por qué importa el orden y evita llamadas
use()dispersas por el código. - Propaga los errores hasta el final de la cadena.
next(err)— o lanzar en código async — debería aterrizar en un único middleware de errores que mapea errores a respuestas, nunca en formatos de error improvisados por eslabón. - Acota el estado a la petición. Todo lo que es por petición vive en el objeto de contexto, no en
this. - Mide cada eslabón en producción. Un middleware que mide la latencia aguas abajo es, él mismo, un buen primer eslabón.
Véase También
Preguntas frecuentes
¿En qué se diferencia del patrón Decorator?
Decorator envuelve un objeto para añadir comportamiento y todas las capas se ejecutan; una cadena de handlers pasa la petición hasta que un eslabón la procesa o la cortocircuita — los eslabones pueden saltarse por completo.
¿El middleware de Express es un Chain of Responsibility?
En la práctica, sí: app.use() registra handlers que llaman a next() para reenviar la petición. Express añade un segundo canal — next(err) salta directo al middleware de errores — que la cadena GoF clásica no modela.
¿Puedo añadir o reordenar middleware en tiempo de ejecución?
Sí, si construyes la cadena a través de un registro o una API estilo use() en lugar de llamadas setNext hardcodeadas. La mayoría de apps solo necesitan configuración al arrancar; el reordenamiento dinámico es raro y hace los bugs de orden más difíciles de rastrear.
¿Qué pasa si un middleware olvida llamar a next()?
La petición se cuelga. Nada aguas abajo se ejecuta, no se escribe respuesta y el cliente espera hasta que salta un timeout. Es el bug de middleware más común — pon un middleware de timeout cerca del inicio para que aflore como error en lugar de un socket parado.
¿El middleware debería ser async o síncrono?
El que pida tu framework. En código async, haz siempre await del trabajo aguas abajo antes de retornar — un next() lanzado sin esperar deja que la petición termine antes de que la validación o el logging acaben. Mezclar next() síncrono con handlers async es de donde salen las peticiones colgadas y los rechazos no manejados.
¿Cómo comparto datos entre middleware?
Adjúntalos al objeto de petición/contexto (req.user, req.locals, un mapa de contexto), nunca a la instancia del middleware — una instancia sirve a todas las peticiones concurrentes. Los eslabones aguas abajo pueden leerlos, pero no deberían depender de que un eslabón concreto los haya producido.
¿Un middleware que falla debería rechazar la petición o continuar?
Depende del trabajo del eslabón. Los de seguridad fallan cerrados — auth, comprobación de firma, CSRF. Los de observabilidad fallan abiertos: si métricas o logging se rompen, regístralo y llama a next() — un dashboard muerto no debería tumbar tu API.
¿Cómo decido el orden del middleware?
Los rechazos baratos primero (tamaño de petición, firma), después identidad (auth), después comprobaciones caras (rate limits, validación), después enriquecimiento, y al final el endpoint. Inviértelo y pagas el precio completo por peticiones que habrías rechazado en microsegundos.
¿Cómo pruebo una cadena de middleware?
Prueba cada eslabón por unidad con un next() simulado y verifica si lo llamó, escribió una respuesta o reenvió un error. Conserva un test de integración que ejecute la cadena completa — los bugs de orden e interacción solo aparecen cuando corren eslabones reales juntos.
¿Cuándo es este patrón excesivo?
Con uno o dos pasos fijos, un if o una llamada a función es más claro. La cadena se gana su complejidad cuando los pasos son muchos, condicionales, configurables dinámicamente o necesitan cortocircuito independiente.
Recursos Relacionados
Intercepting Filter: Filtros de Petición Reutilizables
Compón responsabilidades transversales en una cadena de filtros reutilizables que interceptan peticiones y respuestas, permitiendo lógica de preprocesamiento y postprocesamiento compartida.
PatternPatrón Pipes and Filters: Pipelines de Datos Componibles
Encadena filtros independientes con pipes para construir pipelines de transformación de datos reutilizables y componibles.
PatternPatrón Decorator en pipelines HTTP
Usa el patrón Decorator para componer aspectos transversales como logging, métricas y reintentos en pipelines de peticiones HTTP sin modificar la lógica central
PatternPatrón Circuit Breaker en Python, JavaScript y Java
Previene fallos en cascada deteniendo solicitudes a servicios que están fallando. Un patrón arquitectural para sistemas distribuidos resilientes.
RecipeLimitación de Tasa (Rate Limiting)
Cómo implementar rate limiting en APIs usando token bucket, sliding window y fixed window en Python, JavaScript y Java.