Menu

Validazione ai confini delle API: client, server e il divario tra loro

La validazione ai confini delle API appartiene a entrambi i lati: al client per un riscontro immediato, al server per l'autorità. Confondere i due produce messaggi che traggono in inganno gli utenti.

Pubblicato

  • validazione
  • API
  • dati di test

Un numero entra in un prodotto in più di un punto. Viene digitato in un modulo, riecheggiato attraverso un client, trasmesso a un servizio, memorizzato e in seguito riletto da un operatore. Ognuno di quei punti è un confine, e ogni confine ha un motivo diverso per controllare il valore.

I team che li trattano tutti come lo stesso controllo finiscono con una logica duplicata che si allontana, e con il peggio di entrambi i mondi: un riscontro lento perché il controllo autorevole è remoto, e un’autorità debole perché il controllo veloce è l’unico che viene davvero eseguito. Separare i livelli sistema entrambi i problemi.

La validazione deve stare sul client o sul server?

Su entrambi, e per motivi diversi. Il client è dove conta la latenza. Un utente che digita un identificatore lungo vuole sapere subito di un carattere digitato male, e un’andata e ritorno per ogni pressione di tasto è il modo sbagliato di dirglielo. I controlli locali, offline, che non richiedono alcuna consultazione appartengono a questo livello.

Il server è dove conta l’autorità. Tutto ciò che il client afferma può essere falsificato, aggirato chiamando direttamente l’endpoint, o prodotto da una build più vecchia con un insieme di regole obsoleto. Il servizio deve rieseguire il controllo su qualsiasi cosa accetti, non perché diffidi del proprio client ma perché il client non fa parte del suo confine di fiducia.

I due livelli dovrebbero condividere un’unica specifica e un’unica implementazione, pubblicate come pacchetto o artefatto generato, così il controllo veloce e quello autorevole non possono essere in disaccordo su come appare un valore valido. Quando invece si allontanano, il sintomo è un input che il client accetta e il server rifiuta, che gli utenti vivono come un fallimento inesplicabile alla fine di un modulo.

Gli esempi discussi qui sono strutturali. Nessun numero reale di conto, identità o carta dovrebbe comparire in un registro di richieste, in una fixture di test o in un payload di errore, e i valori descritti in questo articolo sono soltanto forme illustrative.

Cosa controllare al confine e cosa lasciar passare

Il confine dovrebbe confermare ciò che può confermare a basso costo e passare il resto più avanti come dati.

Controlla al margine: il set di caratteri, la lunghezza, la forma normalizzata e ogni carattere di controllo pubblicato per uno schema che il servizio conosce. Rifiuta presto e con un motivo specifico, perché l’alternativa è un valore malformato che viaggia più in profondità in un sistema che non ha un vocabolario per descriverlo.

Lascia passare: qualsiasi cosa che richieda una consultazione di registro, a meno che il servizio abbia un rapporto contrattuale che renda la consultazione economica. Un controllo di esistenza del conto su un endpoint pubblico è un vettore di negazione del servizio e un pericolo per la privacy allo stesso tempo, poiché trasforma un modulo in un oracolo per indovinare se un valore è attivo.

Normalizza una volta, al confine, e memorizza la forma normalizzata come valore canonico conservando l’originale per la visualizzazione. Tutto ciò che sta a valle confronta allora un’unica rappresentazione, e la classe di bug in cui lo stesso numero appare in tre formati scompare.

Classificare le risposte di errore e la loro semantica di ritentativo

Non ogni rifiuto significa la stessa cosa, e un unico codice di errore per tutti costringe ogni chiamante a indovinare come rispondere.

Situazione Significato Ripetibile
Input malformato Il valore infrange la regola di formato No — il chiamante deve inviare dati diversi
Carattere di controllo fallito La forma è lecita ma l’aritmetica non concorda No — stesso motivo
Schema non supportato Nulla di ciò che il servizio implementa corrisponde a questa forma No — a meno che il servizio aggiunga copertura
Temporaneamente non disponibile Una dipendenza necessaria al controllo è inattiva o limitata Sì, con attesa progressiva
Frequenza limitata Il chiamante ha superato una quota Sì, dopo l’intervallo indicato dalla risposta

Ridurre i primi quattro a una generica richiesta errata è l’errore di progetto più comune, perché rende un errore di input permanente indistinguibile da un’interruzione temporanea. I client ritentano allora i fallimenti sbagliati o rinunciano a quelli che sarebbero riusciti.

