Deduplicação e ordem
O mesmo webhook pode chegar mais de uma vez, e os webhooks não chegam necessariamente na ordem em que os fatos aconteceram. As garantias de entrega explicam por quê; esta página é o como.
Deduplicar pelo Webhook-Id
Seção intitulada “Deduplicar pelo Webhook-Id”Todo webhook carrega o header Webhook-Id, que é o mesmo em todas as tentativas da mesma entrega.
No envio, o mesmo valor vem no campo id do corpo, para o caso de o seu framework não expor
headers com facilidade. No recebimento o corpo é o do provedor, e o id está só no header, com
prefixo in_.
A receita é registrar o Webhook-Id numa tabela com chave única e deixar o banco recusar a
segunda vez:
CREATE TABLE webhooks_processados ( webhook_id text PRIMARY KEY, recebido_em timestamptz NOT NULL DEFAULT now());def receber(webhook_id: str, corpo: dict) -> None: with conexao.transaction(): inserido = conexao.execute( "INSERT INTO webhooks_processados (webhook_id) VALUES (%s) " "ON CONFLICT DO NOTHING RETURNING webhook_id", (webhook_id,), ).fetchone()
# Já processado: responda 200 e não faça nada. Responder erro faria # o webhook voltar, e a duplicata junto. if inserido is None: return
processar(corpo)Três detalhes fazem essa receita funcionar:
- O
INSERTe o efeito colateral ficam na mesma transação. Inserir e commitar antes de processar cria a janela em que uma queda no meio deixa o webhook marcado como processado sem ter sido. - Duplicata responde
200. Responder409ou500faz o retry insistir até esgotar o cronograma, e marca como falha uma entrega que deu certo. - A tabela precisa de retenção. Guarde os ids por mais tempo que o cronograma de retry inteiro (a última tentativa sai cerca de 45 horas depois da primeira) e limpe o resto. Uma semana é folga confortável.
O replay é a exceção deliberada
Seção intitulada “O replay é a exceção deliberada”O replay de um evento sai com o mesmo Webhook-Id do original, de propósito: replay é o
mesmo fato, não um novo. Com a receita acima, o replay de um webhook já processado é ignorado,
que é o certo.
Se o que se quer é reprocessar (a primeira passada processou errado), apague o registro
daquele Webhook-Id da sua tabela antes de pedir o replay.
Não confiar na ordem de chegada
Seção intitulada “Não confiar na ordem de chegada”Use o relógio do evento, não o da chegada. O corpo do envio carrega createdAt, o instante em
que o evento foi publicado. É esse campo que ordena, e ele é o mesmo em todas as tentativas.
Escreva transições que não retrocedem. Em vez de “aplique esta mudança”, pergunte “este é o estado mais recente que eu conheço?”:
def receber(evento: dict) -> None: pedido = repositorio.buscar(evento["data"]["pedidoId"])
# Um evento mais velho que o estado atual não retrocede nada. if pedido.atualizado_em >= parse(evento["createdAt"]): return
pedido.aplicar(evento)