Signatur
Beschreibung
Die Schnittstelle EventSource ist die clientseitige Schnittstelle für Server-sent Events (SSE). Mit new EventSource(url) wird eine dauerhafte HTTP-Verbindung aufgebaut. Der Server sendet darüber Ereignisse im Format text/event-stream. Jede empfangene Nachricht löst im Browser ein Ereignis aus: standardmäßig message, bei benannten Ereignissen (event: name) ein Ereignis mit genau diesem Namen.
SSE ist unidirektional, Daten fließen also nur vom Server zum Client. Damit eignet es sich für Live-Ticker, Benachrichtigungen, Fortschrittsanzeigen, Log-Streams oder das Streamen von Antworten großer Sprachmodelle (LLMs). Anders als bei WebSockets läuft alles über gewöhnliches HTTP. Der Browser baut abgebrochene Verbindungen automatisch neu auf und schickt dabei die zuletzt empfangene Ereignis-ID im Header Last-Event-ID mit.
Wichtige Bestandteile:
- Eigenschaften:
readyState(0=CONNECTING,1=OPEN,2=CLOSED),urlundwithCredentials. - Methode:
close()beendet die Verbindung endgültig. Danach findet kein automatischer Neuaufbau mehr statt. - Ereignisse:
open,message,errorsowie beliebige benannte Ereignisse. Registrieren lassen sie sich überaddEventListener()oder die Eigenschaftenonopen,onmessageundonerror.
Benannte Ereignisse lassen sich nur mit addEventListener() abonnieren. Der Handler onmessage erhält ausschließlich Nachrichten ohne event:-Feld.
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $url Pflicht | string | URL | Adresse des Endpunkts, der text/event-stream liefert. Relative Angaben werden relativ zur Adresse des Dokuments aufgelöst. |
|
| $options | { withCredentials?: boolean } | { withCredentials: false } | Konfiguration. Mit withCredentials: true werden bei Anfragen an andere Ursprünge (Cross-Origin) Cookies und Anmeldedaten mitgesendet. Der Server muss das per CORS erlauben. |
Rückgabewert
EventSource-Instanz. Sie beginnt sofort mit dem Verbindungsaufbau (readyState === 0).Beispiele
Verbindung öffnen und Nachrichten empfangen
const source = new EventSource('/api/stream');
source.onopen = () => {
console.log('Verbunden, readyState:', source.readyState);
};
source.onmessage = (event) => {
// event.data ist immer ein String
const payload = JSON.parse(event.data);
console.log('Neue Nachricht:', payload, 'ID:', event.lastEventId);
};
source.onerror = () => {
if (source.readyState === EventSource.CONNECTING) {
console.warn('Verbindung unterbrochen – Browser verbindet neu …');
} else if (source.readyState === EventSource.CLOSED) {
console.error('Verbindung endgültig geschlossen.');
}
};
Benannte Ereignisse mit addEventListener abonnieren und wieder entfernen
const source = new EventSource('https://api.example.com/notifications', {
withCredentials: true
});
// Server sendet z. B.:
// event: order-shipped
// data: {"orderId": 1234}
const onShipped = (event) => {
const { orderId } = JSON.parse(event.data);
console.log(`Bestellung ${orderId} wurde versandt`);
};
source.addEventListener('order-shipped', onShipped);
// Später: nur diesen Listener entfernen
setTimeout(() => {
source.removeEventListener('order-shipped', onShipped);
}, 60_000);
Fortschrittsanzeige mit sauberem Schließen
const trackJob = (jobId) => new Promise((resolve, reject) => {
const source = new EventSource(`/jobs/${jobId}/progress`);
source.addEventListener('progress', (e) => {
console.log(`Fortschritt: ${e.data} %`);
});
source.addEventListener('done', (e) => {
source.close(); // verhindert automatischen Neuaufbau
resolve(JSON.parse(e.data));
});
source.addEventListener('error', () => {
if (source.readyState === EventSource.CLOSED) {
reject(new Error('Stream abgebrochen'));
}
});
});
const result = await trackJob('abc123');
console.log('Ergebnis:', result);
Minimaler Node.js-Server (Gegenstück)
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++;
res.write(`id: ${id}\nretry: 5000\ndata: ${JSON.stringify({ time: Date.now() })}\n\n`);
}, 1000);
req.on('close', () => clearInterval(timer));
}).listen(3000);
// Wichtig · Fallstricke
- Nur GET und keine eigenen Header:
EventSourcekann wederAuthorization-Header noch einen Anfragetext (Body) senden. Zur Authentifizierung dienen Cookies (withCredentials) oder ein Token in der URL. Ein Token in der URL landet allerdings in Server-Logs. Wer POST oder eigene Header braucht, nutzt stattdessenfetch()mitReadableStream. - Verbindungslimit bei HTTP/1.1: Browser erlauben pro Domain nur etwa 6 gleichzeitige Verbindungen, und das gilt über alle Tabs hinweg. Mehrere offene Tabs mit SSE können so die Seite blockieren. HTTP/2 hebt diese Grenze praktisch auf.
- Automatischer Neuaufbau: Nach einem Netzwerkfehler verbindet der Browser selbstständig neu. Den Abstand legt der Server über das Feld
retry:fest (in Millisekunden). Antwortet der Server mit einem HTTP-Status außer 200 oder mit einem falschenContent-Type, wird die Verbindung endgültig geschlossen (readyState === 2). Der Server kann den Neuaufbau auch gezielt mit Status 204 unterbinden. - Immer
close()aufrufen, wenn der Stream nicht mehr gebraucht wird, etwa beim Unmount einer Komponente. Sonst bleiben Verbindungen offen, und durch die ständigen Neuaufbau-Versuche entstehen Speicherlecks. - Proxys und Puffer: Reverse-Proxys wie nginx puffern Antworten mitunter, sodass Nachrichten verzögert ankommen. Abhilfe schafft der Header
X-Accel-Buffering: no. Kompression (gzip) kann ebenfalls puffern. - Sicherheit:
event.dataist ein unbehandelter String. Niemals ungeprüft perinnerHTMLeinfügen, sonst droht Cross-Site-Scripting (XSS). Bei Cross-Origin-Anfragen gilt CORS. Außerdem muss eine gesetzte Content Security Policy den Endpunkt inconnect-srcerlauben. - Kompatibilität: Wird von allen modernen Browsern unterstützt, auch in Web Workern. Der Internet Explorer kennt kein
EventSource. Node.js bringt keine globale Implementierung mit (nur experimentell); dort helfen Pakete wieeventsource.