Vai al contenuto
DedicatedPHP Contatto

Webhook fuori ordine in PHP senza corrompere lo stato

Progetta un'integrazione PHP resiliente a eventi duplicati, tardivi e concorrenti mediante validazione, audit e idempotenza.

Diagramma editoriale di eventi webhook duplicati e tardivi elaborati da un'applicazione PHP con controllo dello stato

I webhook fuori ordine in PHP sono un problema di coerenza, non solo di connettività. Un provider può inviare nuovamente una consegna perché non ha ricevuto una risposta valida, una coda può ritardare un messaggio oppure due eventi della stessa entità possono percorrere percorsi diversi. Se l'applicazione presuppone che ogni evento arrivi una sola volta e in sequenza, una conferma precedente può sovrascrivere un annullamento successivo, oppure una ripetizione può eseguire due volte un'operazione irreversibile.

La regola di partenza è semplice: un webhook è una notifica del fatto che qualcosa potrebbe essere cambiato in un altro sistema. Non è, di per sé, un'istruzione affidabile per modificare lo stato locale senza verifiche. Il design deve conservare l'evidenza ricevuta, decidere quali eventi sono ammissibili e applicare le modifiche in modo idempotente e ordinato secondo le regole del dominio.

Separare ricezione, validazione e applicazione al dominio

Separare ricezione, validazione e applicazione al dominio — guía visual de DedicatedPHP

L'endpoint HTTP deve fare poco e farlo in modo prevedibile. La sua responsabilità è ricevere la richiesta, verificarla, persistere un record immutabile e rispondere entro il termine previsto dal mittente. Il lavoro che modifica ordini, abbonamenti, inventario o qualsiasi altra entità di business dovrebbe avvenire in seguito, normalmente tramite un processo asincrono.

Separare le fasi evita che un'interruzione transitoria di un'API interna trasformi una consegna valida in un tentativo ambiguo. Consente inoltre di riprendere l'elaborazione senza chiedere al provider di inviare nuovamente eventi precedenti.

  1. Ricezione: acquisire header, corpo non trasformato, istante di ricezione e origine identificata.
  2. Validazione dell'input: verificare firma, formato, dimensione, tipo di contenuto e campi minimi.
  3. Persistenza: archiviare l'evento e il suo stato iniziale in una transazione breve.
  4. Accodamento: segnalare che esiste lavoro in sospeso, senza dipendere dalla sua elaborazione all'interno della risposta HTTP.
  5. Applicazione: un worker interpreta l'evento, ottiene lo stato necessario ed esegue una transizione di business controllata.

È importante distinguere una consegna da un evento. La stessa consegna può ripetersi e alcuni provider assegnano un identificatore diverso a ogni tentativo di consegna. Se esiste un identificatore di evento stabile, quello è solitamente la base migliore per la deduplicazione. Se non esiste, sarà necessario definire una chiave con origine, entità esterna, tipo e una versione o marca temporale dal significato noto.

Cosa registrare per poter eseguire audit e rielaborare

Una tabella degli eventi non deve conservare solo il JSON interpretato. Conserva il corpo originale, perché normalizzarlo prima di archiviarlo può eliminare informazioni necessarie per verificare una firma, analizzare un incidente o adattare un parser successivo.

Come minimo, il record dovrebbe contenere:

  • Origine o provider e ambiente di integrazione.
  • Identificatore esterno dell'evento e, se presente, identificatore della consegna.
  • Tipo di evento, identificatore dell'entità esterna e versione, sequenza o data effettiva.
  • Header rilevanti e payload originale protetto da modifiche.
  • Istante di ricezione locale e, separatamente, la marca temporale dichiarata dal mittente.
  • Impronta crittografica del payload per diagnostica e deduplicazione ausiliaria.
  • Stato di elaborazione: ricevuto, validato, in sospeso, applicato, ignorato, fallito o in revisione.
  • Numero di tentativi, errore riassunto, istante dell'ultimo tentativo e riferimento all'entità locale interessata.

Un vincolo univoco su (origine, external_event_id) risolve la ripetizione quando il provider offre un ID stabile. Inserisci prima e tratta il conflitto come una consegna già nota, non come un errore di business. La risposta può continuare a essere positiva per fermare i tentativi.

Ma deduplicare il messaggio non basta a garantire l'idempotenza. Per esempio, due eventi distinti possono esprimere la stessa conferma e tentare entrambi di creare un movimento contabile. L'operazione di business deve disporre di una propria protezione: una chiave di idempotenza, un vincolo univoco sull'effetto o una transizione che verifichi se il risultato esiste già.

Validare l'autenticità e limitare la superficie di input

Non accettare un webhook perché proviene da un indirizzo IP previsto o perché include un campo che sembra segreto. Quando il provider lo consente, valida una firma calcolata sul corpo grezzo e una marca temporale. Il confronto deve avvenire in tempo costante e la finestra temporale deve limitare i replay, tenendo conto del ragionevole disallineamento degli orologi.

Prima della persistenza, imponi limiti operativi: dimensione massima del corpo, tempo di lettura, formati accettati e schema minimo. Un JSON valido non è necessariamente un evento valido. Rifiuta i tipi sconosciuti se non esiste una policy esplicita per archiviarli senza applicare effetti.

I segreti di firma richiedono rotazione. Durante un cambiamento, può essere necessario accettare una chiave precedente e una nuova per un periodo delimitato, registrando quale ha validato la consegna. Non includere corpi completi, token o dati personali non necessari nei log dell'applicazione. Il registro di audit deve disporre di controlli di accesso e di una policy di conservazione adeguata alla sensibilità dei dati.

