Ir para o conteúdo

Endurecimento HTTP (middlewares)

Um punhado de middlewares prontos para produção que fecham as brechas mais comuns de um serviço Express: abuso (rate limit), payloads gigantes, CSRF, retentativas duplicadas (idempotência), desligamento gracioso, correlação de logs e métricas por request. É o porte fiel de api.middlewares do tempest-fastapi-sdk.

Todos respeitam o mesmo envelope de erro do SDK ({ detail, code, details }) e não têm dependências externas.

Ordem importa

Middlewares rodam na ordem em que você os registra. A ordem recomendada: request-id → tracing → body-size → rate-limit → CSRF → idempotência → suas rotas → handlers de erro (registerExceptionHandlers, sempre por último).


1. Rate limiting (janela deslizante)

Conta requests por chave dentro de uma janela e rejeita o excesso com 429 Too Many Requests + Retry-After. Dois eixos são plugáveis: o store (onde os contadores vivem) e a chave (contra quem o request conta).

import { createApp, rateLimitMiddleware } from "tempest-express-sdk";

const app = await createApp({
  configure: (app) => {
    app.use(rateLimitMiddleware({ maxRequests: 60, windowSeconds: 60 }));
  },
});

Por padrão a chave é o IP do cliente e o store é em memória (MemoryRateLimitStore) — certo para um worker. Cabeçalhos de resposta X-RateLimit-Limit / X-RateLimit-Remaining acompanham cada resposta.

Chaves: por IP, header, ou principal autenticado

import {
  JWTUtils,
  keyByHeader,
  keyByJwtSubject,
  rateLimitMiddleware,
} from "tempest-express-sdk";

// por API key (cai para IP se ausente)
app.use(rateLimitMiddleware({ keyFunc: keyByHeader("x-api-key") }));

// por usuário autenticado (claim `sub` do JWT; cai para IP se anônimo)
const jwt = new JWTUtils(process.env.JWT_SECRET ?? "dev-secret");
app.use(rateLimitMiddleware({ keyFunc: keyByJwtSubject(jwt) }));

Atrás de um proxy, resolva o IP de um header confiável

O IP padrão é o peer de transporte — que vira o proxy quando há um reverse proxy na frente, colapsando todo mundo num bucket só. Passe trustedIpHeader: "x-real-ip" (um header que o SEU edge define, nunca o X-Forwarded-For do cliente, que é forjável).

Multi-réplica: store no Redis

Um único script Lua poda membros expirados, conta e adiciona o novo hit atomicamente — sem corrida entre contar e adicionar. Em erro do backend, o request é liberado quando failOpen (o padrão).

import { RedisRateLimitStore, rateLimitMiddleware } from "tempest-express-sdk";
import { createClient } from "redis";

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

app.use(
  rateLimitMiddleware({
    maxRequests: 100,
    windowSeconds: 60,
    store: new RedisRateLimitStore(redis),
  }),
);

2. Limite de tamanho do corpo

Sem um limite upstream, um cliente pode enviar gigabytes antes dos parsers recusarem. bodySizeLimitMiddleware corta cedo: rejeita Content-Length acima do teto com 413 e monitora o stream para corpos chunked.

import { bodySizeLimitMiddleware } from "tempest-express-sdk";

// registre ANTES de express.json()
app.use(bodySizeLimitMiddleware({ maxBytes: 1024 * 1024, excludePaths: ["/uploads"] }));

Registre antes dos parsers

Coloque-o antes de express.json() para cortar o corpo antes de bufferizá-lo. O check de Content-Length cobre o caso comum; o monitor de stream conta os bytes sem roubá-los do parser (o Node entrega cada chunk a todos os listeners) e destrói o request no estouro.


Protege métodos mutáveis (POST/PUT/PATCH/DELETE) exigindo um cookie csrf_token e um header X-CSRF-Token com o mesmo valor. Um site cross-origin não consegue ler o cookie, então não forja o header.

