← Все статьи

SiteGuard12 мин чтения

SSE приходит с задержкой или обрывается: как проверить поток через прокси

Страница открывается, соединение с сервером отмечено как успешное, но новые статусы появляются пачкой раз в минуту. Или обновления работают до первой паузы, затем интерфейс замирает. Для сайта с уведомлениями, результатами обработки и статусами заказов такой сбой заметен посетителю раньше, чем обычная ошибка доступности. Проверять нужно весь путь события: создание, отправку, прохождение посредников, получение браузером и применение в интерфейсе.

Когда Server Sent Events не работает, отключение буферизации в одном месте может оказаться лишь частью решения. Ниже — последовательность диагностики SSE через прокси, пример ограниченной настройки nginx и критерии, по которым команда может подтвердить исправление. Все адреса, события, интервалы и измерения в примерах учебные; выбирать рабочие значения нужно по требованиям конкретного сайта.

Что именно перестало работать: соединение, событие или интерфейс

Начните с наблюдаемого результата. «Не приходят уведомления» может означать, что браузер не открыл поток, получил байты с неправильным форматом, не вызвал нужный обработчик либо применил событие к уже закрытому экрану. Для каждого этапа нужны отдельные свидетельства. Один статус HTTP не показывает, сколько времени пользователь ждал изменения.

SSE передаётся в обычном HTTP-ответе. Для принимаемого EventSource потока требуется успешный статус 200 и тип text/event-stream; условия обработки ответа определены в спецификации WHATWG. Ожидать WebSocket-переключение с кодом 101 или добавлять заголовки Upgrade для SSE не требуется.

Зафиксируйте URL, версию приложения, время открытия, пользователя тестового аккаунта и ожидаемое событие. Разделите время до открытия соединения, задержку доставки события и время до обновления интерфейса. Это позволит обсуждать с подрядчиком конкретный участок, а не общее ощущение медленной работы сайта.

Владелец продукта дополнительно определяет допустимое поведение устаревшего экрана. Например, статус обработки можно снабдить временем последнего подтверждения и кнопкой безопасного обновления данных. Повторная отправка исходного заказа ради проверки статуса создаёт совсем другую операцию. Такой сценарий нужно исключить ещё на уровне интерфейса, даже если техническое восстановление потока обычно происходит быстро.

Проверьте тип ответа и завершение каждого события

В руководстве MDN по SSE описан текстовый поток UTF-8: блоки событий разделяются пустой строкой, строки с двоеточием в начале являются комментариями. Именованное событие принимается обработчиком соответствующего имени. Поэтому корректные байты могут приходить вовремя, но обработчик onmessage не получит событие с именем order-status.

Учебный ответ для тестового заказа выглядит так. После JSON оставлена пустая строка: она входит в формат сообщения.

HTTP/1.1 200 OK
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache
X-Accel-Buffering: no

: keepalive

id: evt-104
event: order-status
data: {"order_id":"demo-27","status":"accepted"}

Для этого события клиент подписывается через addEventListener("order-status", handler). Значение data остаётся текстом до разбора JSON кодом приложения. Проверяйте отдельно корректность SSE и корректность вложенных данных: ошибка JSON внутри обработчика отличается от отсутствия события в потоке.

По правилам WHATWG, завершающая пустая строка нужна для передачи сформированного события обработчику. Пока поток открыт, незавершённый блок остаётся в ожидании продолжения. При достижении конца потока такой блок отбрасывается и не выдаётся как событие. Поэтому закрытие соединения не заменяет разделитель, а статус 200 не подтверждает правильность формата.

Получите исходный ответ без лишнего буфера в инструменте

Откройте вкладку Network браузера и найдите именно запрос потока. Проверьте окончательный URL после перенаправлений, статус, заголовки и первые байты тела. Страница входа, HTML ошибки прокси или JSON с описанием отказа не становятся SSE от того, что запрос создан через EventSource. Полезно сохранить обезличенный пример ответа вместе с временем проверки.

Для отдельного тестового маршрута разработчик может использовать такую команду:

curl --no-buffer --include 'https://stream.example/events'

Опция --no-buffer, согласно официальному руководству curl, отключает буферизацию вывода самого curl. Она не управляет сервером, nginx или CDN. Если передать вывод другой программе, собственная буферизация этой программы снова может исказить впечатление о времени поступления строк.

