Procedure ottimali per la gestione degli errori

Gestione degli errori software: come classificare, registrare e ripristinare i sistemi in produzione in caso di errori.

La gestione degli errori non è una funzionalità da aggiungere dopo che il sistema funziona. È una decisione di progettazione che determina il comportamento del sistema quando qualcosa smette di funzionare, il che in produzione è una questione di "quando", non di "se". Le reti vanno in timeout. I database diventano temporaneamente non disponibili. Gli utenti inviano input che violano ogni presupposto dello sviluppatore. I servizi esterni restituiscono risposte inaspettate. L'hardware si guasta. Il sistema che gestisce tutte queste condizioni in modo prevedibile, senza corrompere i dati o esporre informazioni sensibili, è ben progettato. Il sistema che si blocca, corrompe silenziosamente lo stato o divulga dettagli di implementazione interni quando si verifica una qualsiasi di queste situazioni presenta un problema strutturale che nessun sviluppo di funzionalità potrà risolvere.

Gestione degli errori per l'intero codice sorgente

SMART TS XL Rileva le eccezioni non gestite e le lacune nella gestione degli errori in ogni linguaggio e piattaforma del tuo ambiente.

Esplora SMART TS XL

Le conseguenze pratiche di una gestione inadeguata degli errori non sono ipotetiche. La gestione impropria degli errori è ora esplicitamente riconosciuta come uno dei rischi per la sicurezza più critici nello sviluppo del software: OWASP A10:2025 (La gestione errata delle condizioni eccezionali si concentra sulla gestione impropria degli errori, sugli errori logici, sul failing in modalità aperta e su altri scenari correlati derivanti da condizioni anomale) che i sistemi possono incontrare. Questa è una nuova categoria nella OWASP Top 10 del 2025, che riflette una comprensione matura di come i fallimenti nella gestione degli errori producano non solo instabilità operativa, ma anche vulnerabilità di sicurezza sfruttabili. Tra le debolezze più rilevanti in questa categoria si annoverano CWE-209 Generazione di messaggi di errore contenenti informazioni sensibili, CWE-476 Dereferenziazione di puntatori NULL e CWE-636 Mancata gestione sicura dei guasti. Ognuna di queste vulnerabilità è prevenibile con pratiche di gestione degli errori disciplinate e applicate in modo coerente all'intero codice sorgente.

Che cos'è la gestione degli errori nello sviluppo software?

La gestione degli errori è l'insieme dei meccanismi mediante i quali un sistema software rileva, classifica e risponde alle condizioni che impediscono la normale esecuzione. Include la gestione delle eccezioni, la gestione dello stato di errore, la registrazione diagnostica, la comunicazione dei guasti agli utenti o ai sistemi a valle e il ripristino o la terminazione controllata del processo interessato. Un sistema con una corretta gestione degli errori non è un sistema che non si guasta mai: è un sistema che risponde ai guasti in modo prevedibile, senza corruzione dei dati, senza esporre informazioni sensibili e senza propagare il guasto ai componenti che potrebbero altrimenti continuare a funzionare.

Questa distinzione, tra guasto prevedibile e guasto caotico, è operativamente significativa. Un sistema che si guasta in modo prevedibile produce log chiari, attiva meccanismi di ripristino definiti e fornisce al team operativo le informazioni necessarie per diagnosticare e risolvere il problema. Un sistema che si guasta in modo caotico produce log incompleti, permette a errori silenziosi di compromettere lo stato prima che si manifesti un guasto visibile e costringe il team di reperibilità a dedicare la maggior parte del tempo a ricostruire l'accaduto anziché a risolverlo. La differenza tra un incidente di dieci minuti e uno di tre ore spesso non risiede nel guasto in sé, ma nella qualità della gestione degli errori che lo accompagna.

La gestione degli errori ha anche implicazioni dirette per la sicurezza. Il problema di sicurezza più comune causato da una gestione degli errori impropria si verifica quando messaggi di errore interni dettagliati, come stack trace, dump del database e codici di errore, vengono visualizzati all'utente. Questi messaggi rivelano dettagli di implementazione che non dovrebbero mai essere divulgati, fornendo agli hacker indizi importanti su potenziali vulnerabilità del sito. Una gestione efficace degli errori mantiene una netta separazione tra le informazioni diagnostiche registrate internamente e le informazioni restituite agli utenti o esposte tramite API.

Tipologie di errori software e come identificarli

Gli errori software non costituiscono una categoria uniforme. Differiscono per il momento in cui si verificano, per le modalità di rilevamento, per la risposta che richiedono e per la possibilità di automatizzare tale risposta. Comprendere la tassonomia è il prerequisito per progettare una strategia di gestione appropriata per ogni tipo di errore, anziché applicare lo stesso meccanismo a tutti.

