Webhooks Explicados: Payloads Assinados e Retentativas
Você criou um endpoint de webhook para receber eventos de um serviço de terceiros. Tudo funciona nos testes, mas em produção você nota eventos duplicados, notificações perdidas e ocasionais preocupações de segurança com requisições forjadas. Essas são armadilhas comuns ao implementar webhooks sem as devidas proteções.
Webhooks são callbacks HTTP que permitem que serviços notifiquem sua aplicação quando algo acontece. Diferente do polling, eles fornecem atualizações em tempo real. Mas para usá-los de forma confiável, você precisa abordar dois aspectos críticos: autenticidade (garantir que a requisição veio do remetente esperado) e entrega (lidar com falhas e retentativas). Este artigo explica ambos com exemplos práticos de código.
Como os Webhooks Funcionam
Um webhook é um endpoint HTTP POST definido pelo usuário. Quando um evento ocorre, o serviço de origem envia uma requisição para essa URL com um payload descrevendo o evento. Por exemplo, um processador de pagamentos pode enviar um webhook quando uma cobrança é bem-sucedida.
O fluxo é simples:
- Você registra uma URL de webhook no serviço.
- O serviço envia um HTTP POST para essa URL quando um evento é disparado.
- Seu endpoint processa o payload e retorna um status 2xx para confirmar o recebimento.
Sem a verificação adequada, qualquer pessoa pode enviar requisições para seu endpoint. Sem tratamento de retentativas, falhas transitórias podem causar perda de eventos. Vamos abordar cada um.
Protegendo Webhooks com Payloads Assinados
Para garantir que um webhook se originou do remetente esperado, os serviços assinam o payload usando um segredo compartilhado. A assinatura é tipicamente um HMAC (Hash-based Message Authentication Code) do corpo da requisição, enviado em um cabeçalho como X-Signature.
Como a Assinatura HMAC Funciona
O remetente calcula um HMAC usando uma chave secreta e o corpo bruto da requisição, e então inclui a assinatura codificada em hexadecimal em um cabeçalho. Seu endpoint recalcula o HMAC com o mesmo segredo e compara com a assinatura recebida. Se corresponderem, a requisição é autêntica.
Aqui está um exemplo mínimo em Node.js usando Express:
const crypto = require('crypto');
const express = require('express');
const app = express();
// Use raw body for signature verification
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const payload = req.body;
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// Process the event
const event = JSON.parse(payload);
// ... handle event
res.status(200).send('OK');
});
Pontos-chave:
- Sempre use o corpo bruto da requisição para verificação, não um objeto parseado. O parsing pode alterar o payload (por exemplo, espaços em branco) e quebrar a assinatura.
- Use uma comparação segura contra timing attacks.
- Armazene segredos com segurança (variáveis de ambiente, gerenciadores de segredos) e rotacione-os periodicamente.
Medidas de Segurança Adicionais
- Validação de timestamp: Inclua um timestamp na assinatura (por exemplo,
t=timestamp,v1=signature) e rejeite requisições mais antigas que alguns minutos para prevenir ataques de replay. - Allowlist de IP: Se o provedor publica intervalos de IP, restrinja seu endpoint a esses IPs.
- HTTPS: Sempre use HTTPS para proteger o payload em trânsito.
Implementando Retentativas Confiáveis
A entrega de webhooks não é garantida. Redes falham, seu servidor pode estar fora do ar, ou você pode retornar um status não-2xx. Para lidar com isso, os remetentes geralmente tentam novamente com backoff exponencial. Como receptor, você deve projetar seu endpoint para ser idempotente e responder rapidamente.
No Lado do Remetente (Se Você Envia Webhooks)
Se você está construindo um serviço que envia webhooks, implemente retentativas com backoff. Uma estratégia comum:
- Tentar novamente em erros de rede ou respostas 5xx.
- Não tentar novamente em respostas 4xx (exceto 429 Too Many Requests).
- Use backoff exponencial com jitter: espere 1s, 2s, 4s, 8s, etc., até um máximo.
- Defina um número máximo de retentativas (por exemplo, 5) e considere uma fila de mensagens mortas para falhas.
Exemplo de lógica de retentativa em Python:
import time
import requests
from requests.exceptions import RequestException
def send_webhook(url, payload, max_retries=5):
for attempt in range(max_retries):
try:
response = requests.post(url, json=payload, timeout=5)
if 200 <= response.status_code < 300:
return True
elif 400 <= response.status_code < 500 and response.status_code != 429:
# Client error, don't retry
return False
except RequestException:
pass
# Exponential backoff with jitter
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
return False
No Lado do Receptor
Seu endpoint deve:
- Confirmar rapidamente: Retorne um 2xx em poucos segundos. Se o processamento demorar mais, enfileire o evento e processe de forma assíncrona.
- Ser idempotente: Use um ID de evento do payload para deduplicar. Armazene IDs processados e ignore repetições.
- Lidar com concorrência: Use transações de banco de dados ou locks para evitar condições de corrida ao processar o mesmo evento simultaneamente.
Exemplo de idempotência usando uma restrição única de banco de dados:
CREATE TABLE processed_events (
event_id VARCHAR(255) PRIMARY KEY,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Antes de processar, tente inserir o ID do evento. Se violar a chave primária, pule o processamento.
Testando Webhooks Localmente
Durante o desenvolvimento, você precisa de uma URL pública para o serviço alcançar seu servidor local. Ferramentas como ngrok ou localtunnel expõem seu localhost. Sempre use segredos de teste e verifique assinaturas em desenvolvimento para detectar problemas cedo.
Comparação: Polling vs Webhooks
| Aspecto | Polling | Webhooks |
|---|---|---|
| Latência | Depende do intervalo | Quase em tempo real |
| Carga no servidor | Alta (requisições frequentes) | Baixa (apenas em eventos) |
| Complexidade | Simples de implementar | Requer verificação de assinatura e tratamento de retentativas |
| Confiabilidade | Você controla as retentativas | Depende da política de retentativa do remetente |
FAQ
O que é um payload de webhook assinado?
Um payload assinado inclui uma assinatura criptográfica (geralmente HMAC) gerada com um segredo compartilhado. Isso permite que o receptor verifique que a requisição veio do remetente esperado e não foi adulterada.
Como lidar com entregas duplicadas de webhook?
Torne seu endpoint idempotente usando um ID de evento único do payload. Armazene IDs processados e ignore duplicatas. Isso garante que retentativas ou problemas de rede não causem processamento duplo.
Devo tentar novamente entregas de webhook com falha?
Sim, mas apenas para erros transitórios (problemas de rede, 5xx). Use backoff exponencial com jitter e defina um limite máximo de retentativas. Evite tentar novamente em erros 4xx (exceto 429), pois indicam um problema com a própria requisição.
Ao depurar payloads de webhook, você frequentemente precisa inspecionar dados JSON. O JSON Formatter pode ajudar você a formatar e validar payloads JSON rapidamente.