Prevenire le Race Condition nel Refresh dei Token nelle Applicazioni Single-Page
Gli utenti vengono disconnessi senza spiegazione. Il token di sessione sembra valido in localStorage, l'ultima richiesta riuscita è avvenuta pochi secondi fa, e il pattern è esasperantemente inconsistente — tranne che tende a raggrupparsi attorno ai caricamenti di pagina che attivano più chiamate API concorrenti. Questo è uno dei bug di produzione più confusi nelle applicazioni single-page, e la causa principale è quasi sempre la stessa: una race condition nel refresh del token.
Il meccanismo è semplice una volta identificato. I JWT scadono. Quando un token scade a metà sessione, il comportamento corretto è scambiare il refresh token con un nuovo access token e ripetere la richiesta originale in modo trasparente. In realtà, però, le applicazioni React raramente fanno una richiesta alla volta. Una pagina dashboard potrebbe recuperare simultaneamente dati utente, riepilogo account, conteggio notifiche e attività recenti — tutto al montaggio del componente, tutto in parallelo. Se l'access token scade durante quella raffica, ognuna di quelle richieste riceve un 401. Ognuna di esse rileva indipendentemente il token scaduto. Ognuna di esse tenta di chiamare l'endpoint di refresh. La prima chiamata riesce e il vecchio refresh token viene consumato. La seconda chiamata tenta lo stesso refresh token — ora invalidato — e fallisce. L'utente viene disconnesso.
Perché raramente emerge in sviluppo
La race condition richiede una coincidenza specifica: più richieste veramente concorrenti, combinate con una scadenza del token che avviene esattamente nel momento sbagliato. Gli ambienti di sviluppo tipicamente hanno lunghe durate del token, la latenza di localhost è quasi zero quindi le richieste si completano rapidamente e raramente si sovrappongono completamente, e la maggior parte degli sviluppatori testa una funzionalità alla volta piuttosto che caricare pagine ricche di dati. Il risultato è che questo bug si nasconde comodamente in sviluppo ed emerge in produzione con pattern di utilizzo reali — più spesso influenzando gli utenti che sono stati attivi abbastanza a lungo da far scadere il loro token iniziale a metà sessione.
Il pattern singleton promise
La soluzione standard è il pattern singleton promise: garantire che solo una richiesta di refresh venga mai in-flight alla volta, e che tutte le altre richieste che necessitano di un refresh attendano il completamento di quella singola promessa invece di avviarne le proprie.
L'implementazione core è un modulo a livello globale che mantiene un riferimento a qualsiasi richiesta di refresh in-flight:
let refreshPromise: Promise<string> | null = null;
async function getValidToken(): Promise<string> {
const token = getStoredToken();
if (\!isExpired(token)) return token;
if (\!refreshPromise) {
refreshPromise = refreshAccessToken().finally(() => {
refreshPromise = null;
});
}
return refreshPromise;
}
Quando la prima richiesta rileva un token scaduto, crea la promise di refresh e la assegna a refreshPromise. Quando la seconda richiesta arriva millisecondi dopo, refreshPromise è già non-null — quindi attende la stessa promise invece di crearne una nuova. Il refresh endpoint viene chiamato una volta. Tutti i richiedenti ricevono il nuovo token quando si risolve.
Implementazione dell'interceptor queue
Il pattern singleton gestisce il refresh, ma le richieste in-flight che hanno già ricevuto un 401 prima che il refresh si completasse devono ancora essere gestite. L'approccio della coda degli interceptor risolve questo accodando le richieste fallite, eseguendo il refresh una volta e poi riprovando tutte le richieste accodate con il nuovo token.
Gestione specifica di Supabase
Supabase gestisce internamente il refresh del token attraverso il suo client, ma pattern simili si applicano quando si costruiscono proxy o layer wrapper attorno al client Supabase. Il client Supabase ha un proprio meccanismo di refresh built-in che gestisce le race condition, ma quando si costruiscono chiamate API personalizzate o si accede direttamente all'endpoint REST, si ricrea il problema.
Segnali osservabili
I sintomi caratteristici di una race condition nel refresh del token sono disconnessioni sporadiche che si verificano dopo lunghi periodi di inattività o durante i caricamenti di pagina che avviano molte richieste. I log di errore mostreranno cluster di errori 401 per richieste che dovrebbero essere autenticate, a volte seguiti da errori del refresh token invalid. L'utilizzo del refresh token è il segnale più diretto: se si vede il refresh endpoint chiamato più volte in rapida successione dalla stessa sessione, si ha una race condition.