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.
Visión General
Si alguna vez escribiste app.use(authenticate) en Express, registraste un Filter de servlet en web.xml, o agregaste middleware a un pipeline de ASP.NET Core, ya usaste este patrón. El Patrón Intercepting Filter compone responsabilidades transversales — autenticación, logging, compresión, validación — en una cadena de filtros reutilizables que interceptan las peticiones antes de que lleguen al manejador, y las respuestas en el camino de vuelta.
Cada filtro hace una sola cosa y delega al siguiente a través del objeto de cadena. El destino final (un servlet, controlador o manejador) ejecuta la lógica de negocio. Como los filtros se registran de forma declarativa, puedes agregarlos, quitarlos o reordenarlos sin tocar el manejador — razón por la que todos los frameworks web populares adoptaron este patrón bajo el nombre de “middleware”.
Cuándo Usarlo
Usa el Patrón Intercepting Filter cuando:
- Las responsabilidades transversales (autenticación, logging, caché) deben reutilizarse entre varios manejadores
- El procesamiento de peticiones y respuestas necesita etapas de preprocesamiento o postprocesamiento
- Necesitas un pipeline configurable donde los filtros puedan agregarse o reordenarse
- Varios manejadores comparten el mismo conjunto de responsabilidades transversales
Para casos más simples — un solo manejador que necesita una capa de envoltura — el Patrón Decorator es suficiente.
Cuándo No Usarlo
- Un solo manejador con responsabilidades únicas de un solo uso (un decorador simple basta)
- Rutas críticas de latencia donde el coste por filtro es inaceptable
- Cuando las dependencias de orden entre filtros se vuelven tan complejas que razonar sobre la cadena es un problema en sí mismo
- Scripts pequeños donde unas pocas llamadas directas son más claras que un pipeline
Solución
Python
from abc import ABC, abstractmethod
from typing import Dict, Any
from dataclasses import dataclass, field
@dataclass
class HttpRequest:
path: str
headers: Dict[str, str]
body: Any = None
user: Any = None
authenticated: bool = False
@dataclass
class HttpResponse:
status: int = 200
headers: Dict[str, str] = field(default_factory=dict)
body: Any = None
class Filter(ABC):
"""Filtro base que puede encadenar al siguiente filtro."""
def __init__(self, next_filter: 'Filter' = None):
self.next_filter = next_filter
@abstractmethod
def do_filter(self, request: HttpRequest, response: HttpResponse):
pass
def _invoke_next(self, request: HttpRequest, response: HttpResponse):
if self.next_filter:
self.next_filter.do_filter(request, response)
class AuthenticationFilter(Filter):
"""Verifica si la petición tiene un token válido."""
def do_filter(self, request: HttpRequest, response: HttpResponse):
token = request.headers.get("Authorization")
if token and token.startswith("Bearer "):
request.user = "authenticated_user"
request.authenticated = True
self._invoke_next(request, response)
else:
# Cortocircuito: nunca llama a _invoke_next
response.status = 401
response.body = {"error": "Unauthorized"}
class LoggingFilter(Filter):
"""Registra los detalles de la petición antes y después de procesarla."""
def do_filter(self, request: HttpRequest, response: HttpResponse):
print(f"[LOG] Petición a {request.path}")
self._invoke_next(request, response)
print(f"[LOG] Estado de respuesta: {response.status}")
class CompressionFilter(Filter):
"""Comprime el cuerpo de la respuesta si el cliente lo acepta."""
def do_filter(self, request: HttpRequest, response: HttpResponse):
self._invoke_next(request, response)
if "gzip" in request.headers.get("Accept-Encoding", ""):
response.headers["Content-Encoding"] = "gzip"
print("[COMPRESS] Respuesta comprimida")
class TargetHandler(Filter):
"""El manejador final que procesa la petición principal."""
def __init__(self):
super().__init__(None)
def do_filter(self, request: HttpRequest, response: HttpResponse):
if response.status == 200:
response.body = {"message": f"Hola, {request.user or 'invitado'}!"}
class FilterChain:
"""Construye y ejecuta el pipeline de filtros."""
def __init__(self):
self.filters: list[type[Filter]] = []
def add_filter(self, filter_cls):
self.filters.append(filter_cls)
return self
def execute(self, request: HttpRequest) -> HttpResponse:
# Construye la cadena de la cola a la cabeza
target = TargetHandler()
current = target
for filter_cls in reversed(self.filters):
new_filter = filter_cls()
new_filter.next_filter = current
current = new_filter
response = HttpResponse()
current.do_filter(request, response)
return response
# Uso
chain = FilterChain()
chain.add_filter(AuthenticationFilter) \
.add_filter(LoggingFilter) \
.add_filter(CompressionFilter)
request = HttpRequest(
path="/api/hello",
headers={"Authorization": "Bearer abc123", "Accept-Encoding": "gzip"}
)
response = chain.execute(request)
print(f"Resultado: {response.status} - {response.body}")
Java
import java.util.*;
class HttpRequest {
private final String path;
private final Map<String, String> headers;
private String user;
private boolean authenticated;
public HttpRequest(String path, Map<String, String> headers) {
this.path = path; this.headers = headers;
}
public String getPath() { return path; }
public Map<String, String> getHeaders() { return headers; }
public String getUser() { return user; }
public void setUser(String user) { this.user = user; }
public boolean isAuthenticated() { return authenticated; }
public void setAuthenticated(boolean auth) { this.authenticated = auth; }
}
class HttpResponse {
private int status = 200;
private final Map<String, String> headers = new HashMap<>();
private Object body;
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
public Map<String, String> getHeaders() { return headers; }
public Object getBody() { return body; }
public void setBody(Object body) { this.body = body; }
}
interface Filter {
void doFilter(HttpRequest request, HttpResponse response, FilterChain chain);
}
class FilterChain {
private final List<Filter> filters = new ArrayList<>();
private int currentIndex = 0;
public void addFilter(Filter filter) { filters.add(filter); }
public void doFilter(HttpRequest request, HttpResponse response) {
if (currentIndex < filters.size()) {
Filter filter = filters.get(currentIndex++);
filter.doFilter(request, response, this);
}
}
// Punto de entrada: reinicia el cursor para servir la siguiente petición.
public void execute(HttpRequest request, HttpResponse response) {
currentIndex = 0;
doFilter(request, response);
}
}
class AuthenticationFilter implements Filter {
public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) {
String token = request.getHeaders().get("Authorization");
if (token != null && token.startsWith("Bearer ")) {
request.setUser("authenticated_user");
request.setAuthenticated(true);
chain.doFilter(request, response);
} else {
response.setStatus(401);
response.setBody(Map.of("error", "Unauthorized"));
}
}
}
class LoggingFilter implements Filter {
public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) {
System.out.println("[LOG] Petición a " + request.getPath());
chain.doFilter(request, response);
System.out.println("[LOG] Estado de respuesta: " + response.getStatus());
}
}
class CompressionFilter implements Filter {
public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) {
chain.doFilter(request, response);
String encoding = request.getHeaders().getOrDefault("Accept-Encoding", "");
if (encoding.contains("gzip")) {
response.getHeaders().put("Content-Encoding", "gzip");
System.out.println("[COMPRESS] Respuesta comprimida");
}
}
}
class TargetHandler implements Filter {
public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) {
if (response.getStatus() == 200) {
response.setBody("Hola, " + (request.getUser() != null ? request.getUser() : "invitado") + "!");
}
}
}
// Uso
HttpRequest request = new HttpRequest("/api/hello", Map.of(
"Authorization", "Bearer abc123",
"Accept-Encoding", "gzip"
));
HttpResponse response = new HttpResponse();
FilterChain chain = new FilterChain();
chain.addFilter(new AuthenticationFilter());
chain.addFilter(new LoggingFilter());
chain.addFilter(new CompressionFilter());
chain.addFilter(new TargetHandler());
chain.execute(request, response);
System.out.println("Resultado: " + response.getStatus() + " - " + response.getBody());
JavaScript
class HttpRequest {
constructor(path, headers) {
this.path = path;
this.headers = headers;
this.user = null;
this.authenticated = false;
}
}
class HttpResponse {
constructor() {
this.status = 200;
this.headers = {};
this.body = null;
}
}
class FilterChain {
constructor() {
this.filters = [];
this.index = 0;
}
addFilter(filter) {
this.filters.push(filter);
return this;
}
doFilter(request, response) {
if (this.index < this.filters.length) {
const filter = this.filters[this.index++];
filter.doFilter(request, response, this);
}
}
// Punto de entrada: reinicia el cursor para poder reutilizar la cadena.
execute(request, response) {
this.index = 0;
this.doFilter(request, response);
}
}
class AuthenticationFilter {
doFilter(request, response, chain) {
const token = request.headers['Authorization'];
if (token && token.startsWith('Bearer ')) {
request.user = 'authenticated_user';
request.authenticated = true;
chain.doFilter(request, response);
} else {
response.status = 401;
response.body = { error: 'Unauthorized' };
}
}
}
class LoggingFilter {
doFilter(request, response, chain) {
console.log(`[LOG] Petición a ${request.path}`);
chain.doFilter(request, response);
console.log(`[LOG] Estado de respuesta: ${response.status}`);
}
}
class CompressionFilter {
doFilter(request, response, chain) {
chain.doFilter(request, response);
const encoding = request.headers['Accept-Encoding'] || '';
if (encoding.includes('gzip')) {
response.headers['Content-Encoding'] = 'gzip';
console.log('[COMPRESS] Respuesta comprimida');
}
}
}
class TargetHandler {
doFilter(request, response, chain) {
if (response.status === 200) {
response.body = { message: `Hola, ${request.user || 'invitado'}!` };
}
}
}
// Uso
const request = new HttpRequest('/api/hello', {
Authorization: 'Bearer abc123',
'Accept-Encoding': 'gzip',
});
const response = new HttpResponse();
const chain = new FilterChain();
chain.addFilter(new AuthenticationFilter())
.addFilter(new LoggingFilter())
.addFilter(new CompressionFilter())
.addFilter(new TargetHandler());
chain.execute(request, response);
console.log('Resultado:', response.status, response.body);
Cómo Funciona
La petición entra por el primer filtro. Cada filtro tiene tres oportunidades de actuar: antes de delegar (preprocesamiento), eligiendo no delegar (cortocircuito), y después de que el delegado retorna (postprocesamiento). Fíjate en que LoggingFilter registra en ambos sentidos y CompressionFilter solo actúa en el camino de vuelta — es la pila de llamadas desenrollándose a través de la cadena lo que hace gratis los filtros de respuesta.
El orden es un contrato, no una preferencia. La autenticación debe correr antes que la autorización, y esta antes que la caché — una caché colocada antes de la autenticación servirá alegremente respuestas no autorizadas desde memoria. Declara el orden en un solo lugar (código de registro, archivo de configuración) y trata los cambios como cambios de API: pueden romper el comportamiento silenciosamente.
El manejo de errores necesita una estrategia desde el inicio. Si un filtro lanza una excepción, ¿la cadena la propaga, la convierte en respuesta de error, o la registra y continúa? Los frameworks suelen instalar una etapa de manejo de errores primero en la cadena para que capture todo lo que venga después. Si lo implementas a mano, envuelve todo el chain.execute() en un único try/catch que convierta excepciones en respuestas de error — cualquier otra opción deja escapar trazas de pila hacia la salida del cliente.
El async cambia las reglas. El truco de “actuar al retornar” funciona porque las llamadas son síncronas. En pipelines asíncronos (next() de Express, await _next(context) de ASP.NET), el código “de después” corre dentro de una continuación — olvida el await/next() y tus filtros de respuesta nunca se ejecutan, o peor, se ejecutan cuando la respuesta ya fue enviada. Es uno de los bugs de producción más comunes en pipelines de middleware.
Cada filtro suma coste. Cada etapa agrega un dispatch, y en rutas calientes eso se acumula. Diez filtros pequeños cuestan más que dos consolidados haciendo el mismo trabajo — fusiona responsabilidades estrechamente relacionadas (un ObservabilityFilter en lugar de filtros separados de logging + métricas + tracing) cuando la cadena se alarga.
Variantes
| Variante | Mecanismo | Caso de Uso |
|---|---|---|
| Cadena lineal | Cada filtro llama al siguiente | Middleware web estándar |
| Decoradores | Envoltura de objetos | Composición funcional, un solo manejador |
| Dirigido por eventos | Filtros suscritos a eventos del ciclo | Sistemas altamente desacoplados |
| Pipeline DAG | Grafo acíclico dirigido de etapas | Procesamiento complejo de datos (ETL, ML) |
Intercepting Filter vs. Patrones Relacionados
| Intercepting Filter | Chain of Responsibility | Decorator | |
|---|---|---|---|
| Objetivo | Pre/postprocesar alrededor de un destino | Encontrar un manejador para la petición | Agregar comportamiento a un objeto |
| ¿Quién procesa? | Todos los filtros + un destino | Exactamente un manejador (el primero que acepta) | El objeto envuelto |
| Camino de respuesta | Se desenrolla por todos los filtros | Ninguno — el manejador cierra la cadena | Se desenrolla por las envolturas |
| Uso típico | Middleware web, filtros servlet | Validación, enrutado de eventos, dispatch de comandos | Streams de I/O, envoltura de UI |
Regla práctica: si cada etapa debe ver la petición y un destino fijo hace el trabajo, es un Intercepting Filter. Si las etapas compiten por ser el manejador, es una Chain of Responsibility.
Qué Funciona
- Ordena los filtros deliberadamente. Autenticación antes de autorización antes de caché; documenta la dependencia si no es obvia.
- Mantén los filtros sin estado. Una instancia sirve todos los hilos — los campos de instancia son condiciones de carrera esperando ocurrir.
- Cortocircuita ante fallos. Un fallo de autenticación detiene la cadena; no anota y continúa.
- Usa ambos sentidos. El postprocesamiento en el camino de vuelta es la ventaja gratuita del patrón — compresión, cabeceras, medición de tiempo.
- Registra el destino dentro de la cadena. El manejador debe ser la última etapa registrada, no un
elseoculto en el constructor de la cadena.
Código complementario: El repo companion de intercepting-filter-pattern tiene una cadena de filtros ejecutable en Python y JavaScript, más un ejemplo de middleware de Express con tests.
Errores Comunes
- Caché antes de autenticación. La caché sirve respuestas no autorizadas desde memoria — revisa el orden primero cuando aparezca un bug de seguridad.
- Estado de instancia en filtros. Un mismo objeto filtro sirve todas las peticiones concurrentes; el estado por petición va en el objeto request/context, no en
this. - Tragar excepciones. Una excepción capturada e ignorada a mitad de cadena produce una respuesta a medias que parece exitosa.
- Proliferación de filtros. Veinte filtros diminutos agregan coste de dispatch y vuelven el orden inmanejable — consolida responsabilidades relacionadas.
- Olvidar
next()/await. En pipelines asíncronos el código de postprocesamiento nunca se ejecuta silenciosamente, o lo hace demasiado tarde.
Ejemplos del Mundo Real
Filtros Servlet (Java EE)
La interfaz javax.servlet.Filter de Java define doFilter(request, response, chain). Los filtros se configuran en web.xml o con anotaciones @WebFilter, y el contenedor garantiza el orden según el registro.
Middleware de Express.js
Las funciones middleware de Express son Intercepting Filters: app.use((req, res, next) => { ... next() }). La llamada next() es la delegación de cadena — y olvidarla es el bug clásico de Express donde las peticiones se cuelgan para siempre. Consulta la receta de patrones de middleware en Express para composición, manejo de errores y cadenas con alcance de ruta.
Middleware de ASP.NET Core
ASP.NET Core construye el pipeline con app.Use() y app.Map(), donde cada middleware recibe un RequestDelegate para la siguiente etapa. app.Run() registra un middleware terminal — el “destino” de la cadena.
El compañero Front Controller
Intercepting Filter suele situarse delante de un Front Controller: los filtros resuelven el trabajo transversal y después el front controller despacha al manejador correcto. Java EE los describió como pareja exactamente por esta razón.
Lectura Adicional
- Core J2EE Patterns — Intercepting Filter — la entrada original del catálogo de patrones (Oracle).
- Express — Writing middleware — el modelo de middleware en el que se convirtió este patrón.
- ASP.NET Core Middleware — la guía de pipeline de Microsoft.
- Para el patrón compañero J2EE, consulta el Patrón Business Delegate y el Patrón Context Object.
Preguntas frecuentes
¿Cuál es la diferencia entre Intercepting Filter y Chain of Responsibility?
Chain of Responsibility pasa la petición entre manejadores candidatos hasta que uno la acepta — el objetivo es encontrar el procesador. Intercepting Filter tiene exactamente un destino; cada filtro solo pre/postprocesa a su alrededor. Si todas las etapas ven la petición, es una cadena de filtros; si compiten por manejarla, es CoR.
¿Cómo manejo las excepciones en una cadena de filtros?
Instala un filtro de manejo de errores al inicio de la cadena para que su try/catch envuelva todo lo posterior, o envuelve el chain.execute() completo en un único try/catch que produzca la respuesta de error. Nunca permitas que un filtro trague una excepción silenciosamente y continúe — obtienes respuestas a medias que parecen exitosas.
¿Pueden los filtros modificar la respuesta en el camino de vuelta?
Sí — es la ventaja incorporada del patrón. El código después de chain.doFilter() corre mientras la pila de llamadas se desenrolla, que es como funcionan la compresión, las cabeceras de respuesta y los filtros de medición. En pipelines asíncronos ese código "de después" vive en una continuación; si falta un await, nunca se ejecuta o se ejecuta tarde.
¿En qué orden debería registrar los filtros?
Seguridad primero (autenticación → autorización), luego normalización de la petición (validación, rate limiting), luego caché, luego observabilidad y finalmente el destino. La regla general: un filtro nunca debería necesitar algo que produce un filtro posterior. Si dos filtros se disputan el orden, probablemente uno de ellos está haciendo dos trabajos.
¿Es el middleware de Express lo mismo que este patrón?
Sí — app.use((req, res, next) => { ...; next() }) es un Intercepting Filter donde next() es la delegación de cadena. Express añade cadenas con alcance de ruta (app.use('/admin', auth)) y middleware de manejo de errores (funciones de cuatro parámetros), pero el mecanismo es idéntico.
Recursos Relacionados
Patrón Chain of Responsibility
Pasa solicitudes a lo largo de una cadena de manejadores hasta que uno la procese. Un patrón de comportamiento para desacoplar emisores y receptores.
PatternPatrón Decorator
Añade nueva funcionalidad a objetos dinámicamente envolviéndolos. Patrón de diseño estructural para extensión flexible de comportamiento.
PatternPatrón Proxy
Proporciona un sustituto o marcador de posición para otro objeto para controlar el acceso a él. Un patrón estructural para control de acceso, carga perezosa y logging.
PatternPatrón Business Delegate
Reduce el acoplamiento entre capas de presentación y negocio introduciendo un intermediario que maneja lookup, creación e invocación de servicios de negocio.
PatternPatrón Context Object: Ejemplos
Aprende el patrón Context Object para reducir el bloat de parámetros. Ejemplos en Python, Java y JavaScript para contextos de petición y DI.
PatternPatrón Front Controller
Enruta todas las peticiones entrantes a través de un único handler que despacha al comando de página apropiado, centralizando el procesamiento de requests y seguridad.