InquirerDOT eyes new diving tourism spot in Misamis OrientalCNN TürkHava Durumu (29-10-2026)PunchAkpabio alleges payment for social media attacks on TinubuESPN DeportesAmérica extiende racha sin ganar en el Banorte한겨레“아빠, 보고싶어”…무면허 지게차 사고 유족, 진상 규명 요구ZDF heuteAktuelle Pressemitteilungen des ZDFThe South AfricanOrlando Pirates expected starting XI against Kaizer ChiefsGolem.deApple: Mac Studio mit 512 GB RAM kostet mehr als 25.000 Euro3DNews«Да, мы вайбкодим»: Epic Games подтвердила повсеместное использование ИИ в разработке Fortnite, Unreal Engine 6 и не толькоSportstarLockie Ferguson ruled out of New Zealand vs India T20I series; Replacement namedMyJoyOnlineThe Inconvenient Truth: One Africa in our speeches, 54 agendas in our decisionsHet Laatste NieuwsEerste dag van nationale rouw gestart, geluidsopnames onthullen dat verkeersleider in de fout gegaan is
The Daily Newsstand · Free, Always
Thursday, October 29, 2026

Байты приходят, события нет: проверяем кадры SSE и gzip

Translate

Сервер отправляет по SSE три коротких сообщения с паузой в четверть секунды, а обработчик на клиенте срабатывает только в конце: либо три раза подряд, либо один раз со всеми данными сразу. Логи сервера показывают, что каждая запись ушла вовремя. Где задержались данные?

Причин у такого симптома много: прокси, CDN, буфер приложения, медленный клиент. Я разобрал две из них на маленьком стенде, где все остальные слои убраны: незавершённый кадр SSE и gzip без промежуточного сброса. Обе причины описаны в документации. Задача стенда другая: показать, по каким наблюдениям их можно отличить друг от друга, не гадая.

Три точки наблюдения

Между записью на сервере и обработчиком события на клиенте данные проходят три этапа, и на каждом можно поставить отметку времени:

  1. Байты тела HTTP. Чтение из сокета вернуло хоть что-то.

  2. Декодированный текст. После распаковки Content-Encoding появились строки data:.

  3. Готовое событие. Парсер SSE увидел конец блока и отдал событие дальше.

Если задержка возникает между первой и второй точкой, данные застряли в кодеке. Если между второй и третьей, текст пришёл, но парсер ещё не считает событие законченным.

Стенд

Один скрипт на стандартной библиотеке Python 3.15.0 (zlib 1.3.1) в контейнере python:3.15-slim. HTTP/1.0-сервер и клиент работают в одном процессе на 127.0.0.1, без TLS, прокси и браузера. Сервер пишет три строки data: 0, data: 1, data: 2 с паузой 250 мс, после каждой записи вызывает wfile.flush(). Клиент читает тело через HTTPResponse.read1(4096), при необходимости распаковывает и разбирает события. Каждый режим повторён три раза; ниже медианы, полный вывод с размерами каждого чтения лежит в журнале проверки.

Режимы отличаются одним параметром:

Режим

Что меняется

plain

после каждой строки data: пустая строка, без сжатия

missing_separator

между строками data: только один перевод строки; пустая строка дописывается один раз, после третьей

gzip_finish

кадры правильные, ответ сжат gzip, компрессор сбрасывается только в конце (Z_FINISH)

gzip_sync

то же сжатие, но после каждого кадра flush(zlib.Z_SYNC_FLUSH)

Ядро сервера:

for i in range(COUNT):
    frame = f'data: {i}\n'.encode()
    if mode != 'missing_separator':
        frame += b'\n'
    payload = encode_frame(codec, frame, mode == 'gzip_sync') if codec else frame
    if payload:
        self.wfile.write(payload)
        self.wfile.flush()
    time.sleep(PERIOD)

Клиентский парсер минимальный: он режет текст по \n\n и собирает строки data: . Он покрывает ровно то подмножество формата, которое использует стенд, и полноценным EventSource не является.

Результат

Режим

Первые байты тела

Первый текст после декодера

Первое событие

Событий

plain

1 мс

1 мс

1 мс

3

missing_separator

1 мс

1 мс

751 мс

1

gzip_finish

1 мс

751 мс

752 мс

3

gzip_sync

1 мс

1 мс

1 мс

3

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

Различает то, где появляется задержка в 750 мс (три паузы по 250 мс).

missing_separator. Текст после декодера виден сразу, все три строки data: дошли вовремя. События нет до тех пор, пока сервер не допишет пустую строку. Потом приходит одно событие с данными 0\n1\n2. По стандарту WHATWG событие отправляется на пустой строке, а несколько строк data: до неё склеиваются через перевод строки. Если поток закончится без пустой строки, незаконченный блок будет отброшен, поэтому в стенде сервер дописывает её явно.

gzip_finish. Байты тоже приходят сразу, но это 10 байт заголовка gzip. Вторая и третья запись на сервере вообще ничего не отдали в сокет: компрессор принял по 9 байт и вернул пустую строку. Все данные ушли одним куском в 27 байт при завершающем flush(), и декодер выдал текст только тогда. Три события появились одновременно, на 752-й миллисекунде.

Обработчик HTTP здесь ни при чём: в стенде wfile пишет в сокет сразу, без своего буфера, и wfile.flush() после каждой записи ничего не меняет. Данные лежат внутри компрессора: compress() вернул пустую строку, и отправлять в сокет было нечего. В документации zlib для Python это разделено явно: Z_SYNC_FLUSH выдаёт всё накопленное и оставляет поток открытым для продолжения, а flush() без аргумента использует Z_FINISH и закрывает поток.

gzip_sync. С Z_SYNC_FLUSH после каждого кадра сервер отправил 25, 12 и 11 байт, а в конце ещё 10 байт завершения потока. Текст и события появляются сразу, как без сжатия.

Порядок диагностики

Для симптома «события приходят пачкой» по стенду получается такая последовательность:

  1. Отметить время чтений тела ответа. Учтите, что первые байты могут оказаться только заголовком gzip без единой строки data:.

  2. Отметить время текста после декодера. Если байты есть, а текста нет, проверьте, выдаёт ли компрессор данные до завершения ответа. Самая быстрая проверка: тот же поток без сжатия и тот же поток с синхронным сбросом.

  3. Если строки data: видны, а события нет, проверьте пустую строку после каждого кадра. Признак именно этой причины: вместо нескольких событий приходит одно с многострочными данными.

  4. Если текст и события появляются вовремя, причина за пределами этих двух веток. Стенд её не показывает, и угадывать её по этим данным не стоит.

Ограничения

  • Парсер стенда понимает только UTF-8, LF и поле data:. CRLF, id, event, retry, переподключение и поведение браузерного EventSource не проверялись.

  • Нет TLS, HTTP/2, nginx, CDN, медленного клиента и большого потока. Буферизация на этих слоях существует, но здесь не воспроизводится. Список причин из статьи не полный.

  • 750 мс заданы паузами сервера и ничего не говорят о скорости SSE или gzip. Миллисекунды, размеры и число чтений относятся к этому прогону и этим версиям.

  • Сравнивается конкретный кодек с конкретным режимом сброса. Из опыта не следует, что сжатие несовместимо с SSE или что его обязательно отключать: в режиме gzip_sync оба требования выполнены одновременно.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.