Сравните публичный маршрут и разрешённый диагностический доступ к приложению за прокси. Сохраняйте сопоставимые параметры: авторизацию, маршрут, фильтр событий, сжатие и тестовые данные. Успех неавторизованного демопотока не доказывает исправность личных уведомлений. Доступ к origin организуют через предусмотренный защищённый канал; ради проверки не нужно открывать внутренний сервер всем посетителям.

Найдите слой, который удерживает готовые данные

Составьте фактическую цепочку доставки: обработчик приложения, библиотека ответа, сервер приложения, обратный прокси, балансировщик, CDN, браузер. В конкретном проекте часть звеньев отсутствует, другие повторяются. Рядом с каждым укажите, кто отвечает за настройку и где можно измерить получение байтов. Такой список быстро показывает, к кому обращаться после проверки приложения.

Запись строки в объект ответа ещё не является доказательством отправки в сеть. Рантайм может ожидать заполнения буфера, middleware сжатия — накопления блока, промежуточный узел — своей порции данных. Команда проверяет потоковый режим и доступный механизм flush на каждом используемом слое. Успешный вызов flush в обработчике не подтверждает немедленную доставку посетителю.

Диагностируйте по одной границе. Если прямое подключение получает событие сразу, а публичное — позже, исследуйте добавленную часть пути. Если задержка уже присутствует у origin, начните с приложения и его сервера. Не отключайте сжатие или кеширование глобально без измерений: локальный эксперимент на SSE-маршруте даёт более понятный результат и ограничивает влияние изменений.

После каждого изменения повторяйте одинаковую последовательность тестовых событий. Иначе улучшение может совпасть с меньшей нагрузкой или изменившимся размером сообщения, а причина задержки останется неизвестной.

SSE и nginx buffering: настройка конкретного маршрута

В документации ngx_http_proxy_module управление буферизацией ответа отделено от кеширования. proxy_buffering off отключает буферизацию проксируемого ответа; proxy_cache off отменяет кеширование, унаследованное с предыдущего уровня. Заголовок Cache-Control сам по себе не выключает proxy buffering.

Следующий учебный фрагмент относится к уже существующему location, который обслуживает SSE через proxy_pass. Это три настройки для проверки, а не готовый конфигурационный файл:

proxy_buffering off;
proxy_cache off;
proxy_read_timeout 75s;

Сначала установите, какой блок действительно выбран для URL, включая переписывание адреса и вложенные правила. Затем согласуйте значения с текущей конфигурацией маршрута, проверьте синтаксис и примените изменение штатной процедурой. Если приложение подключено через FastCGI, приведённые proxy-директивы не являются настройкой его FastCGI-буферов.

Приложение также может отправить X-Accel-Buffering: no, но обработка этого заголовка отключается через proxy_ignore_headers. Кроме того, nginx по умолчанию не передаёт клиенту заголовки X-Accel-*: отсутствие заголовка в браузере не доказывает, что приложение его не выдало. Проверяйте ответ upstream и эффективную конфигурацию.

proxy_read_timeout ограничивает интервал между последовательными чтениями от upstream, а не полную продолжительность ответа. Значение 75 секунд здесь условное. Исправление принимают по повторному измерению потока через публичный маршрут; наличие нужных строк в файле ещё не подтверждает результат.

Как доказать задержку на учебном примере

Предположим, команда отправляет три тестовых события с интервалом две секунды. Часы участников измерения синхронизированы, точность журналов известна. Прямой диагностический клиент получает каждое событие через 0,1 секунды, а клиент через публичный маршрут фиксирует следующую картину:

СобытиеСоздано приложениемПолучено публичным клиентомЗадержка
evt-20112:00:0012:00:1010 секунд
evt-20212:00:0212:00:108 секунд
evt-20312:00:0412:00:106 секунд

Поступление пачкой при быстрой прямой доставке указывает на возможное накопление данных в дополнительной части пути. Это пока не доказательство ошибки именно nginx: там могут находиться другой прокси, сжатие или диагностический инструмент. Следующий шаг — повторить измерение по границам этой части цепочки.

Для реального расследования сохраняйте ID события, время создания, наблюдения на доступных границах, получения обработчиком и применения в интерфейсе. Учитывайте погрешность часов. Если обработчик сработал быстро, а экран изменился поздно, настройка nginx не исправит очередь задач JavaScript. Если само событие создано поздно, сначала проверяют производство события.

Подберите heartbeat по ограничениям всей цепочки

При отсутствии деловых событий соединение всё равно нуждается в проверке жизнеспособности. Комментарий вида : keepalive с завершающей пустой строкой позволяет передать данные, не создавая прикладное сообщение. Однако запись комментария в серверный буфер бесполезна для посредника, до которого байты не дошли.

