Una race condition nell'SSR: perché una variabile globale può mescolare due richieste
Un bug raro ma reale quando configurazione per-request e stato globale convivono nello stesso processo Node.
Nel browser una variabile globale appartiene, di fatto, alla singola sessione della pagina. In un processo SSR Node, invece, lo stesso spazio globale può essere condiviso da richieste differenti.
Questa differenza sembra banale finché una configurazione specifica della richiesta viene salvata globalmente.
Lo scenario
Immaginiamo un helper globale route() configurato con URL, host e route della richiesta corrente.
La sequenza sembra innocua:
- arriva la richiesta A;
- la configurazione globale viene impostata per A;
- il renderer esegue operazioni asincrone;
- arriva la richiesta B;
- la stessa configurazione globale viene sovrascritta;
- A riprende il rendering.
A questo punto A può terminare usando parte della configurazione di B.
Perché succede anche senza thread
Node usa un event loop. Non servono thread paralleli per avere una race condition logica.
È sufficiente interrompere il flusso della richiesta A con un await. Nel frattempo il processo può iniziare a gestire B, che modifica lo stato condiviso.
Il problema non è la libreria: è il lifetime dello stato
Una configurazione legata alla richiesta dovrebbe vivere quanto quella richiesta.
Se la mettiamo in uno scope globale, stiamo implicitamente promuovendo dati per-request a dati del processo.
È una violazione del lifetime corretto e, in sistemi concorrenti, prima o poi produce comportamenti difficili da riprodurre.
Come riprodurre il bug
Il test più utile non verifica una singola richiesta, ma due render concorrenti con configurazioni deliberatamente differenti.
Se A utilizza route che B non possiede, il test può rendere evidente che il renderer di A sta usando la configurazione sbagliata.
Questo è un buon esempio di test che nasce da uno scenario architetturale, non da una singola funzione.
La correzione
La configurazione deve essere installata nel punto più vicino possibile alla fase sincrona di rendering, evitando che tra assegnazione e utilizzo ci sia una finestra asincrona dove un'altra richiesta possa sovrascriverla.
Ancora meglio, quando l'API lo permette, è evitare completamente lo stato globale e passare la configurazione esplicitamente.
Una lezione generale per l'SSR
Con SSR bisogna rivalutare qualsiasi singleton, cache di modulo o variabile globale che contenga dati della richiesta.
Domande utili:
- questo valore cambia in base all'utente?
- dipende dall'URL corrente?
- dipende dall'host?
- può essere modificato prima che il rendering termini?
Se la risposta è sì, probabilmente non dovrebbe essere condiviso globalmente.
Conclusione
L'SSR trasforma un frontend React in parte di un'applicazione server concorrente. Questo cambia completamente il significato di “globale”.
Un pattern innocuo nel browser può diventare una race condition sul server. Ed è proprio per questo che testare richieste concorrenti vale molto più di verificare soltanto che una pagina venga renderizzata correttamente una volta.