Errori di sintassi

Gli errori di sintassi si verificano quando il codice viola le regole grammaticali del linguaggio di programmazione. I compilatori e gli interpreti li rilevano prima dell'esecuzione, rendendoli la categoria più facile da gestire: non possono raggiungere l'ambiente di produzione nei sistemi con pipeline di build automatizzate. Nei linguaggi interpretati come Python o JavaScript, tuttavia, gli errori di sintassi nei percorsi di codice non testati dalla suite di test possono raggiungere l'ambiente di produzione e causare errori di runtime alla prima esecuzione di tali percorsi. Gli strumenti di linting e di analisi statica individuano gli errori di sintassi in questi ambienti prima del deployment.

Errori di runtime

Gli errori di runtime si verificano durante l'esecuzione quando il programma incontra una condizione che non può gestire tramite il normale flusso di controllo: un dereferenziamento di puntatore nullo, una divisione per zero, un file inesistente, una connessione di rete non funzionante, un database temporaneamente non disponibile. Sono l'obiettivo principale dei meccanismi di gestione degli errori nei sistemi di produzione perché sono imprevedibili, dipendono da condizioni esterne al controllo del codice e possono verificarsi in qualsiasi momento durante l'esecuzione di una transazione.

Gli errori di runtime si suddividono ulteriormente in condizioni recuperabili e non recuperabili, e questa è la classificazione operativa più importante che il sistema di gestione degli errori deve effettuare. Un errore temporaneo di connessione al database è un errore di runtime recuperabile: riprovare dopo un breve ritardo ha buone probabilità di successo. Un file di configurazione danneggiato che impedisce l'inizializzazione dell'applicazione è un errore di runtime non recuperabile: riprovare non servirà a nulla e la risposta corretta è la terminazione controllata con un messaggio diagnostico chiaro. Trattare queste due categorie in modo identico, applicando la stessa logica di ripetizione a una condizione che non può essere risolta con un nuovo tentativo, è una delle cause più comuni di comportamento incontrollato della gestione degli errori nei sistemi di produzione.

Errori logici

Gli errori di logica rappresentano la categoria più pericolosa proprio perché sono invisibili ai meccanismi standard di gestione degli errori. Il programma viene eseguito senza generare alcuna eccezione, ma produce risultati errati perché la logica implementata non corrisponde al comportamento previsto. Un calcolo dei prezzi con un errore di uno in un ciclo, un confronto di date che non tiene conto delle differenze di fuso orario, un controllo di autorizzazione che concede l'accesso al gruppo di utenti sbagliato: questi sono errori di logica. Non attivano alcun gestore di eccezioni, non compaiono in alcun registro degli errori e spesso propagano i loro risultati errati attraverso diversi sistemi a valle prima che qualcuno si accorga che qualcosa non va.

L'individuazione degli errori logici richiede la convalida dei risultati piuttosto che la semplice acquisizione delle eccezioni. Ciò significa utilizzare asserzioni che verifichino le post-condizioni, test di confronto che convalidino gli output rispetto a un riferimento noto e corretto e un sistema di monitoraggio che segnali eventuali deviazioni dei parametri aziendali dagli intervalli previsti.

errori di sistema

Gli errori di sistema hanno origine al di fuori del codice dell'applicazione: guasti hardware, esaurimento della memoria, limiti delle risorse del sistema operativo, guasti all'infrastruttura di rete. In genere non possono essere risolti dalla sola applicazione e richiedono risposte coordinate con il livello infrastrutturale: failover su componenti ridondanti, degrado controllato delle funzionalità o arresto controllato con notifica al team operativo. Il ruolo del codice dell'applicazione è quello di rilevare tempestivamente queste condizioni, rispondere con un degrado appropriato anziché con un guasto catastrofico e produrre informazioni diagnostiche che consentano al team infrastrutturale di comprendere cosa è successo.

La tabella seguente associa ciascun tipo di errore al relativo meccanismo di rilevamento e alla strategia di risposta appropriata:

Tipo di erroreQuando si verificaMeccanismo di rilevamentoStrategia di risposta
SintassiCompilare/interpretare il tempoCompilatore, linter, analisi staticaCorrezione prima dell'implementazione
Tempo di esecuzione (recuperabile) Try-catch, gestione delle eccezioniRiprova con backoff e percorso di fallback
Tempo di esecuzione (irrecuperabile) Try-catch, gestione delle eccezioniTerminazione controllata, escalation
Elementi Logici Validazione dei risultati, monitoraggioCorrezione logica, verifica dei dati
Sistema Monitoraggio delle infrastrutture, avvisiFailover, degradazione graduale