Restituisci un codice stabile leggibile dalla macchina accanto a un messaggio leggibile dall’uomo, e documenta quali codici sono ripetibili. Tratta quella classificazione come parte del contratto dell’interfaccia: cambiarla in seguito è una modifica che rompe la compatibilità per chiunque vi abbia collegato una logica di ritentativo.

Perché un controllo fallito non deve essere segnalato come numero mancante?

Perché le due affermazioni hanno condizioni di verità diverse. Un carattere di controllo fallito dice che la stringa non concorda con se stessa, cosa che il servizio sa con certezza. Un numero mancante dice che nessun simile conto o record esiste, cosa che il servizio di solito non può sapere affatto.

La confusione causa danni reali in entrambe le direzioni. A un utente con un valore autentico a cui viene detto che un numero non esiste può capitare di abbandonare una transazione legittima o di inserire qualcos’altro. A un utente a cui viene detto che un numero è valido perché un controllo è passato può capitare di credere che un conto sia stato confermato quando lo è stata solo l’aritmetica.

Il messaggio giusto descrive la stringa: il formato corrispondeva, oppure la cifra di controllo non reggeva, oppure nessuna regola implementata riconosce l’input. Nulla in quell’elenco afferma qualcosa sul mondo, e ogni voce dice al chiamante qualcosa su cui può agire. La stessa disciplina vale dentro i processi a lotti, dove una categoria etichettata male può mandare un revisore a caccia di un difetto che non esiste; la categorizzazione usata per i file grandi è costruita esattamente su questa distinzione.

Limiti di frequenza, timeout e ripieghi per le consultazioni esterne

Non appena un controllo ha bisogno di dati esterni al processo, acquisisce i modi di fallire di una chiamata di rete. Progettare per essi non è pessimismo; è la differenza tra un degrado e un’interruzione.

Ogni chiamata esterna ha bisogno di un timeout più breve della richiesta che la contiene, così che una dipendenza lenta non consumi l’intero budget. Ha bisogno di una politica di ritentativi con attesa progressiva e jitter per i fallimenti temporanei, e di un interruttore di sicurezza così che una dipendenza che fallisce persistentemente smetta di essere chiamata a pieno ritmo.

Ha anche bisogno di un comportamento definito per il caso in cui la consultazione non possa avvenire. Due risposte sono legittime, e la scelta è una decisione di prodotto: far fallire la richiesta, oppure accettare il valore provvisoriamente e contrassegnarlo come non verificato. Ciò che non è legittimo è trattare silenziosamente una consultazione irraggiungibile come un superamento, perché questo converte un’interruzione in un problema di qualità dei dati.

La cache aiuta e ha bisogno delle sue regole. Metti in cache solo ciò che il contratto permette, usa come chiave il valore normalizzato e dai alle voci una durata commisurata alla rapidità con cui il fatto sottostante può cambiare. Non mettere mai in cache un rifiuto come se fosse un fatto verificato sul numero.

Per gli sviluppatori: contratti, versioni e log

Tratta l’insieme di regole come una dipendenza con versione dell’API, non come un dettaglio implementativo.

Esponi quale versione dell’insieme di regole ha prodotto un giudizio, così un chiamante può capire se un cambiamento di comportamento è venuto dalla propria build o dal servizio. Versiona l’insieme di regole indipendentemente dall’endpoint quando cambia la copertura, e mantieni disponibili le versioni vecchie abbastanza a lungo perché i client migrino. Quando uno schema pubblica nuovi parametri, il cambiamento dovrebbe essere un aggiornamento dei dati con un nuovo numero di versione, non una modifica al codice le cui note di rilascio omettono la differenza di comportamento.

Registra i giudizi, mai i valori completi. Registra lo schema, il giudizio, la versione dell’insieme di regole e un identificatore di correlazione, e tieni il valore fuori dalla riga di log del tutto — compresi i percorsi di errore, che è dove i valori trapelati compaiono più spesso.

Infine, ricorda cosa il confine non può stabilire. Un formato validato è un’affermazione su una stringa, come il caso di solo formato rende chiaro per gli schemi privi di qualsiasi aritmetica, e il modello a tre livelli della validazione è il riferimento per mantenere separati i livelli.

Passi successivi

Prendi l’endpoint che riceve il tuo identificatore più sensibile ed elenca ogni controllo che esegue, contrassegnando ciascuno come aritmetica locale o consultazione esterna. Poi conferma che il client esegua presto quelli locali e che il server li esegua tutti di nuovo; lo strumento di validazione dei numeri mostra la formulazione dei giudizi che un client può riutilizzare in sicurezza senza affermare più di quanto l’aritmetica supporti.

Continua a leggere

Guide su Validatore di numeri di carta e documenti d'identità