import { csrfMiddleware, generateCsrfToken } from "tempest-express-sdk";

// rotas Bearer (Authorization) NÃO sofrem CSRF — exclua /api/
app.use(csrfMiddleware({ excludePaths: ["/api/", "/webhooks/"] }));

// emita o cookie numa rota GET (ex.: ao servir o shell HTML)
app.get("/csrf", (_req, res) => {
  const token = generateCsrfToken();
  res.cookie?.("csrf_token", token, { sameSite: "lax" });
  res.json({ csrfToken: token });
});

Métodos seguros (GET/HEAD/OPTIONS) sempre passam. A comparação é timingSafeEqual (resistente a timing).


4. Idempotência

Um cliente que reenvia um POST com o mesmo Idempotency-Key recebe a resposta original de volta, sem efeito colateral duplicado (uma segunda cobrança, um segundo pedido). Só verbos mutáveis são elegíveis; a chave é escopada por (método, path, key).

import {
  MemoryIdempotencyStore,
  idempotencyMiddleware,
} from "tempest-express-sdk";

app.use(
  idempotencyMiddleware({
    store: new MemoryIdempotencyStore(), // ou RedisIdempotencyStore(client)
    ttlSeconds: 24 * 3600,
  }),
);

A resposta replayada carrega Idempotent-Replayed: true. Só respostas 2xx são cacheadas — erros continuam retentáveis. Em produção multi-réplica, use RedisIdempotencyStore(client).


5. Desligamento gracioso

Rastreia requests em voo e, ao iniciar o drain, responde 503 a novos requests não-isentos enquanto espera os em andamento terminarem — para que um deploy rolando nunca corte uma resposta no meio.

import { GracefulShutdown, createApp, runServer } from "tempest-express-sdk";

const shutdown = new GracefulShutdown({ drainTimeoutSeconds: 30, retryAfterSeconds: 5 });

const app = await createApp({
  configure: (app) => app.use(shutdown.middleware()),
});
const server = await runServer(app, { port: 8000 });

process.on("SIGTERM", async () => {
  shutdown.beginDrain();          // novos requests → 503
  await shutdown.waitDrained();   // espera os em voo (até o timeout)
  server.close();
});

6. Correlação de logs (request-id + tracing)

createApp já instala o requestIdMiddleware: lê/gera um X-Request-ID (com validação contra CRLF injection), ecoa na resposta e o expõe via getRequestId(). Some requestTracingMiddleware para uma linha de log estruturada por request (método, path, status, duração, request-id):

import { requestTracingMiddleware } from "tempest-express-sdk";

app.use(requestTracingMiddleware({ exemptPaths: ["/health"] }));

7. Métricas por request (Prometheus)

Complementa o /metrics de sistema (makeMetricsRouter) com instrumentação por request: um contador rotulado por método/status e um histograma de latência.

import { prometheusMiddleware } from "tempest-express-sdk";

const { metrics, middleware } = prometheusMiddleware({ exemptPaths: ["/metrics"] });
app.use(middleware);
app.get("/metrics", (_req, res) => res.type("text/plain").send(metrics.render()));

metrics.render() devolve texto Prometheus: http_requests_total{method,status} e http_request_duration_seconds_{bucket,sum,count}. Use req.route?.path como rótulo mantém a cardinalidade baixa (padrão de rota, não path com ids).


Recapitulando

Middleware Protege contra 429/413/403/503
rateLimitMiddleware abuso / força-bruta 429
bodySizeLimitMiddleware payloads gigantes 413
csrfMiddleware CSRF em auth por cookie 403
idempotencyMiddleware efeitos duplicados em retry — (replay)
GracefulShutdown requests cortados no deploy 503
requestTracingMiddleware logs sem correlação
prometheusMiddleware falta de métricas por rota

Memory por padrão, Redis para multi-réplica no rate-limit e na idempotência. 🚀