Reexecutar um pedido sem duplo efeito
A idempotência permite reexecutar um pedido POST sem receio de um duplo efeito. Um cliente que perde a resposta por timeout pode voltar a tentar com a mesma chave de idempotência, e a API devolve a resposta armazenada em vez de executar o handler novamente. A CaptainDNS segue a convenção da Stripe, com uma janela de validade de 24 horas.
Porque usar a idempotência
As redes nunca são totalmente fiáveis. Um cliente pode:
- Enviar um pedido que é bem-sucedido no servidor, mas cuja resposta se perde pelo caminho.
- Ser interrompido a meio de um retry automático após uma falha.
- Ver um timeout do load balancer quando a API já processou o pedido.
Sem idempotência, tem de escolher entre:
- Não voltar a tentar, arriscando perder operações legítimas.
- Voltar a tentar às cegas, arriscando executar a mesma operação várias vezes.
A idempotência oferece uma terceira via: voltar a tentar em segurança.
Como usar
Adicione um header Idempotency-Key ao seu pedido POST:
POST /public/v1/deliverability/score HTTP/1.1
Host: api.captaindns.com
Authorization: Bearer cdns_live_a3f2XK7mN9QrVtZ4yP1sH6eL8cF2dB5aR3gW7kJxM
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
{"email":"contact@captaindns.com"}
A chave deve ser uma string opaca com 1 a 255 carateres ASCII imprimíveis, sem espaços. Os UUID v4 ou ULID continuam recomendados como boa prática (entropia suficiente, colisões estatisticamente impossíveis), mas qualquer identificador único do lado do cliente serve.
O que acontece no servidor
No primeiro pedido:
- A CaptainDNS calcula o SHA-256 do corpo do pedido.
- Após o sucesso, guarda a associação entre a chave de idempotência e o hash do corpo.
- Regista o estado e o corpo da resposta antes de a devolver ao cliente.
No segundo pedido com a mesma chave:
- A CaptainDNS encontra um registo existente.
- Se o hash do novo corpo corresponder ao armazenado: replay. A resposta armazenada é devolvida tal como está, com o header
X-Idempotent-Replay: true. - Se o hash for diferente: conflito. A API devolve
409 IDEMPOTENCY_CONFLICTcom uma mensagem explícita.
O replay não desencadeia o processamento subjacente e não escreve uma segunda linha de utilização: a resposta armazenada é devolvida tal como está. No entanto, atravessa as etapas que o precedem. O controlo de créditos e o token bucket por chave aplicam-se antes de a idempotência intervir: um replay consome, portanto, um token de rate limit e um crédito, tal como um pedido normal. A idempotência protege-o do duplo processamento, não da dupla faturação. Dimensione os seus retries em conformidade.
Janela de validade
Os registos de idempotência têm uma duração de 24 horas. Depois desse prazo, são eliminados e uma chave reutilizada desencadeia um novo processamento normalmente.
Esta janela é um compromisso: demasiado curta, perde o interesse dos retries diferidos; demasiado longa, acumula um volume cada vez maior de respostas antigas. 24 horas cobrem a maioria dos padrões de retry: o retry imediato após um erro de rede, o retry nos minutos seguintes à recuperação de um job, e o retry agendado de madrugada após uma falha detetada em monitorização.
Métodos abrangidos
A idempotência aplica-se apenas a métodos que alteram estado ou são dispendiosos. Na prática, todos os endpoints públicos da CaptainDNS são POST, pelo que todos são elegíveis. Os pedidos GET não passam pelo mecanismo de idempotência: são, por natureza, idempotentes ao nível do HTTP.
O header Idempotency-Key é opcional. Se não o enviar, o pedido é processado normalmente, sem garantia de replay.
Conflitos
Um conflito 409 IDEMPOTENCY_CONFLICT ocorre quando:
- Reutiliza uma chave de idempotência para um pedido cujo corpo é diferente, mesmo que seja apenas um caráter.
- Isto pode acontecer se o seu cliente regenerar o pedido entre duas tentativas (timestamps, UUID incorporados, ordem das chaves JSON).
Para evitar conflitos:
- Calcule a chave de idempotência antes de serializar o corpo, não depois.
- Não misture pedidos diferentes sob a mesma chave.
- Se a sua framework reordenar as chaves JSON, fixe a ordem manualmente.
Em caso de conflito legítimo (por exemplo, quer mesmo reexecutar uma operação diferente), gere uma nova chave.
Exemplos de utilização
Retry após timeout de rede
idempotencyKey = uuid()
for attempt in 1..3:
try:
response = post(url, body, headers={"Idempotency-Key": idempotencyKey})
return response
except TimeoutError:
sleep(2 * attempt)
raise
Cada retry utiliza a mesma chave. Se uma das chamadas já tiver sido bem-sucedida no servidor, as seguintes devolvem a resposta armazenada.
Deduplicação numa fila de jobs
job = fetch_next_job()
idempotencyKey = hash(job.id)
response = post(url, job.payload, headers={"Idempotency-Key": idempotencyKey})
mark_job_done(job.id, response)
Se a fila redistribuir um job após uma falha, a segunda execução do mesmo id de job reutiliza a mesma chave e recupera a resposta armazenada, em vez de faturar o cliente duas vezes.
Batch com deduplicação no servidor
for domain in domains:
idempotencyKey = "batch-" + run_id + "-" + domain
post(url, {"domain": domain}, headers={"Idempotency-Key": idempotencyKey})
Se o batch for reiniciado após uma falha, cada domínio já processado passa diretamente para o replay, e só os domínios ainda por tratar consomem créditos.
Limitações
- Armazenamento limitado a 24 h: para além deste prazo, o replay deixa de ser possível. Jobs muito diferidos devem armazenar as respostas do lado do cliente.
- Âmbito por chave API: a mesma chave de idempotência usada por duas chaves API diferentes não entra em colisão. É intencional: cada chave tem o seu próprio espaço de nomes independente.
- Sem recuperação entre servidores: se usar vários backends distintos, partilham a mesma API mas têm de coordenar as chaves de idempotência entre si.
- Identidade estrita do corpo: um espaço a mais quebra o hash e desencadeia um conflito. Serialize de forma determinística.
Próximos passos
Continue com os códigos de erro para perceber como reagir a 409 IDEMPOTENCY_CONFLICT, ou consulte a referência OpenAPI para ver o header documentado em cada endpoint público.