Conseguenze di una gestione impropria degli errori

Le conseguenze di una gestione inadeguata degli errori rientrano in quattro categorie, ognuna con un impatto diretto a livello operativo o aziendale. Comprenderle concretamente è ciò che giustifica l'investimento ingegneristico in un approccio sistematico alla gestione degli errori.

Instabilità dell'applicazione e guasti a cascata

Un'eccezione non gestita che si propaga fino alla cima dello stack di chiamate termina il processo o il thread che l'ha incontrata. In un'applicazione web, ciò significa che la richiesta dell'utente non riceve alcuna risposta oppure riceve una risposta di errore generica che non fornisce informazioni utili. Nei sistemi con transazioni attive o stato di sessione, la transazione può rimanere in uno stato parzialmente completato, incoerente dal punto di vista del database.

Nelle architetture a microservizi, l'instabilità delle applicazioni dovuta a errori non gestiti ha un effetto moltiplicatore. Un servizio che non implementa dei circuit breaker sulle sue dipendenze esterne, quando queste diventano lente o non disponibili, esaurirà il proprio pool di connessioni tentando di eseguire richieste che non vengono completate. Una volta esaurito il pool di connessioni, il servizio diventa non disponibile per i suoi chiamanti a monte, indipendentemente dal fatto che la causa principale li abbia coinvolti o meno. Una gestione inadeguata degli errori, come ignorare le eccezioni, divulgare dati sensibili nei messaggi di errore o fallire silenziosamente, è una fonte comune di bug e vulnerabilità di sicurezza. Fallire silenziosamente è particolarmente dannoso nei sistemi distribuiti perché consente al guasto di propagarsi in modo invisibile prima che venga generato un avviso.

Corruzione dell'integrità dei dati

Gli errori che si verificano nel mezzo di operazioni di scrittura a più fasi possono lasciare il sistema in uno stato incoerente se tali operazioni non sono racchiuse in transazioni atomiche. L'esempio canonico è l'elaborazione dei pagamenti: se l'addebito sul metodo di pagamento dell'utente va a buon fine ma la creazione del record d'ordine corrispondente fallisce senza attivare una transazione di compensazione, all'utente viene addebitato un acquisto che non esiste nel sistema. Risolvere questo problema a posteriori richiede una riconciliazione manuale, che è costosa, soggetta a errori e incompleta.

I problemi di integrità dei dati causati da una gestione degli errori inadeguata vengono spesso scoperti molto tempo dopo, quando i sistemi a valle che hanno utilizzato i dati errati hanno già intrapreso azioni basate su di essi. Il costo della correzione aumenta con il ritardo tra l'errore e la sua scoperta, motivo per cui la prevenzione tramite la progettazione di transazioni atomiche è significativamente più economica della correzione.

Vulnerabilità di sicurezza derivanti dall'output di errore

L'esposizione di dati sensibili tramite una gestione impropria degli errori del database, che rivela all'utente l'errore di sistema completo, fornisce agli aggressori le informazioni necessarie per creare attacchi più mirati. Questo rischio è ora formalmente classificato tra i primi dieci rischi per la sicurezza in OWASP 2025. Le tracce dello stack esposte nelle risposte HTTP rivelano le versioni del framework, i percorsi dei file, i nomi delle classi e le firme dei metodi. I messaggi di errore del database rivelano i nomi delle tabelle, i nomi delle colonne e le strutture delle query. Questi dettagli riducono lo sforzo necessario per creare un attacco di SQL injection o di path traversal efficace, passando da un approccio basato su congetture a un targeting informato.

La soluzione richiede due cose: in primo luogo, che tutti i gestori di eccezioni al confine con l'utente restituiscano solo messaggi appropriati per l'utente, mai dettagli interni; in secondo luogo, che le informazioni diagnostiche interne vengano acquisite in un sistema di logging con controlli di accesso appropriati anziché essere scartate. Il messaggio all'utente e il messaggio diagnostico hanno scopi diversi e dovrebbero essere generati indipendentemente.

Debiti di manutenzione derivanti da una gestione incoerente degli errori

Le codebase prive di un approccio standardizzato alla gestione degli errori accumulano debiti di manutenzione man mano che crescono. Ogni sviluppatore implementa le proprie convenzioni: alcuni utilizzano eccezioni personalizzate, altri restituiscono codici di errore, altri ancora registrano l'errore nel punto in cui si verifica, altri ancora lo propagano senza registrarlo. Il risultato è un sistema in cui ricostruire la causa di un errore in produzione richiede la lettura di più file di log con formati incompatibili, la comprensione di convenzioni di gestione degli errori che variano a seconda del modulo e di chi lo ha scritto, e spesso la scoperta che la vera causa principale non è stata registrata perché il blocco catch pertinente era vuoto o registrava solo un messaggio generico che ignorava il contesto originale dell'eccezione.

