Idempotenza nelle API: evitare richieste duplicate, ordini doppi e automazioni impazzite 🔁🧠

Un cliente clicca due volte sul pulsante di pagamento. Un webhook viene consegnato nuovamente perché la prima risposta è arrivata troppo tardi. Un job in background riparte dopo un errore di rete. Situazioni normalissime, eppure possono produrre conseguenze fastidiose: due ordini, due email, due addebiti o due record identici. 🔁

Quando progetto integrazioni tra applicazioni, cerco di ricordare una regola: non posso sempre impedire che una richiesta venga ripetuta, ma posso progettare il sistema affinché la ripetizione non provochi effetti indesiderati. È qui che entra in gioco l’idempotenza.

Che cosa significa idempotenza?

In informatica, un’operazione è idempotente quando ripeterla una o più volte produce lo stesso effetto sullo stato del sistema di eseguirla una sola volta. Un esempio semplice: impostare lo stato di una risorsa su “attivo” è potenzialmente idempotente; aggiungere ogni volta 10 euro al saldo non lo è.

La distinzione riguarda l’effetto finale, non necessariamente il contenuto identico delle risposte HTTP. Una seconda richiesta potrebbe restituire un messaggio diverso e rimanere comunque idempotente rispetto ai dati memorizzati.

Perché le richieste duplicate sono inevitabili 🌐

Internet non garantisce che ogni richiesta arrivi una sola volta. Una connessione può interrompersi dopo che il server ha già completato l’operazione, lasciando il client senza risposta. Il client riprova, ma il server potrebbe aver già creato l’ordine.

Lo stesso accade con le code e con molti sistemi di webhook: la consegna può essere ripetuta per aumentare l’affidabilità. È quindi rischioso interpretare ogni nuova chiamata come un evento necessariamente nuovo.

Questo problema si collega alla gestione delle attività pianificate: nell’articolo su WP-Cron e cron di sistema ho spiegato perché le esecuzioni possono sovrapporsi e perché il semplice scheduler non basta a garantire risultati corretti.

GET, PUT e POST: cosa dice HTTP?

La semantica HTTP considera metodi come GET, PUT e DELETE idempotenti dal punto di vista dell’effetto previsto della richiesta. POST, invece, non lo è necessariamente. Questo non significa che ogni implementazione rispetti automaticamente tali proprietà: il comportamento reale dipende dal codice applicativo.

Un endpoint POST che crea un nuovo ordine a ogni chiamata può generare duplicati. Un endpoint progettato per riconoscere la stessa operazione, invece, può evitare di ripeterla anche se il client usa POST.

Le idempotency key: riconoscere la stessa operazione 🔑

Una soluzione comune consiste nell’associare a una richiesta una chiave di idempotenza, cioè un identificatore univoco generato dal client per quella specifica operazione.

POST /api/orders
Idempotency-Key: ordine-utente-123-operazione-456

Il valore è soltanto illustrativo: in produzione userei identificatori sufficientemente imprevedibili e univoci, generati secondo le caratteristiche del sistema.

Quando riceve la prima richiesta, il server registra la chiave insieme all’operazione e al suo risultato. Se arriva una richiesta con la stessa chiave, può restituire il risultato già ottenuto invece di creare un secondo ordine.

Attenzione: la chiave dovrebbe essere associata anche ai parametri significativi della richiesta. Riutilizzare la stessa chiave con dati differenti non dovrebbe provocare silenziosamente il riuso di un risultato incompatibile.

La parte difficile è la concorrenza ⚠️

Un controllo ingenuo potrebbe fare così: “Cerco la chiave nel database; se non esiste, creo l’ordine”. Il problema è che due richieste contemporanee possono controllare entrambe prima che una delle due abbia salvato il risultato.

Per evitare la corsa, servono meccanismi atomici: per esempio un vincolo UNIQUE sulla chiave nel database, transazioni dove appropriate e una gestione esplicita dello stato “in elaborazione”.

Un indice univoco è spesso più affidabile di un semplice controllo applicativo, perché è il database a impedire l’inserimento contemporaneo dello stesso identificatore.

Un possibile schema MariaDB 🗄️

Per un servizio che registra le operazioni, potrei partire da una tabella dedicata:

CREATE TABLE api_operations (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  idempotency_key VARCHAR(128) NOT NULL,
  request_hash CHAR(64) NOT NULL,
  status VARCHAR(20) NOT NULL,
  response_code SMALLINT UNSIGNED NULL,
  created_at DATETIME NOT NULL,
  UNIQUE KEY uq_idempotency_key (idempotency_key)
);

È uno schema dimostrativo, non un’implementazione completa. In un sistema reale valuterei anche la separazione per account, la scadenza delle chiavi, il recupero dopo crash, la conservazione del risultato e la protezione dei dati sensibili. Il vincolo univoco dovrebbe coprire l’ambito in cui una chiave deve essere unica.

Webhook: non fidarti del fatto che un evento arrivi una sola volta 🔔

Quando ricevo un webhook, cerco di distinguere tra identità dell’evento e tentativo di consegna. Lo stesso evento potrebbe arrivare più volte con tentativi diversi. Se il provider fornisce un identificatore stabile dell’evento, posso registrarlo e usarlo per evitare di applicare nuovamente lo stesso effetto.

Questo non sostituisce la verifica della firma del webhook. La firma aiuta a verificare l’autenticità del messaggio; l’idempotenza impedisce che un messaggio autentico, ma ripetuto, produca effetti doppi.

Ho affrontato il funzionamento generale delle integrazioni nell’articolo su webhook e automazione degli eventi.

Retry, timeout e backoff ⏱️

Le richieste possono fallire temporaneamente. Riprovare è spesso corretto, ma i tentativi dovrebbero essere limitati e distribuiti nel tempo, per esempio con un backoff esponenziale e una componente casuale. Senza questi accorgimenti, centinaia di client possono sovraccaricare un servizio già in difficoltà.

Il retry deve inoltre distinguere tra errori temporanei e definitivi: ripetere all’infinito una richiesta non valida non la renderà corretta. Nei sistemi con code, prevederei anche una gestione degli eventi che continuano a fallire, per evitare blocchi silenziosi.

Gli errori più comuni da evitare ❌

  • Supporre che una richiesta HTTP venga consegnata una sola volta.
  • Controllare la presenza della chiave senza un vincolo atomico.
  • Riutilizzare la stessa chiave per operazioni differenti.
  • Considerare la firma del webhook una protezione contro i duplicati.
  • Effettuare retry senza limiti, timeout o backoff.
  • Non prevedere cosa accade se il processo si interrompe a metà operazione.

La checklist che userei in un nuovo progetto ✅

  1. Individuare le operazioni che non devono essere eseguite due volte.
  2. Definire un identificatore stabile per ciascuna operazione o evento.
  3. Salvare lo stato in modo atomico e gestire richieste concorrenti.
  4. Stabilire come rispondere a una richiesta duplicata.
  5. Prevedere scadenza, pulizia e monitoraggio delle chiavi.
  6. Testare timeout, crash e invii simultanei in staging.

Per approfondire la semantica dei metodi HTTP, il riferimento è la specifica HTTP Semantics (RFC 9110). Per le API che supportano chiavi di idempotenza, vale sempre la pena leggere anche la documentazione del provider: dettagli come durata delle chiavi e gestione delle richieste concorrenti possono variare.

L’idempotenza non è una funzionalità appariscente. È una di quelle decisioni architetturali che gli utenti non noteranno mai, almeno finché tutto funziona. Ed è proprio questo il suo valore. 🚀

Vuoi aggiungere qualcosa?

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *