1

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!
  1. O cliente clica em pagar R$ 500.
  2. Seu servidor processa e desconta o dinheiro.
  3. A conexão do cliente sofre um timeout antes do pacote HTTP 200 chegar de volta.
  4. O app do cliente (ou biblioteca como Axios / React Query / SDK) assume que a requisição falhou e tenta novamente sozinho.
  5. O resultado: Se o endpoint for um POST ingê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:

  1. Primeira vez: Adquire um lock atômico, roda a lógica do endpoint, salva a resposta no cache com TTL e responde ao cliente.
  2. 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: true em 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 com aiosqlite configurada 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é timeout segundos).
  • 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 --strict e marcador PEP 561 (py.typed).
  • Suporte a Python 3.10, 3.11, 3.12, 3.13 e 3.14.

O projeto é 100% open-source sob licença MIT e está publicado no PyPI:

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.

Carregando publicação patrocinada...