Todos los middleware implementan la interfaz funcional Middleware:
@FunctionalInterface
public interface Middleware {
HttpResponse run(HttpRequest request, MiddlewareChain chain);
}Se registran mediante server.use(middleware) y se ejecutan en orden de registro.
Archivo: miniJWS-core/src/main/java/.../middleware/AccessLogMiddleware.java
Logging en formato Apache Common Log con E/S asíncrona.
Hilo de Petición Hilo Trabajador (daemon)
│ │
├── formatLogLine() │
├── queue.offer(line) ──────────►├── queue.take() (bloquea)
│ ├── writer.println(line)
│ ├── writer.flush()
│ └── bucle
└── devuelve respuesta
| Constructor | Destino de Salida |
|---|---|
AccessLogMiddleware() |
System.out (envuelto en PrintWriter) |
AccessLogMiddleware(String filePath) |
Archivo (append, UTF-8) |
AccessLogMiddleware(PrintWriter writer) |
Escritor personalizado |
127.0.0.1 - - [13/Jun/2026:14:30:00 +0000] "GET /hello HTTP/1.1" 200 13 (2ms)
Campos: IP remota (respeta X-Forwarded-For), marca temporal, línea de petición, código de estado, tamaño del cuerpo, ms transcurridos.
BlockingQueue<String>(capacidad 16_384) desacopla el manejo de peticiones de la E/S de logqueue.offer()nunca bloquea (devuelve false si está llena, la línea se descarta)- El trabajador es un hilo daemon (no impide la salida de la JVM)
- El hook de apagado drena las entradas restantes con
flushRemaining()
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
worker.interrupt();
worker.join(2_000); // espera hasta 2s para vaciar
flushRemaining(writer);
}));Archivo: miniJWS-core/src/main/java/.../middleware/CorsMiddleware.java
Implementación de CORS (Cross-Origin Resource Sharing).
| Método | Por Defecto | Descripción |
|---|---|---|
allowOrigin(String) |
* |
Origen permitido |
allowMethods(String...) |
GET, POST, PUT, DELETE, OPTIONS, PATCH | Métodos permitidos |
allowHeaders(String...) |
Content-Type, Authorization | Cabeceras permitidas |
allowCredentials(boolean) |
false |
Si enviar credenciales |
maxAge(int) |
-1 |
Duración de caché preflight |
- Sin cabecera Origin → saltar (pasar al siguiente middleware)
- Petición OPTIONS → devolver 204 con cabeceras CORS (preflight)
- Petición normal → llamar
chain.next(), añadir cabeceras CORS a la respuesta
La especificación CORS prohíbe Access-Control-Allow-Origin: * cuando Access-Control-Allow-Credentials: true. Cuando se llama a allowCredentials(true) con allowOrigin("*"):
allowCredentials(true)lanzaIllegalStateExceptioninmediatamente (fail-fast)- Solución: usar
allowOrigin("https://dominio.especifico")con credenciales, o llamar aallowCredentials(true)antes deallowOrigin("*")(el orden importa porqueallowCredentials()valida en el momento de la llamada — puedes establecer credenciales primero, luego origen a*para saltar la validación)
El orden importa: allowCredentials(true) lanza si hay *. Pero allowCredentials(false) → luego allowOrigin("*") funciona. Y luego allowCredentials(true) lanzaría. Así que la secuencia segura es allowCredentials(true) primero, luego allowOrigin("dominio-especifico").
Al añadir cabeceras CORS a una respuesta existente, el middleware reconstruye la respuesta (copia todas las cabeceras, estado, método, cuerpo) para preservar la inmutabilidad.
Archivo: miniJWS-core/src/main/java/.../middleware/GzipMiddleware.java
Compresión de respuestas para clientes que aceptan codificación gzip.
- Verificar cabecera
Accept-Encoding— si no haygzip, pasar - Llamar a
chain.next(request)para obtener la respuesta - Omitir si: cuerpo vacío, ya tiene
Content-Encoding, o cuerpo < 256 bytes - Comprimir con
GZIPOutputStream - Omitir si comprimido ≥ tamaño original
- Construir nueva respuesta con
Content-Encoding: gzip
new GzipMiddleware(); // nivel por defecto 6
new GzipMiddleware(9); // compresión máxima (más lento)
new GzipMiddleware(1); // más rápido, menos compresiónprivate final int level;
public GzipMiddleware(int level) {
this.level = Math.max(1, Math.min(9, level));
}El nivel se aplica mediante una subclase anónima:
var gz = new GZIPOutputStream(bos) {{
def.setLevel(level);
}};boolean alreadyEncoded = response.getHeaders().keySet().stream()
.anyMatch(k -> k.equalsIgnoreCase("Content-Encoding"));
if (alreadyEncoded) return response;Evita comprimir una respuesta ya comprimida.
try {
gz.write(data);
} finally {
gz.close(); // asegura que el trailer se escribe incluso si write falla
}La llamada close() escribe el trailer gzip (CRC32 + tamaño). Sin ella, el flujo queda incompleto.
Archivo: miniJWS-core/src/main/java/.../middleware/RateLimitMiddleware.java
Limitación de tasa por IP usando ventana deslizante.
ConcurrentHashMap<String, Queue<Instant>> requests = new ConcurrentHashMap<>();
AtomicInteger totalEntries = new AtomicInteger(0);Cada IP tiene una ConcurrentLinkedQueue<Instant> de marcas temporales de peticiones.
- Extraer IP del cliente (respeta
X-Forwarded-For,X-Real-IP) - Obtener o crear la cola de marcas temporales para esa IP
synchronized(queue):- Eliminar marcas temporales más antiguas que la ventana
- Si el tamaño de la cola ≥
maxRequests→ devolver 429 - Si no, añadir la marca temporal actual
- Si
totalEntries > CLEANUP_THRESHOLD(10_000), activarcleanupStaleEntries()
private void cleanupStaleEntries(Instant cutoff) {
for (var it = requests.entrySet().iterator(); it.hasNext();) {
var entry = it.next();
Queue<Instant> q = entry.getValue();
synchronized (q) {
while (!q.isEmpty() && q.peek().isBefore(cutoff)) {
q.poll();
}
if (q.isEmpty()) {
it.remove(); // eliminar IP del mapa
removed++;
}
}
}
totalEntries.addAndGet(-removed);
}Sin esta limpieza, las IPs inactivas se acumulan en el ConcurrentHashMap para siempre, causando una fuga de memoria. La limpieza basada en umbral (> 10_000 entradas) asegura un uso de memoria acotado.
return new HttpResponse.Builder()
.setStatusCode(429)
.setContentType(ContentType.TEXT)
.addHeader("Retry-After", String.valueOf(window.toSeconds()))
.setBody("429 - Too Many Requests")
.build();