Procedure ottimali per la gestione degli errori nell'ingegneria del software

Le seguenti best practice non sono preferenze stilistiche. Ognuna di esse affronta una specifica modalità di errore che genera incidenti di produzione quando la pratica è assente. Sono ordinate dalla più elementare alla più avanzata, rispecchiando l'ordine in cui un team che sviluppa o aggiorna un sistema di gestione degli errori dovrebbe affrontarle.

Classificare gli errori come recuperabili o irrecuperabili al momento del rilevamento.

Ogni decisione relativa alla gestione degli errori inizia con una singola classificazione: questo errore può essere risolto senza intervento umano, oppure richiede un'escalation o la terminazione del processo? Questa classificazione dovrebbe avvenire nel momento in cui l'errore viene rilevato per la prima volta, e non essere rimandata a un livello superiore dello stack di chiamate dove il contesto che fornisce le informazioni per la classificazione è andato perduto.

Gli errori recuperabili sono quelli per i quali un nuovo tentativo, un percorso alternativo o una risposta con funzionalità ridotte possono completare l'operazione in modo accettabile. Gli errori non recuperabili sono quelli per i quali la prosecuzione dell'esecuzione produrrebbe risultati errati, corromperebbe i dati o creerebbe una vulnerabilità di sicurezza. L'assenza di un file di configurazione necessario, il rilevamento di un danneggiamento dei dati in un archivio critico e l'esaurimento di una risorsa senza possibilità di fallback sono esempi di errori non recuperabili. Un timeout di rete temporaneo, una risposta di limitazione della frequenza da un'API esterna e un servizio secondario temporaneamente non disponibile sono esempi di errori recuperabili.

Classificare erroneamente un errore irrecuperabile come recuperabile e applicarvi una logica di ripetizione genera tempeste di tentativi: un processo che si ripete all'infinito in una condizione che non può essere migliorata con ulteriori tentativi, consumando risorse che potrebbero essere utilizzate per gestire altre richieste. Classificare erroneamente un errore recuperabile come irrecuperabile e terminare il processo genera tempi di inattività non necessari. La classificazione è una decisione di progettazione che dovrebbe essere documentata per ogni tipo di errore, non presa ad hoc in ogni blocco catch.

Implementare la gestione centralizzata degli errori

La gestione centralizzata degli errori implica che un unico punto del sistema sia responsabile della ricezione, classificazione e registrazione degli errori con metadati standardizzati, nonché della definizione della politica di risposta. I singoli moduli rilevano e propagano gli errori, ma non sono responsabili del formato di registrazione, della soglia di allerta o della strategia di risposta. Questi parametri vengono definiti una sola volta dal gestore centralizzato e applicati in modo coerente.

In un'applicazione web, la gestione centralizzata degli errori assume in genere la forma di un componente middleware che cattura tutte le eccezioni non gestite al confine della richiesta, le registra con il contesto della richiesta (identificativo utente, identificativo richiesta, endpoint, durata), applica la logica di classificazione e restituisce una risposta appropriata alla classe di errore. I framework del linguaggio forniscono l'hook per questo: Express middleware in Node.js, @ControllerAdvice in Spring, componenti del confine di errore in React, app.errorhandler in Flask.

Il vantaggio principale è la coerenza. Ogni errore registrato in qualsiasi punto del sistema ha lo stesso formato. Ogni errore che attraversa il confine dell'interfaccia utente viene filtrato attraverso la stessa logica di sanificazione. Ogni errore che supera una soglia di gravità definita attiva lo stesso avviso. Questa coerenza è ciò che rende l'analisi dei log e la risposta agli incidenti efficienti, anziché manuali.

Implementare il backoff esponenziale con jitter per i tentativi

I tentativi di ripetizione senza intervallo di tempo tra le richieste amplificano il problema che cercano di risolvere. Se un database è temporaneamente sovraccarico e cento client iniziano simultaneamente a ritentare le richieste fallite a intervalli di un secondo, il traffico generato dai tentativi di ripetizione può impedire completamente il ripristino del database. L'algoritmo di backoff esponenziale aumenta progressivamente il ritardo tra i tentativi, riducendo la pressione sul componente in errore e dandogli il tempo di riprendersi.

Il jitter introduce casualità nel ritardo per prevenire ondate di tentativi: se tutti i client utilizzano lo stesso schema di backoff deterministico, tutti riprovano nello stesso momento dopo ogni periodo di ritardo, riproducendo il problema di sincronizzazione. La randomizzazione del ritardo all'interno di un intervallo garantisce che il traffico di tentativi proveniente da più client sia distribuito nel tempo anziché sincronizzato.

