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

Связанные темы

Ресурсы