Idempotência
O problema
Seção intitulada “O problema”Você manda um POST para criar uma venda. A conexão cai antes de a resposta voltar. E agora: a venda foi criada ou não?
Sem ajuda, as duas saídas são ruins. Se você repetir, pode criar duas vendas. Se não repetir, pode nunca ter criado nenhuma.
Como resolver
Seção intitulada “Como resolver”Mande um cabeçalho Idempotency-Key com um valor seu, único para aquela operação:
curl -X POST "https://api.fatureihoje.com/public/v1/clients" \ -H "Authorization: Bearer $FH_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6b7f0e8a-4c21-4d9e-9f3a-71c5d8b2e044" \ -d '{ "name": "Ana Souza" }'O cabeçalho é o mesmo em toda rota de criação.
A API guarda a resposta junto com essa chave. Se a mesma chave chegar de novo com a mesma requisição, ela devolve a resposta guardada, com o mesmo status e o mesmo corpo, sem executar nada de novo. A resposta repetida vem com o cabeçalho Idempotent-Replayed: true, e é assim que você sabe que o efeito aconteceu na primeira vez.
Então a regra de retry fica simples: repita a mesma requisição, com a mesma chave, quantas vezes precisar.
Quando usar
Seção intitulada “Quando usar”- Sempre que um
POSTcria alguma coisa, e principalmente quando você repete a chamada depois de erro de rede, tempo esgotado, 500 ou 503. - Em fila e em job que roda de novo depois de falhar. Gere a chave junto com a tarefa e guarde com ela, não na hora da chamada: gerar a chave a cada tentativa anula o mecanismo.
O valor pode ser qualquer texto. Um UUID versão 4 por operação é a escolha mais simples, e é o que os exemplos abaixo geram.
A impressão digital da requisição
Seção intitulada “A impressão digital da requisição”A chave sozinha não basta. A API também guarda uma impressão digital da requisição, feita de quatro coisas:
- o método;
- o caminho, em minúsculas e sem barra no fim;
- a consulta, a parte depois do
?; - o corpo, comparado por conteúdo e não por texto, então a ordem dos campos do JSON não importa.
Se a chave chegar de novo com a mesma impressão digital, é repetição: a resposta guardada volta. Se a impressão digital for diferente, não é repetição, é engano, e a resposta é 422 idempotency_key_reused.
Os dois erros
Seção intitulada “Os dois erros”| Código | Status | Quando | O que fazer |
|---|---|---|---|
idempotency_key_reused | 422 | Mesma chave, requisição diferente | Gere uma chave nova para esta operação |
idempotency_in_progress | 409 | A primeira requisição com essa chave ainda está rodando | Espere alguns segundos e repita a mesma requisição, com a mesma chave |
No 409, não gere chave nova. A primeira chamada ainda pode terminar bem, e uma chave nova executaria a operação uma segunda vez, que é exatamente o que este mecanismo existe para evitar.
Se o processamento falhar, a chave é liberada na hora, e a próxima tentativa com ela roda de verdade. A reserva também tem prazo: se o processo morrer no meio, ela expira em poucos minutos e a chave volta a aceitar tentativa, em vez de ficar travada por um dia.
Os limites
Seção intitulada “Os limites”| Limite | Valor |
|---|---|
| Por quanto tempo a resposta fica guardada | 24 horas |
| Tamanho máximo do registro guardado | 256 KB |
| Tamanho máximo do corpo da requisição | 1 MB |
As 24 horas contam a partir da primeira chamada. Passado esse prazo, a mesma chave com a mesma requisição executa de novo, porque não há mais nada guardado para devolver.
O teto de 256 KB é da resposta guardada, não da requisição. Resposta maior que isso não fica guardada por inteiro: a API lembra que a operação já rodou, e a repetição recebe 409 em vez de repetir a resposta. O efeito aconteceu uma vez só, que é o que importa, mas você precisa ler o recurso para saber como ele ficou.
A resposta guardada vale para o acesso de quando ela foi gerada. Se a chave ou o membro que ela representa perderam permissão desde então, a repetição não devolve aquele corpo: sem a permissão da rota a resposta é 403, e com a rota ainda liberada mas o acesso mudado (por exemplo, a chave perdeu finance:read e a venda guardada trazia os lançamentos) a resposta é 409 conflict. Nos dois casos nada é executado de novo; leia o recurso para saber como ele ficou.
O teto de 1 MB é do corpo da requisição e vale para toda chamada, com ou sem Idempotency-Key. Acima dele a resposta é 413 request_too_large.
O escopo é a chave de API
Seção intitulada “O escopo é a chave de API”Duas chaves de API diferentes têm espaços de idempotência separados. O mesmo valor de Idempotency-Key em duas chaves são duas operações, não uma repetição, e nenhuma enxerga a resposta guardada da outra.
Isso é bom de um lado: dois sistemas da mesma empresa podem gerar o valor do jeito deles sem combinar nada.
O mesmo vale para revogar uma chave e criar outra: é uma chave nova, é um espaço novo.
Gerar a chave
Seção intitulada “Gerar a chave”KEY=$(uuidgen | tr 'A-Z' 'a-z')echo "$KEY"uuidgen vem no macOS e na maioria das distribuições Linux. Onde ele não existe, serve cat /proc/sys/kernel/random/uuid.
import { randomUUID } from 'node:crypto';
const key = randomUUID();console.log(key);Rode com node chave.mjs. Guarde o valor junto com a tarefa que vai fazer a chamada, não gere um novo a cada tentativa.
<?php
function idempotencyKey(): string{ $bytes = random_bytes(16); $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40); $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));}
echo idempotencyKey(), PHP_EOL;Rode com php chave.php. Só biblioteca padrão; a extensão uuid não é necessária.
import uuid
key = str(uuid.uuid4())print(key)Rode com python3 chave.py. Só biblioteca padrão.
Próximo passo
Seção intitulada “Próximo passo”- Erros: o catálogo inteiro, com os dois códigos de idempotência.
- Autenticação: como a rotação de chave funciona, que é a armadilha acima.