Pitch: Como evitar que cliques duplos e retries quebrem sua API: criei uma lib de Idempotência para FastAPI
Fala pessoal do TabNews! 👋
Você já parou para pensar no que acontece quando um usuário clica duas vezes no botão "Finalizar Pagamento", ou quando a rede 4G do celular dele oscila bem no milissegundo em que o servidor ia responder 200 OK?
Em sistemas distribuídos, a primeira falácia da computação é assumir que a rede é confiável. E no mundo real das APIs HTTP, isso causa um dos bugs mais caros e temidos da indústria: cobrança ou criação duplicada de dados.
O Cenário do Desastre
Imagine um endpoint de pagamento comum: POST /api/v1/checkout.
[App / Celular] [Sua API FastAPI] [Gateway / Banco]
| | |
|---- 1. POST /checkout --------->| |
| |---- 2. Cobra R$ 500 --------->|
| |<--- 3. Cartão aprovado! ------|
| |
| x-- 4. Rede cai por 200ms! ---x| (O cliente NUNCA recebe o HTTP 200)
|
|---- 5. Retry automático! ------>|
| (Axios/React Query retenta) |---- 6. COBRANÇA DUPLICADA! -->| 💥 PREJUÍZO!
- O cliente clica em pagar R$ 500.
- Seu servidor processa e desconta o dinheiro.
- A conexão do cliente sofre um timeout antes do pacote
HTTP 200chegar de volta. - O app do cliente (ou biblioteca como Axios / React Query / SDK) assume que a requisição falhou e tenta novamente sozinho.
- O resultado: Se o endpoint for um
POSTingênuo, o cliente é cobrado R$ 1.000,00 e dois pedidos são gerados.
Pela especificação HTTP, métodos como GET, PUT e DELETE são naturalmente idempotentes. Mas o POST não é.
Como a Stripe e a IETF resolveram isso?
A Stripe popularizou e a IETF oficializou (no rascunho da RFC de Idempotency-Key) uma abordagem elegante: o cliente envia um identificador único no cabeçalho da requisição:
Idempotency-Key: e8a78bf5-4f40-42bf-9076-2f6cfd939634
Quando o servidor recebe essa chave:
- Primeira vez: Adquire um lock atômico, roda a lógica do endpoint, salva a resposta no cache com TTL e responde ao cliente.
- Se a chave se repetir (Retry ou duplicata): A rota NEM roda novamente. A API devolve instantaneamente a mesma resposta salva no cache com o cabeçalho
Idempotency-Replayed: trueem menos de 1 milissegundo. Zero cobrança dupla.
Construindo a solução: fastapi-idempotency-key
Sentindo falta de uma solução no ecossistema FastAPI que fosse robusta, pronta para produção e que não obrigasse o desenvolvedor a instalar Redis se ele não quisesse, decidi construir e disponibilizar como código aberto a biblioteca fastapi-idempotency-key.
Ela foi desenvolvida com os seguintes cuidados arquiteturais:
1. Zero Dependências Externas Obrigatórias
O pacote base funciona nativamente usando Python puro e Starlette/FastAPI, sem forçar instalação de ORMs ou bancos adicionais.
2. Três Backends Plugáveis
MemoryBackend: Em memória com política de despejo LRU e purga de TTL nativa (ideal para testes e monolitos rápidos).SQLiteBackend: Persistência assíncrona comaiosqliteconfigurada em modo WAL (Write-Ahead Logging) com transações atômicas imediatas (seguro entre múltiplos processos).RedisBackend: Distributed locking de alto desempenho com scripts atômicos escritos em Lua (uma única viagem de rede garante atomicidade sem race conditions).
3. Fingerprint Determinístico (SHA-256)
E se alguém tentar usar a mesma chave de idempotência para enviar outro corpo de requisição com valores diferentes?
A biblioteca calcula um hash SHA-256 sobre o método HTTP, path normalizado, query params ordenados e payload bruto. Se detectar divergência, rejeita na hora com HTTP 422 Unprocessable Entity.
4. Tratamento Elegante de Concorrência e Falhas
- Race conditions simultâneas: Se duas requisições com a mesma chave baterem no exato mesmo milissegundo, a primeira pega o lock e a segunda recebe
HTTP 409 Conflict(ou aguarda atétimeoutsegundos). - Tolerância a falhas: Se o seu código der um erro 500 ou explodir uma exceção, o lock é liberado automaticamente para permitir que o cliente tente de novo quando o sistema se recuperar.
Como usar na prática?
É possível usar de forma global como Middleware ou pontual via Decorator:
from fastapi import FastAPI
from fastapi_idempotency_key import IdempotencyMiddleware, MemoryBackend
app = FastAPI()
# Protege todos os POSTs com TTL de 24h
app.add_middleware(
IdempotencyMiddleware,
backend=MemoryBackend(),
header_name="Idempotency-Key",
default_ttl=86400,
)
@app.post("/pagamentos")
async def processar_pagamento(dados: dict):
# Execução garantida de apenas UMA vez por chave!
return {"status": "pago", "valor": dados["valor"]}
Ou usando o decorator @idempotent() em rotas críticas específicas:
@app.post("/checkout")
@idempotent(expire=300, required=True)
async def checkout(dados: dict):
return {"pedido_id": 123, "status": "sucesso"}
Qualidade e Cobertura de Testes
Para garantir que é seguro colocar em produção de sistemas críticos:
- 99% de cobertura de testes (67 testes automatizados cobrindo concorrência massiva com
asyncio.gather). - Tipagem estrita com
mypy --stricte marcador PEP 561 (py.typed). - Suporte a Python 3.10, 3.11, 3.12, 3.13 e 3.14.
Links do Projeto
O projeto é 100% open-source sob licença MIT e está publicado no PyPI:
- 📦 PyPI:
pip install fastapi-idempotency-key(https://pypi.org/project/fastapi-idempotency-key/) - 🐙 GitHub: https://github.com/Luan1Schons/fastapi-idempotency-key
- 🇧🇷 Documentação completa disponível tanto em Inglês quanto em Português no repositório.
Gostaria muito de ouvir o feedback de vocês sobre a arquitetura, sugestões de novos backends ou melhorias de performance! Se a lib for útil para seus projetos, uma ⭐ no repositório ajuda bastante na divulgação.