JavaScript Server-Sent Events (EventSource)
Impara i Server-Sent Events in JavaScript con l'API EventSource: ricevi aggiornamenti in streaming dal server via HTTP, con riconnessione automatica ed eventi nominati.
I Server-Sent Events (SSE) sono un modo standard con cui un server può inviare in push un flusso continuo e unidirezionale di aggiornamenti testuali al browser attraverso una singola connessione HTTP a lunga durata. Invece che il client esegua ripetuti polling a un endpoint tramite fetch chiedendo "c'è qualcosa di nuovo?", la connessione rimane aperta e il server scrive messaggi ogni volta che ha qualcosa da inviare. Il browser espone questa funzionalità tramite l'interfaccia built-in EventSource, quindi non è necessaria alcuna libreria esterna per consumare uno stream.
Gli SSE eccellono quando gli aggiornamenti fluiscono in una sola direzione — dal server al client. Feed di notizie in diretta, notifiche, barre di avanzamento per build o upload, dashboard di monitoraggio e ticker azionari sono tutti casi d'uso naturali.
Ricevere uno stream con EventSource
Aprire uno stream richiede una sola riga. Si crea un EventSource puntato a un endpoint del server, poi si allegano i gestori per gli eventi che genera.
const es = new EventSource('/events');
es.onmessage = (event) => {
console.log('New message:', event.data);
};
es.onopen = () => {
console.log('Connection opened');
};
es.onerror = (error) => {
console.log('Connection error:', error);
};Vale la pena notare alcune cose:
- La connessione si apre non appena si costruisce l'
EventSource. onmessagesi attiva per ogni messaggio predefinito (senza nome) inviato dal server. Il payload è sempre una string, disponibile tramiteevent.data.onopensi attiva una volta che la connessione è stabilita (e nuovamente dopo una riconnessione).onerrorsi attiva quando la connessione cade o fallisce. A differenza di unafetchfallita, questo non significa necessariamente che si debba rinunciare — il browser di solito tenta di riconnettersi autonomamente (maggiori dettagli a seguire).
Ecco un esempio leggermente più completo che analizza payload JSON e aggiorna la pagina:
const es = new EventSource('/notifications');
es.onmessage = (event) => {
const data = JSON.parse(event.data);
const li = document.createElement('li');
li.textContent = data.message;
document.querySelector('#feed').append(li);
};Il formato wire
Il server risponde con il tipo di contenuto text/event-stream e scrive testo UTF-8 semplice in un formato basato su righe. Ogni messaggio è un blocco di righe campo: valore, e una riga vuota segna la fine di un messaggio.
data: Hello there
data: First line
data: Second line
event: priceUpdate
data: {"symbol":"ACME","price":42.10}
id: 42
data: This message has an id
retry: 10000
data: Reconnect after 10 seconds next timeI campi riconosciuti sono:
data:— il payload del messaggio. Se un messaggio ha più righedata:, vengono unite con un newline (\n) prima di essere consegnate comeevent.data.event:— un nome evento opzionale. Senza di esso, il messaggio viene consegnato aonmessage.id:— un identificatore opzionale che il browser memorizza per la riconnessione.retry:— il ritardo di riconnessione in millisecondi.
Le righe che iniziano con i due punti (:) sono commenti e vengono ignorati — i server spesso li inviano come ping keep-alive.
Eventi nominati
Per impostazione predefinita ogni messaggio va a onmessage. Un server può invece etichettare un messaggio con un campo event:, e il client ascolta quel nome specifico tramite addEventListener. Questo consente di instradare diversi tipi di aggiornamenti a gestori differenti sulla stessa connessione.
Dato uno stream come questo:
event: priceUpdate
data: {"symbol":"ACME","price":42.10}
event: newsAlert
data: Markets closed early today
data: a plain default messageIl client si configura come segue:
const es = new EventSource('/market');
es.addEventListener('priceUpdate', (event) => {
const { symbol, price } = JSON.parse(event.data);
console.log(`${symbol} is now ${price}`);
});
es.addEventListener('newsAlert', (event) => {
console.log('News:', event.data);
});
// Messages with no `event:` field still land here.
es.onmessage = (event) => {
console.log('Default message:', event.data);
};Riconnessione automatica
Questa è la caratteristica che distingue gli SSE dall'implementare manualmente lo streaming con fetch. Se la connessione cade — una rete instabile, un riavvio del server, un timeout del proxy — il browser si riconnette automaticamente dopo un breve ritardo. Non è necessario scrivere alcun ciclo di retry.
Per rendere la ripresa affidabile, il browser tiene traccia dell'ultimo id: ricevuto. Alla riconnessione invia quel valore nell'header di richiesta Last-Event-ID, così il server può riprendere esattamente da dove lo stream si era interrotto anziché riprodurre tutto dall'inizio.
GET /events HTTP/1.1
Last-Event-ID: 42
Accept: text/event-streamIl server può anche regolare il ritardo prima della prossima riconnessione inviando un campo retry: (in millisecondi):
retry: 5000
data: The browser will wait 5 seconds before reconnecting if droppedLa riconnessione automatica avviene solo quando la connessione è considerata recuperabile. Se il server risponde a una riconnessione con un errore HTTP come 204 No Content o uno stato 4xx, il browser considera lo stream terminato e smette di tentare.
Stato della connessione e chiusura
Un EventSource espone il suo stato corrente tramite la proprietà readyState, che corrisponde a tre costanti:
EventSource.CONNECTING(0) — in connessione, o in attesa di riconnettersi.EventSource.OPEN(1) — connesso e in ricezione.EventSource.CLOSED(2) — chiuso e non si riconnetterà.
Quando lo stream non è più necessario, chiamare es.close(). Questo chiude la connessione immediatamente e — cosa importante — il browser non si riconnetterà in seguito.
const es = new EventSource('/events');
// Stop listening after 30 seconds.
setTimeout(() => {
es.close();
console.log('readyState is now', es.readyState); // 2 (CLOSED)
}, 30000);Richieste cross-origin e credenziali
Per impostazione predefinita un EventSource segue la same-origin policy. Per fare streaming da un'origine diversa, il server deve inviare gli opportuni header CORS (come Access-Control-Allow-Origin).
I cookie non vengono inviati nelle richieste cross-origin a meno che non si faccia opt-in. Passare l'opzione withCredentials, e assicurarsi che il server consenta le credenziali nella sua configurazione CORS:
const es = new EventSource('https://api.example.com/events', {
withCredentials: true,
});Una nota sul lato server
Gli SSE sono principalmente una funzionalità del browser, ma il server deve cooperare. Qualunque linguaggio si utilizzi, la ricetta è la stessa: rispondere con Content-Type: text/event-stream, mantenere la connessione aperta invece di terminare la risposta, e scrivere chunk in formato evento man mano che i dati diventano disponibili.
Ecco un esempio minimo con Node.js per mostrarne la struttura:
import http from 'node:http';
http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
let id = 0;
const timer = setInterval(() => {
id += 1;
res.write(`id: ${id}\n`);
res.write(`data: The time is ${new Date().toISOString()}\n\n`);
}, 1000);
// Stop the timer when the client disconnects.
req.on('close', () => clearInterval(timer));
}).listen(3000);Ogni chunk termina con una riga vuota (\n\n) per completare il messaggio. Il client connesso con new EventSource('http://localhost:3000') riceverebbe un aggiornamento al secondo.
Limitazioni
Gli SSE trasportano solo testo (UTF-8) — non esiste un tipo di frame binario, quindi non è possibile trasmettere in streaming byte grezzi come immagini o audio senza prima codificarli. Sono anche strettamente unidirezionali: per inviare dati al server è comunque necessaria una normale richiesta tramite fetch o XMLHttpRequest, oppure una connessione WebSocket full-duplex.
Un ulteriore limite pratico: su HTTP/1.1, i browser limitano il numero di connessioni simultanee verso una singola origine a circa sei. Poiché ogni EventSource mantiene aperta una connessione, aprire molte schede sullo stesso sito può esaurire quel budget. Questo è molto meno preoccupante su HTTP/2, dove molti stream vengono multiplexati su una singola connessione.
SSE vs. WebSockets
Gli SSE e i WebSockets forniscono entrambi dati in tempo reale, ma risolvono problemi diversi.
| Server-Sent Events | WebSockets | |
|---|---|---|
| Direzione | Unidirezionale (server → client) | Bidirezionale (full duplex) |
| Protocollo | HTTP semplice | Upgrade ws:// / wss:// |
| Dati | Solo testo (UTF-8) | Testo e binario |
| Riconnessione | Automatica, integrata | Da implementare manualmente |
| Ideale per | Feed, notifiche, avanzamento | Chat, giochi, editing collaborativo |
Scegliere SSE quando il browser deve solo ascoltare — notifiche, feed in diretta, dashboard, aggiornamenti di avanzamento. La riconnessione automatica, il trasporto HTTP semplice e la piccola API client ne fanno lo strumento più semplice per il lavoro. Scegliere WebSockets quando il client deve anche inviare frequentemente, quando si ha bisogno di dati binari, o quando la latenza per messaggio è importante, come nelle app di chat e nei giochi multiplayer.
Se i propri dati sono già gestiti tramite un flusso asincrono basato su promise, abbinare i gestori SSE con async/await per eventuali richieste successive (ad esempio, recuperare i dettagli completi quando arriva una notifica leggera) mantiene il codice pulito e leggibile.