I tentativi di ripetizione sono sicuri solo quando l'operazione ripetuta è idempotente, ovvero quando eseguirla più volte produce lo stesso risultato di eseguirla una sola volta. Le operazioni di lettura sono intrinsecamente idempotenti. Le operazioni di scrittura devono essere rese idempotenti per impostazione predefinita, in genere includendo una chiave di idempotenza nella richiesta che il server utilizza per eliminare i duplicati di più invii della stessa richiesta.

python

import time
import random

def with_retry(operation, max_attempts=4, base_delay_seconds=1.0):
    """
    Execute an operation with exponential backoff and jitter.
    Only retries on recoverable IOError and TimeoutError.
    Propagates all other exceptions immediately without retry.
    """
    for attempt in range(max_attempts):
        try:
            return operation()
        except (IOError, TimeoutError) as exc:
            if attempt == max_attempts - 1:
                raise  # exhausted retries, propagate
            delay = base_delay_seconds * (2 ** attempt) + random.uniform(0, 0.5)
            print(f"Attempt {attempt + 1} failed ({exc}). Retrying in {delay:.1f}s")
            time.sleep(delay)
        except Exception:
            raise  # unrecoverable, do not retry

Utilizzare la registrazione strutturata con contesto diagnostico completo

Una voce di registro che contiene solo il messaggio di eccezione senza contesto sull'operazione in esecuzione, sugli input ricevuti e sullo stato del sistema in quel momento, costringe il tecnico addetto al debug a riprodurre l'errore per comprenderlo. In produzione, la riproduzione è spesso impossibile. La registrazione strutturata cattura gli errori come oggetti con campi definiti: timestamp in formato ISO 8601, livello di gravità, identificatore univoco dell'errore, modulo e funzione, stack trace completo e campi di contesto specifici dell'operazione, come l'identificativo dell'utente, l'identificativo della richiesta e i parametri rilevanti per l'operazione che ha generato l'errore.

Questa struttura consente di effettuare query sul sistema di logging che non sarebbero possibili con il testo dei log non strutturato: tutti gli errori di timeout nel modulo pagamenti negli ultimi trenta minuti, tutti gli errori che interessano le richieste dell'utente con ID 12345 nelle ultime 24 ore, tutti gli errori in cui lo stack trace contiene un riferimento a una funzione specifica. Queste query sono ciò che rende efficiente l'analisi post-incidente.

Il messaggio di errore visualizzato all'utente è una questione distinta dalla voce del registro interno. La voce del registro dovrebbe contenere tutte le informazioni necessarie per la diagnosi. Il messaggio visualizzato all'utente non dovrebbe contenere dettagli di implementazione e dovrebbe informare l'utente su cosa è successo, se deve intraprendere qualche azione e cosa può fare se il problema persiste.

Come le piattaforme software dovrebbero notificare gli errori agli utenti

Una comunicazione efficace degli errori all'utente si basa su quattro principi. Primo, descrivere il problema in termini comprensibili all'utente, non in termini che riflettano la struttura interna del sistema. "Non è stato possibile elaborare il pagamento in questo momento" è preferibile a "Annullamento della transazione: violazione del vincolo sulla tabella degli ordini". Secondo, indicare se il problema è temporaneo o richiede un intervento da parte dell'utente. Un'interruzione temporanea del servizio giustifica un "riprova tra qualche minuto". Un errore di convalida giustifica un "verifica che il numero della tua carta sia corretto". Terzo, per gli errori che interessano le transazioni in corso, confermare esplicitamente lo stato della transazione. Se un pagamento non è stato addebitato, dirlo esplicitamente. Se l'ordine non è stato effettuato, dirlo esplicitamente. L'incertezza sullo stato della transazione è una fonte significativa di sfiducia da parte dell'utente. Quarto, fornire un percorso di supporto nel caso in cui l'utente non sia in grado di risolvere il problema autonomamente.

L'implementazione di questi principi richiede che il codice di gestione degli errori all'interfaccia utente abbia accesso alla classificazione degli errori (per determinare quale tipo di messaggio visualizzare), al contesto dell'errore (per rendere il messaggio specifico all'azione dell'utente) e a un sistema di modelli che produca formati di messaggio coerenti in tutta l'applicazione.

Progettazione a prova di errore: nega l'accesso in caso di errori nei controlli di sicurezza.

