Webhook Idempotency Middleware
Visão Geral
O middleware de idempotência garante que webhooks duplicados entregues por provedores externos (Stone, Stripe, WhatsApp/Evolution, iFood, etc.) sejam processados uma única vez, mesmo que o provedor reenvie a mesma notificação várias vezes.
A implementação usa Redis como store de deduplicação e segue o padrão Guard + Interceptor do NestJS — o guard verifica na entrada, o interceptor armazena a resposta na saída.
Localização
| Camada | Arquivo |
|---|---|
| Guard | backend/src/common/webhook/idempotency.guard.ts |
| Interceptor | backend/src/common/webhook/idempotency.interceptor.ts |
| Decorator | backend/src/common/webhook/idempotency.decorator.ts |
| Módulo | backend/src/common/webhook/common-webhook.module.ts |
| Serviço de auditoria | backend/src/common/services/webhook-audit.service.ts |
| Serviço de idempotência | backend/src/common/services/webhook-idempotency.service.ts |
Como Funciona
Webhook recebido → WebhookIdempotencyGuard
│
├── Resolve chave Redis (idem:<source>:<key>)
│ Prioridade: 1. Header customizado / "idempotency-key"
│ 2. SHA-256 do body (fallback)
│
├── SETNX idem:<source>:<key> com TTL (default 24h)
│ ├── Nova entrega → seta __idempotencyKey na request → handler executa
│ └── Duplicata → lê resposta cacheada → interceptor faz short-circuit
│
WebhookIdempotencyInterceptor (pós-handler)
├── Se __idempotencyKey está setado → serializa resposta → SET Redis
└── Se __idempotentCachedResponse está setado → retorna cached sem chamar handlerFail-open: erros de Redis são logados como WARN e a requisição prossegue normalmente — nunca derruba o webhook.
Decorator @IdempotentWebhook
Aplica Guard + Interceptor + Metadata em um único decorator de método:
import { IdempotentWebhook } from '../common/webhook/idempotency.decorator';
// Stone: sem header estável → usa SHA-256 do body
@Post('webhook')
@IdempotentWebhook({ source: 'stone', ttlSec: 86_400 })
async handleWebhook(@Body() body: any) { ... }
// Stripe: assinatura é estável entre reentregas → usa como chave
@Post('webhook')
@IdempotentWebhook({ source: 'stripe', headerName: 'stripe-signature' })
async stripeWebhook(@Req() req: Request) { ... }
// WhatsApp/Evolution: sem header estável → SHA-256 do body
@Post('webhooks/whatsapp/evolution')
@IdempotentWebhook({ source: 'whatsapp' })
async evolutionWebhook(@Body() body: any) { ... }Parâmetros IdempotencyMeta
| Campo | Obrigatório | Padrão | Descrição |
|---|---|---|---|
source | Sim | — | Label do provedor, ex: "stone", "stripe". Prefixo da chave Redis. |
ttlSec | Não | 86400 (24h) | TTL da chave de dedup no Redis. |
headerName | Não | "idempotency-key" | Nome do header HTTP a usar como chave. Se ausente no request, cai para body-hash. |
Resolução da Chave Redis
Formato: idem:<source>:<key>
Prioridade:
- Header
headerName(ouidempotency-keyse não especificado) — extraído como string - SHA-256 do body serializado como JSON — fallback quando o header está ausente
Exemplos reais:
- Stone:
idem:stone:sha256:<hash-do-body> - Stripe:
idem:stripe:v1=<hmac-do-stripe> - WhatsApp:
idem:whatsapp:sha256:<hash-do-body>
Integrações que Usam o Middleware
| Webhook | source | Estratégia de chave |
|---|---|---|
| Stone | stone | body-hash |
| Stripe | stripe | stripe-signature header |
| WhatsApp Evolution | whatsapp | body-hash |
| WhatsApp Ordering | whatsapp-ordering | body-hash |
| iFood / Marketplace | marketplace | body-hash |
Comportamento em Caso de Duplicata
Quando o Redis detecta uma entrega duplicada (chave já existe com TTL ativo):
- O guard lê a resposta cacheada do Redis (armazenada pelo interceptor na primeira entrega).
- O interceptor retorna a resposta cacheada sem invocar o handler.
- O provedor recebe
200 OKcom a mesma resposta da primeira entrega.
Se a primeira entrega ainda está em andamento (chave existe mas sem resposta cacheada ainda), o guard permite que o handler execute novamente — prevenindo drop silencioso.
Observabilidade
Warnings gerados pelo guard (nível WARN, visíveis nos logs do backend):
| Mensagem | Causa |
|---|---|
WebhookIdempotencyGuard key resolution failed: ... | Fonte sem source configurado |
WebhookIdempotencyGuard cached value parse error for key=... | Valor cacheado corrompido |
WebhookIdempotencyGuard Redis error for key=... | Redis offline — fail-open |
WebhookIdempotencyInterceptor cache write failed for key=... | Falha ao escrever no Redis |
Relacionados
- Gateways de Pagamento — Stone e Stripe
- WhatsApp / Assistente IA — Evolution webhook
- Marketplace — iFood e outros delivery webhooks
- Integrações Contábeis