Допустим, в учебной инфраструктуре самый короткий подтверждённый таймаут бездействия составляет 60 секунд, а nginx настроен на 75 секунд между чтениями. Команда выбирает комментарий каждые 20 секунд и проверяет прохождение через все слои. Это оставляет запас относительно 60 секунд при нормальной доставке, но не гарантирует сохранение соединения при перегрузке, сетевой паузе или блокирующем обработчике.

Различайте таймаут бездействия и максимальную жизнь соединения. Регулярные данные могут помогать против первого, но не отменяют принудительное закрытие по абсолютному сроку. Поэтому нормальное переподключение требуется даже там, где heartbeat работает. Интервал должен учитывать также стоимость обслуживания большого количества клиентов.

Проверяйте отдельно отсутствие бизнес-событий и отсутствие любых байтов. Первый случай может быть штатной тишиной, второй — зависанием транспорта. Внутри приложения полезно различать «соединение открыто», «получены свежие данные» и «получен ожидаемый результат», чтобы интерфейс не показывал уверенное «всё актуально» бесконечно.

Дополните транспортное испытание остановкой учебного производителя событий при продолжающемся heartbeat. Если комментарии поступают, соединение выглядит живым, но ожидаемый тестовый статус не появляется. Этот опыт показывает границу проверки: поддержание канала не подтверждает работу очереди, обработчика заказа или источника уведомлений. Для каждого значимого результата нужен свой наблюдаемый признак завершения.

Разберите авторизацию, CORS и завершение сессии

Если EventSource соединение прерывается сразу или после истечения сессии, сначала посмотрите HTTP-ответ. Ошибка 401, запрет доступа, перенаправление на вход и неправильный MIME требуют разных действий. Повторное открытие того же запроса не восстановит истёкшие полномочия. Предусмотрите понятное состояние интерфейса и согласованный процесс обновления сессии.

У нативного конструктора EventSource есть URL и настройка withCredentials, но нет параметра для произвольного заголовка Authorization. Поэтому схему доступа выбирают заранее. Для подходящей архитектуры можно использовать сессионные cookies; если обязательны специальные заголовки, нужен другой согласованный клиентский механизм с собственной корректной обработкой потока и восстановления.

При запросе между разными origin с учётными данными требуется корректная CORS-политика: явный разрешённый origin и Access-Control-Allow-Credentials: true. Подстановка * вместо origin для такого ответа не подходит. Отдельно действуют ограничения cookies, включая SameSite и политику браузера, как поясняет руководство MDN по CORS.

Проверяйте действующую сессию, истёкшую сессию и пользователя без доступа к конкретному объекту. Не помещайте долговечный секрет в URL ради обхода ограничения API: адрес может попасть в журналы и другие системы. Обезличивайте диагностические материалы, сохраняя статус, маршрут и технические идентификаторы, необходимые для расследования.

Отдельный тест: открыть поток, завершить сессию тестового пользователя и выполнить повторное подключение. Запишите, кто обнаруживает потерю доступа, что видит посетитель и когда прекращается доставка защищённых данных. Затем снова войдите и убедитесь, что приложение создаёт предусмотренную подписку без накопления старых соединений. Это проверяет согласованность сессии и потока на всём переходе.

Согласуйте переподключение и Last-Event-ID

Перед ожиданием повтора проверьте readyState. Значение CONNECTING означает, что соединение ещё не установлено или браузер пытается его восстановить. При CLOSED этот объект больше не переподключается. В частности, ответ 204 No Content прекращает автоматические попытки; вызов close() тоже закрывает объект. Сопоставьте состояние с ответом сервера и действиями клиентского кода.

При предусмотренном протоколом переподключении EventSource передаёт непустой последний идентификатор в заголовке Last-Event-ID. Это поведение описано WHATWG. Оно сообщает серверу точку продолжения, но не создаёт на сервере архив и не восстанавливает пропущенные события автоматически.

Команда должна определить срок хранения событий, порядок выдачи после курсора и поведение при неизвестном ID. В учебном сценарии клиент получил evt-301, отключился, затем сервер создал evt-302 и evt-303. После восстановления проверяют фактически переданный курсор и получение пропущенных событий из серверного хранилища.

Обязательно проверьте продолжение после смены экземпляра приложения: хранение истории только в памяти одного процесса может разрушить ожидаемый replay. Доступ к истории проверяется для текущего пользователя; знание чужого идентификатора не должно открывать чужие уведомления.