Un problema di sicurezza comune causato da una gestione degli errori impropria è il controllo di sicurezza fail-open. Tutti i meccanismi di sicurezza dovrebbero negare l'accesso finché non viene esplicitamente concesso, non concederlo finché non viene negato, che è una causa comune degli errori fail-open. Quando un controllo di autenticazione genera un'eccezione imprevista, il comportamento corretto è negare l'accesso. Quando un controllo di autorizzazione non riesce a recuperare i permessi dell'utente a causa di un errore del database, il comportamento corretto è negare l'accesso. Restituire un risultato che concede l'accesso quando il meccanismo che lo negherebbe non funziona è la definizione di fail-open ed è esplicitamente elencato nella categoria A10 di OWASP 2025 come schema di vulnerabilità critico.

Implementare una gestione degli errori a prova di errore nei controlli di sicurezza significa racchiudere il controllo in un gestore di errori che, in caso di eccezione, adotta per impostazione predefinita l'esito più restrittivo possibile. Significa non utilizzare mai un semplice blocco catch in un contesto sensibile alla sicurezza che consenta la prosecuzione dell'esecuzione. E significa testare i percorsi di gestione degli errori nei controlli di sicurezza con la stessa rigorosità riservata al percorso di esecuzione corretto.

Modelli di progettazione per la gestione degli errori nei sistemi distribuiti

Schema dell'interruttore automatico

Il pattern circuit breaker impedisce che i guasti in un servizio si propaghino a cascata ai suoi consumatori. Quando una dipendenza da un servizio supera una soglia di tasso di errore definita, il circuit breaker si apre e interrompe l'inoltro delle richieste a tale dipendenza, restituendo immediatamente un errore o una risposta di fallback senza attendere la risposta della dipendenza. Dopo un periodo di attesa configurabile, il circuit breaker passa a uno stato semiaperto che consente il passaggio di un piccolo numero di richieste di probing. Se queste hanno successo, il circuito si chiude e il traffico normale riprende. Se falliscono, il circuito si riapre e il periodo di attesa si azzera.

Senza interruttori di circuito, una dipendenza lenta o non disponibile causa il blocco dei thread del servizio che la utilizza, in attesa di risposte che potrebbero non arrivare mai. Il pool di thread si riempie, le nuove richieste non possono essere elaborate e il servizio stesso diventa non disponibile per i suoi chiamanti. L'interruttore di circuito trasforma un errore a cascata in un errore circoscritto: la dipendenza non è disponibile, ma il servizio che la utilizza rimane operativo e può gestire le richieste che non dipendono da quella specifica dipendenza.

Schema di paratia

Il pattern bulkhead isola i pool di risorse in base alle dipendenze, in modo che l'esaurimento di un pool non possa influire sulle richieste che non utilizzano tale dipendenza. In un servizio che chiama tre API esterne, assegnare a ciascuna API un proprio pool di thread significa che un'ondata di richieste lente all'API A esaurirà solo il pool di thread dell'API A. Le richieste alle API B e C continueranno a essere elaborate normalmente, perché i loro pool di thread sono separati.

Il confine di isolamento può essere applicato a livello di pool di thread, di pool di connessioni o di processo, a seconda della criticità dell'isolamento e del sovraccarico introdotto da ciascun approccio. Il principio è sempre lo stesso: il malfunzionamento di una dipendenza non deve essere in grado di consumare le risorse necessarie ad altre dipendenze.

Modello a saga per transazioni distribuite

Nei sistemi distribuiti in cui un'operazione aziendale si estende su più servizi, il mantenimento dell'integrità dei dati in caso di errore in una fase richiede una strategia di compensazione. Il pattern Saga definisce una sequenza di transazioni locali, ognuna delle quali ha una transazione di compensazione corrispondente che ne annulla l'effetto. Se la fase N della saga fallisce, la saga esegue le transazioni di compensazione per le fasi da N-1 a 1 in ordine inverso, ripristinando il sistema allo stato precedente alla saga.

Il pattern Saga non garantisce l'atomicità a livello di database: raggiunge la coerenza finale tramite compensazione anziché rollback. Ciò significa che, per un intervallo di tempo tra il successo di un passaggio e l'esecuzione della sua compensazione, il sistema potrebbe trovarsi in uno stato non previsto da alcuna regola aziendale. La gestione degli errori per ogni passaggio deve tenerne conto: le transazioni di compensazione devono essere idempotenti e l'orchestratore della saga deve essere progettato per sopravvivere ai guasti e riprendere dall'ultimo stato coerente.

Come prevenire la gestione non sicura degli output

La gestione non sicura dell'output nel contesto dei messaggi di errore è una delle categorie di vulnerabilità più frequentemente sfruttate nelle applicazioni web. Lo schema di attacco è diretto: forzare l'applicazione a generare un errore inviando input non validi, tipi di dati inattesi o valori limite che attivano percorsi di eccezione. Leggere il messaggio di errore o il corpo della risposta HTTP. Estrarre i dettagli di implementazione rivelati. Utilizzare tali dettagli per perfezionare l'attacco.

