StackPractices
intermediate Por Mathias Paulenko

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

flowchart diagram: Cliente

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

VarianteMecanismoCaso de Uso
Cadena linealCada filtro llama al siguienteMiddleware web estándar
DecoradoresEnvoltura de objetosComposición funcional, un solo manejador
Dirigido por eventosFiltros suscritos a eventos del cicloSistemas altamente desacoplados
Pipeline DAGGrafo acíclico dirigido de etapasProcesamiento complejo de datos (ETL, ML)

Intercepting Filter vs. Patrones Relacionados

Intercepting FilterChain of ResponsibilityDecorator
ObjetivoPre/postprocesar alrededor de un destinoEncontrar un manejador para la peticiónAgregar comportamiento a un objeto
¿Quién procesa?Todos los filtros + un destinoExactamente un manejador (el primero que acepta)El objeto envuelto
Camino de respuestaSe desenrolla por todos los filtrosNinguno — el manejador cierra la cadenaSe desenrolla por las envolturas
Uso típicoMiddleware web, filtros servletValidación, enrutado de eventos, dispatch de comandosStreams 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 else oculto 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

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.