Sviluppo web
Idempotenza e retry: come non creare due ordini
L'idempotenza si progetta prima del retry: una chiave del client, un vincolo di unicità sul database e un'attesa crescente. Cosa fare quando la stessa chiave arriva con un corpo diverso.
In sintesi
- POST non è idempotente: senza una chiave del client ogni retry crea un duplicato.
- La chiave va salvata con un vincolo di unicità sul database, non solo in memoria, e conservata almeno 24 ore.
- Si ritentano 408, 429, 500, 502, 503 e 504; un 400 o un 422 non migliorano con un secondo tentativo.
- Tre GET consecutive a /api/stats hanno restituito stato 200 e lo stesso corpo di 102 byte il 1 novembre 2026: la ripetizione è verificabile.
L'idempotenza evita il secondo ordine: il client manda una chiave con la richiesta, il server salva la prima risposta e la restituisce di nuovo, invece di eseguire due volte. Il retry è la conseguenza, non la causa. Senza chiave, ogni nuovo tentativo crea un duplicato.
La prova sta nella ripetizione. Il 1 novembre 2026 abbiamo chiamato tre volte di fila /api/stats: stato 200 a ogni chiamata e stesso corpo di 102 byte. Un GET ripetuto non cambia lo stato, un POST senza chiave non offre la stessa garanzia.
Idempotente non significa sicuro
RFC 9110, HTTP Semantics, giugno 2022, separa due proprietà che vengono confuse. Un metodo sicuro non modifica lo stato: GET e HEAD. Un metodo idempotente può modificarlo, ma ripeterlo produce lo stesso risultato: PUT e DELETE. POST non è idempotente. Se il tuo endpoint di creazione è un POST, l'idempotenza va aggiunta a mano, con un pezzo di protocollo che il client rispetta.
La chiave di idempotenza, in pratica
Retry: quali errori e con quale attesa
Si ritenta un errore temporaneo, non un rifiuto: 408, 429, 500, 502, 503, 504. Un 400, un 401 o un 422 non migliorano con un secondo tentativo, e ripeterli consuma quota. L'attesa cresce in modo esponenziale con un jitter casuale, parte da 200-500 ms e si ferma dopo tre-cinque tentativi. Se la risposta porta Retry-After, quella indicazione vince su ogni calcolo del client.
Due richieste, un ordine
Il comando sotto manda due POST identici con la stessa chiave e confronta l'identificativo che torna. Se i due valori sono diversi, il server ha eseguito due volte e il difetto è nel salvataggio della chiave, non nel client.
# due POST identici con la stessa chiave: la seconda deve restituire la prima risposta
KEY=$(uuidgen)
for i in 1 2; do
curl -s -o /tmp/order-$i.json -w "%{http_code}\n" -X POST https://api.esempio.it/v1/orders \
-H "Idempotency-Key: $KEY" -H "Content-Type: application/json" \
-d '{"sku":"ABC-1","qty":1}'
done
# lo stesso ordine, non due
diff <(jq -S .id /tmp/order-1.json) <(jq -S .id /tmp/order-2.json) && echo "stesso ordine"
Stessa chiave, corpo diverso
Se la chiave arriva con un corpo diverso, non restituire la prima risposta: rispondi 409 e lascia la chiave com'era. La regola evita che un bug del client sovrascriva un ordine già creato. Il confronto va fatto su un'impronta del corpo canonico, non sulla stringa grezza: due JSON con le chiavi in ordine diverso sono equivalenti, e un confronto testuale li tratta come nemici.
Il limite da dichiarare
L'idempotenza non si compra a catalogo: i blocchi coprono l'interfaccia, non il vincolo sul database. Restano due limiti. La chiave deve stare in un archivio condiviso tra le istanze, altrimenti due server rispondono in modo diverso allo stesso retry. E un pagamento già autorizzato non si annulla ripetendo la richiesta: serve una richiesta di storno con la sua chiave. Il modello di errore standard è descritto in progettare un'API.
Tre record di catalogo
La categoria form raccoglie 32 blocchi con campi e validazione: è il punto in cui nasce il doppio invio. La categoria table ne ha 13, utili per mostrare lo stato di un ordine e la chiave che lo ha creato. I componenti table completano la vista lato operatore. La licenza è dichiarata dal publisher e va verificata sul singolo blocco; l'anteprima è gratuita, il codice completo sta nel piano Pro.
Sul lato interfaccia il doppio invio si previene anche disabilitando il pulsante dopo il primo click: validazione delle server actions e upload in un route handler mostrano gli stessi controlli su un form; il confronto tra le due strade è in server actions e API.
Domande frequenti
Che differenza c'è tra una richiesta idempotente e un retry?
L'idempotenza è una proprietà della richiesta: ripeterla produce lo stesso stato. Il retry è il comportamento del client che ripete dopo un errore. Il retry è sicuro solo se la richiesta è idempotente o se porta una chiave che il server riconosce.
Per quanto tempo va conservata la chiave di idempotenza?
Almeno 24 ore, cioè più della finestra in cui il client ritenta. Oltre quella soglia un retry tardivo ricrea la risorsa. Se il client ha una coda con attese lunghe, la finestra va allineata alla coda, non al contrario.
Cosa rispondo se la stessa chiave arriva con un corpo diverso?
409, con un errore leggibile che dice quale chiave ha un corpo diverso. Non restituire la prima risposta e non sovrascrivere il record: entrambe le scelte nascondono un bug del client.
Quali errori posso ritentare senza rischi?
408, 429, 500, 502, 503 e 504 sono temporanei. Un 400, un 401, un 403 o un 422 segnalano un problema che un secondo tentativo non risolve, e ripeterli consuma quota e log.
Serve un lock distribuito per gestire i retry?
Di solito no. Il vincolo di unicità sulla chiave e una transazione bastano: il secondo inserimento fallisce e il server legge la risposta salvata. Un lock distribuito aggiunge coordinamento e un punto di guasto in più.