Per prevenire la gestione non sicura dell'output è necessario quanto segue:

Non includere mai i dettagli delle eccezioni interne nelle risposte destinate agli utenti. Il corpo della risposta HTTP, l'oggetto di errore JSON e la pagina di errore HTML che un utente riceve devono contenere un messaggio appropriato per l'utente e, facoltativamente, un codice di riferimento dell'errore che il personale di supporto può utilizzare per consultare la voce del registro interno. Non devono mai contenere una traccia dello stack, un'istruzione SQL, un percorso di file, un nome di classe o una versione del framework.

Verificare che il codice di gestione degli errori sia stato testato. I test unitari per le condizioni di errore dovrebbero verificare sia ciò che la risposta di errore non contiene, sia ciò che contiene. Un test che conferma che lo stato della risposta è 500 ma non verifica che il corpo della risposta non contenga stack trace è un test incompleto per questa vulnerabilità.

Utilizzare in modo coerente formati strutturati per le risposte agli errori. Uno schema standardizzato di risposta agli errori, applicato uniformemente a tutti gli endpoint, semplifica la verifica delle informazioni restituite e garantisce che non vengano inclusi dettagli interni. La formattazione ad hoc delle risposte agli errori è invece la causa di incongruenze e fughe di dati accidentali.

Registrare internamente tutti i dettagli diagnostici. Le informazioni diagnostiche che non devono essere incluse nella risposta rivolta all'utente devono essere acquisite in un luogo accessibile al team di ingegneri. Un sistema di logging con campi strutturati e controlli di accesso appropriati è la soluzione ideale. La chiamata di logging e la generazione della risposta rivolta all'utente devono essere operazioni esplicitamente separate nel codice di gestione degli errori, senza condividere una stringa di messaggio comune.

Un esempio concreto in Java che mostra la separazione tra la registrazione diagnostica e la risposta rivolta all'utente:

Giava

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpectedError(
        Exception ex, HttpServletRequest request) {

    // Full diagnostic context logged internally; never sent to the user
    String errorId = UUID.randomUUID().toString();
    log.error("Unhandled exception [errorId={}] [path={}] [userId={}]",
            errorId,
            request.getRequestURI(),
            getCurrentUserId(),
            ex);  // full stack trace captured in the log entry

    // User-facing response: error ID for support lookup, no internal details
    ErrorResponse response = new ErrorResponse(
            "An unexpected error occurred. Reference: " + errorId,
            Instant.now()
    );
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}

Questo schema garantisce che la traccia dello stack, la classe dell'eccezione e tutto il contesto interno vengano acquisiti nel log, mentre l'utente riceve solo un codice di riferimento che il personale di supporto può utilizzare per recuperare la voce di log corrispondente.

Analisi statica del codice per individuare lacune nella gestione degli errori

Le lacune nella gestione degli errori che più probabilmente causano incidenti in produzione non sono quelle ovvie che i revisori del codice individuano. Si tratta piuttosto di schemi strutturali che si accumulano silenziosamente in una codebase in continua crescita: blocchi catch vuoti che inghiottono le eccezioni senza registrarle, blocchi catch che registrano un messaggio generico ignorando l'eccezione originale, valori di ritorno degli errori che i chiamanti non controllano e gestori di eccezioni in percorsi di codice sensibili alla sicurezza che consentono la prosecuzione dell'esecuzione in caso di errore. Questi schemi sono invisibili ai revisori a meno che non li cerchino specificamente e, in una codebase di grandi dimensioni, esaminare ogni singolo blocco catch non è pratico.

Gli strumenti di analisi statica del codice affrontano questo problema in modo sistematico. Senza eseguire il codice, analizzano il codice sorgente trasformandolo in un albero sintattico astratto e interrogano tale struttura alla ricerca di modelli associati a una gestione degli errori errata. SonarQube e strumenti simili rilevano modelli di gestione degli errori insicuri e inaffidabili nel codice sorgente, inclusi blocchi catch vuoti, stack trace esposti e convalida mancante. L'analisi copre l'intera codebase in un'unica passata, non solo i file modificati di recente o i moduli che hanno causato incidenti di recente.

Per i sistemi aziendali che mescolano linguaggi, l'analisi deve coprire tutti i linguaggi presenti nell'ambiente. Un servizio Java che gestisce correttamente gli errori ma chiama un programma COBOL tramite un'interfaccia che non propaga gli errori dal livello mainframe presenta una lacuna nella gestione degli errori che un'analisi statica solo Java non può rilevare. Come discusso nel contesto di analisi statica del codice aziendale in diversi linguaggi di programmazioneUn'analisi unificata che abbracci ogni linguaggio del sistema è il prerequisito tecnico per individuare le lacune nella gestione degli errori a livello di sistema anziché a livello di file.