Decidere l'ordine logico, non fidarsi dell'ordine di rete

L'ora di ricezione non definisce cosa sia avvenuto prima. Nemmeno una data inclusa nel payload è sempre sufficiente: può essere approssimativa, riferirsi alla creazione dell'evento e non alla transizione, oppure essere influenzata da orologi non sincronizzati. Il segnale migliore è una versione monotona o un numero di sequenza per entità fornito dal sistema di origine.

Quando esiste una versione, memorizza l'ultima versione applicata nell'entità locale. Un worker può applicare un evento solo se la sua versione è superiore a quella memorizzata; una versione uguale indica una ripetizione e una inferiore è un evento tardivo. Se vi sono lacune nella sequenza, non inventare lo stato intermedio: contrassegna l'entità per la riconciliazione oppure consulta l'API sorgente, se quell'API è il sistema di riferimento.

Se non esistono né sequenza né versione, le regole devono appartenere al dominio. Una macchina a stati esplicita è più sicura dell'assegnazione diretta di un testo ricevuto. Per esempio, un'entità annullata potrebbe impedire il ritorno allo stato confermato salvo una transizione documentata e autorizzata. Il modello deve definire cosa fare con ogni combinazione di stato corrente ed evento in arrivo.

if ($eventVersion <= $entity->lastExternalVersion) {
    markIgnored($event, 'version_non_piu_recente');
    return;
}

applyAllowedTransition($entity, $event);
$entity->lastExternalVersion = $eventVersion;

Il codice illustra il criterio, non sostituisce la transazione né le regole di transizione. Per eventi senza versione, un confronto tra date è accettabile solo se il contratto del mittente ne garantisce semantica e precisione.

Trattare gli eventi tardivi in base al costo dell'errore

Non tutti gli eventi ritardati meritano la stessa risposta. Scegliere tra ignorare, registrare, ricalcolare o compensare dipende dal fatto che l'evento possa modificare un'obbligazione reale e da quale sia la fonte di verità.

  • Ignorare: appropriato per una versione precedente il cui effetto è già incluso in uno stato successivo verificabile.
  • Registrare e avvisare: utile se la sequenza è incoerente o mancano informazioni per decidere senza intervento.
  • Ricalcolare: consultare lo stato corrente nel sistema esterno e aggiornare il mirror locale quando prevale la fonte esterna.
  • Compensare: creare un'azione correttiva tracciabile quando un effetto precedente ha già prodotto conseguenze e non può essere eliminato in modo sicuro.

Considera un caso ipotetico di un'operazione esterna. Arriva una conferma con versione 12, poi un annullamento con versione 13 e, più tardi, viene ritentata la conferma 12. Con il controllo della versione, la ripetizione non riattiva l'operazione. Se l'annullamento arriva per primo e il sistema sa che manca la versione 12, può applicare l'annullamento se la macchina a stati lo consente oppure richiedere una riconciliazione prima di produrre un effetto sensibile.

Concorrenza interna, code e lock per entità

L'elaborazione asincrona migliora la capacità di risposta, ma introduce race condition interne: due worker possono leggere lo stesso stato prima che uno dei due scriva. La deduplicazione dell'evento non evita questa condizione.

Per le entità sensibili, serializza per chiave dell'entità esterna o locale. Questo può essere ottenuto con partizioni di coda basate su tale chiave, un lock distribuito con scadenza progettato con cura oppure un lock di riga all'interno di una transazione breve. Un'altra opzione è il controllo ottimistico: aggiornare solo se la versione memorizzata è ancora quella prevista e ritentare quando viene rilevato un conflitto.

Evita di mantenere una transazione aperta mentre chiami servizi remoti. Prima riserva o leggi lo stato in modo coerente; poi effettua la chiamata con una chiave idempotente, quando possibile; infine registra il risultato. Se il processo fallisce tra i passaggi, un nuovo tentativo deve poter distinguere un'operazione in sospeso da una già completata.

Operatività, osservabilità e test prima della pubblicazione

Operatività, osservabilità e test prima della pubblicazione — guía visual de DedicatedPHP

Un pannello operativo deve mostrare quanti eventi restano in sospeso, falliscono ripetutamente, vengono ignorati per anzianità, sono rifiutati per firma e presentano lacune nella sequenza. Misura anche l'anzianità della coda e il tempo dalla ricezione all'applicazione. Questi segnali consentono di rilevare un'integrazione degradata prima che il disallineamento diventi un problema di business.

Conserva meccanismi di rielaborazione che partano dall'evento originale e da una versione esplicita del parser o dell'handler. Rielaborare non significa eseguire alla cieca: limita l'ambito, registra chi lo ha richiesto e mantieni attive le stesse garanzie di idempotenza.

Lista di controllo

  • Inviare lo stesso evento più volte, anche in modo concorrente.
  • Consegnare un annullamento prima della relativa conferma.
  • Ritardare un evento precedente fino a dopo uno con una versione superiore.
  • Introdurre lacune, tipi sconosciuti, payload troncati e firme non valide.
  • Simulare l'interruzione del worker dopo aver creato un effetto esterno e prima di contrassegnare l'evento come applicato.
  • Verificare che due worker sulla stessa entità non producano una transizione impossibile.
  • Verificare che la rielaborazione preservi l'audit e non duplichi gli effetti.

L'integrazione robusta non cerca di forzare la rete a consegnare in ordine. Progetta un confine affidabile: conserva ogni input verificabile, applica regole di business idempotenti, usa un ordine logico quando esiste e riconcilia quando non può conoscere lo stato con certezza.

Vuoi applicare queste idee al tuo progetto?Parliamo della tua piattaforma PHP.
Visualizza il servizio correlato