Server-Sent Events (SSE)
SSE — технология однонаправленного потока данных от сервера к клиенту поверх обычного HTTP-соединения, с автоматическим переподключением.
Зачем нужно
Для уведомлений, live-обновлений и потоковой передачи (streaming LLM-ответов) WebSocket избыточен: нужен только поток сервер → клиент. SSE работает поверх HTTP, проходит через любые прокси и CDN, имеет встроенное переподключение и прост в реализации — в отличие от WebSocket не требует смены протокола.
Где используется
- Live-обновления: цены акций, спортивные счёта, статус задачи
- AI-чаты (ChatGPT, Claude) — потоковый вывод токенов
- Уведомления в реальном времени без диалога (push только от сервера)
- CI/CD лог-стриминг (GitHub Actions, Vercel deploy logs)
Клиент: EventSource API
// Открываем SSE-соединение
const es = new EventSource('/api/events');
// Обработчик сообщений по умолчанию (type: message)
es.addEventListener('message', event => {
console.log('Данные:', event.data);
console.log('ID:', event.lastEventId);
});
// Именованные события
es.addEventListener('user-joined', event => {
const user = JSON.parse(event.data);
console.log(`Вошёл: ${user.name}`);
});
es.addEventListener('notification', event => {
const { title, body } = JSON.parse(event.data);
showNotification(title, body);
});
// Ошибки и закрытие
es.addEventListener('error', err => {
if (es.readyState === EventSource.CLOSED) {
console.log('Соединение закрыто');
}
// При сетевой ошибке — браузер переподключается автоматически
});
// Закрыть явно
function stopListening() {
es.close();
}
SSE с авторизацией (fetch + ReadableStream)
// EventSource не поддерживает заголовки — используем fetch
async function connectSSE(url, token, onEvent) {
const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read;
if (done) break;
const text = decoder.decode(value);
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
onEvent(data);
}
}
}
}
Сервер: формат SSE-ответа
// Express
app.get('/api/events', (req, res) => {
// Обязательные заголовки
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders; // отправляем заголовки сразу
// Отправка события
const send = (data, event = 'message', id = null) => {
if (id) res.write(`id: ${id}\n`);
if (event !== 'message') res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`); // \n\n — разделитель событий
};
send({ status: 'connected' });
const interval = setInterval(() => {
send({ time: new Date.toISOString() });
}, 1000);
// Очистка при закрытии соединения
req.on('close', () => {
clearInterval(interval);
res.end();
});
});
Формат SSE-потока
id: 1
event: notification
data: {"title":"Новое сообщение","body":"Привет!"}
id: 2
data: {"status":"ok"}
: это комментарий — используется как keepalive
retry: 3000
Частые ошибки
- Не устанавливают
Content-Type: text/event-stream— браузер не распознаёт SSE - Не очищают ресурсы при событии
closeзапроса — утечка памяти и CPU - Используют SSE для двусторонней коммуникации — нужен WebSocket
- Не обрабатывают
CORSдля SSE с другого домена —EventSourceтребует CORS как fetch