Создание нового объекта EventSource или перезагрузка страницы — отдельный сценарий. Не рассчитывайте, что курсор старого объекта сохранится сам. Если проекту нужно такое продолжение, разработайте явный механизм начальной позиции. Для истёкшего окна истории предусмотрите получение актуального состояния и согласованной позиции потока, чтобы между снимком и дальнейшими событиями не возникал незаметный разрыв.

Проверьте дубли и применение события

Идентификатор события не означает гарантию exactly once. Получение данных транспортом и успешное изменение состояния приложения — разные действия. Обработчик может завершиться ошибкой после приёма, вкладка может закрыться, а повторная доставка может встретить уже применённое событие. Решение определяется смыслом данных, а не одним полем id.

Для статуса тестового заказа полезно передавать версию состояния и применять только подходящие изменения. Для журнала уведомлений можно хранить уже обработанные ID в согласованных границах. Если событие лишь сообщает о наличии обновления, клиент может заново получить актуальный объект. Выберите правило, которое допускает повтор и не запускает повторно денежную операцию или отправку письма.

Испытайте разрыв перед событием, во время передачи блока и сразу после вызова обработчика. Отдельно воспроизведите повтор одного ID, изменение порядка и курсор за пределами хранимой истории. Критерий успеха — правильное конечное состояние и объяснимое восстановление, а не только повторный переход соединения в открытое состояние.

Если последовательность обязательна, сохраняйте доказательства пропуска и запускайте предусмотренную синхронизацию. Тихое игнорирование неизвестного ID делает экран внешне работающим, хотя часть изменений уже потеряна.

Учитывайте вкладки, лимиты и перезапуск приложения

MDN предупреждает о небольшом числе одновременных соединений SSE без HTTP/2: типичный лимит составляет шесть на браузер и домен. В HTTP/2 число одновременных потоков согласуется между сторонами. Поэтому успешный тест одной вкладки не описывает поведение сотрудника с несколькими открытыми экранами.

Проверьте создание и закрытие подписок при переходах внутри сайта. Повторное монтирование компонента не должно незаметно оставлять старый EventSource работающим. При завершении соответствующего сценария клиент закрывает ненужный поток, а сервер освобождает связанную подписку и ресурсы. Наблюдайте фактическое число соединений на тестового пользователя.

Включите в испытания перезапуск приложения и одновременное восстановление группы клиентов. Сервер должен выдерживать не только обычное число открытых потоков, но и повторную авторизацию, поиск курсоров и выдачу истории. Ограничения частоты и поведение при перегрузке согласуйте так, чтобы клиенты не усиливали проблему бесконтрольными попытками.

Для мобильного браузера дополнительно проверьте уход приложения в фон и возвращение. Интерфейс должен уметь признать устаревшее состояние и восстановить актуальные данные. Длительность одного удачного соединения сама по себе не подтверждает эту способность.

Как принять исправление и продолжить наблюдение

Сформулируйте критерии до изменения настроек. Для учебного стенда команда может потребовать доставку тестового события в пределах двух секунд при оговорённой нагрузке и подтверждённое восстановление после короткого разрыва. Такие числа относятся только к этому испытанию; рабочие пределы зависят от задачи, аудитории и инфраструктуры.

  • Публичный маршрут возвращает ожидаемый статус, тип и корректные блоки SSE без HTML-подмены.
  • Времена создания, получения и применения позволяют проверить согласованную задержку.
  • Период без бизнес-событий не вызывает необъяснимую остановку; heartbeat проходит весь путь.
  • Истечение сессии и отказ доступа приводят к предусмотренному состоянию интерфейса.
  • Переподключение, replay и повтор события сохраняют правильный результат без повторного действия.
  • Несколько вкладок, переходы внутри сайта и перезапуск приложения не оставляют лишние подписки.
  • После изменения сохранены версия конфигурации, условия теста и обезличенные доказательства.

Назначьте владельца потокового маршрута и место хранения этих проверок. При следующем изменении прокси или авторизации команда сможет повторить короткий набор сценариев, не восстанавливая всю диагностику заново. Отдельно контролируйте свежесть результата в интерфейсе: обычная доступность страницы не показывает, приходят ли посетителю события вовремя.

Подключите SiteGuard для внешнего HTTP-контроля доступности важных страниц. Задержку SSE, восстановление потока и применение событий проверяйте отдельным сценарием команды.