Per i sistemi legacy, il debito di gestione degli errori è in genere concentrato nelle parti più vecchie del codice sorgente, dove le convenzioni di gestione degli errori sono state stabilite prima che le pratiche moderne venissero standardizzate. Come esaminato nell'analisi di Modernizzazione dei sistemi legacy e gestione degli errori nei sistemi ereditatiIl passaggio da una gestione degli errori frammentata e incoerente a un approccio centralizzato e standardizzato è un processo di modernizzazione che trae vantaggio da strumenti automatizzati in grado di identificare lo stato attuale prima di apportare qualsiasi modifica.

Come SMART TS XL Affronta la gestione degli errori a livello di sistema.

SMART TS XL Questo modello crea un modello unificato di riferimento incrociato dell'intero ambiente software, acquisendo codice sorgente da ogni linguaggio e piattaforma, inclusi COBOL, JCL, Java, .NET, Python, JavaScript, TypeScript e SQL, e costruendo un indice strutturale che rappresenta le relazioni tra tutti i componenti. Per l'analisi della gestione degli errori, questo modello risponde a domande a cui gli strumenti per singoli linguaggi non possono rispondere: quali funzioni in un programma COBOL propagano gli errori ai chiamanti, quali chiamanti di tali funzioni gestiscono l'errore propagato e quali percorsi all'interno del sistema possono raggiungere un output rivolto all'utente senza alcuna gestione degli errori nella catena di chiamate.

La capacità di analisi dell'impatto della piattaforma estende questo concetto alla valutazione delle modifiche: prima di modificare il comportamento di gestione degli errori di un componente condiviso, l'analisi dell'impatto identifica ogni altro componente del sistema che dipende dal comportamento attuale, in modo che le modifiche possano essere pianificate e validate anziché implementate con conseguenze a valle sconosciute. Questa è l'analisi descritta nel documento soluzioni di analisi d'impatto che IN-COM fornisce per gli ambienti aziendali, applicato specificamente al problema di comprendere quali effetti avrà una modifica alla logica di gestione degli errori prima che tale modifica venga implementata.

SMART TS XLLa funzionalità di ricerca aziendale rende l'analisi navigabile: una query per tutte le funzioni del sistema che intercettano un'eccezione senza registrarla restituisce percorsi di file specifici e nomi di funzioni, organizzati per linguaggio e in base alla gravità dell'errore, calcolata in base al numero di chiamanti che accedono a quella funzione. Questa prioritizzazione è ciò che rende la risoluzione dei problemi di gestione degli errori un'operazione fattibile anziché complessa.

Gestione degli errori come proprietà a livello di sistema

Una gestione efficace degli errori non è una proprietà dei singoli moduli considerati isolatamente. Un modulo che gestisce correttamente i propri errori, ma opera all'interno di un sistema privo di logging centralizzato, di circuit breaker sulle dipendenze esterne e di un design basato su transazioni atomiche per le sue operazioni di scrittura a più fasi, produrrà comunque incidenti di produzione difficili da diagnosticare. La correttezza a livello di modulo è necessaria, ma non sufficiente.

Le proprietà a livello di sistema che rendono efficace la gestione degli errori nell'intera applicazione sono: una classificazione coerente degli errori, in modo che le condizioni recuperabili e non recuperabili vengano trattate in modo diverso a ogni livello; una registrazione centralizzata, in modo che tutti gli eventi di errore vengano acquisiti in un unico sistema interrogabile con metadati standardizzati; interruttori di circuito su tutte le dipendenze esterne, in modo che il guasto di una dipendenza non possa esaurire le risorse necessarie ad altre; una progettazione di transazioni atomiche per tutte le scritture a più fasi, in modo che il completamento parziale non possa produrre uno stato incoerente; e impostazioni predefinite a prova di errore in tutti i percorsi di codice sensibili alla sicurezza, in modo che gli errori nei controlli di accesso neghino anziché concedano l'accesso.

Integrare queste proprietà in un sistema che attualmente non le possiede è un lavoro incrementale, non un singolo evento di refactoring. Il percorso pratico prevede un'analisi statica per identificare le lacune attuali, la prioritizzazione di tali lacune in base al loro potenziale impatto su stabilità e sicurezza e una correzione progressiva a partire dai modelli a più alto rischio. Lo stato finale è un sistema in cui la gestione degli errori non è qualcosa a cui gli ingegneri pensano per ogni nuova funzionalità che scrivono, perché i modelli sono standardizzati, il framework li impone e la pipeline CI verifica che il nuovo codice non introduca gli anti-pattern che il team ha concordato di eliminare.