В этом материале
Сначала bytes декодируются с сохранением состояния, затем выделяются SSE-события, и только после этого разбирается JSON payload.
Потоковый ответ может быть корректным на сервере и ломаться у клиента из-за неправильной границы обработки. Один сетевой read содержит половину символа, несколько SSE-событий или часть строки data. Если каждый полученный chunk сразу превращать в отдельный JSON, ошибки будут зависеть от случайной нарезки сети. Надёжная проверка строит несколько последовательных уровней и сохраняет состояние между вызовами чтения.
Отделите bytes от текста
Стандарт SSE задаёт UTF-8 для event stream. Это не означает, что любой сетевой фрагмент содержит целое число символов. Многобайтовая последовательность может закончиться в следующем read, и декодер должен сохранить её незавершённый остаток до получения продолжения.
TextDecoder.decode описывает streaming-режим, в котором дополнительные данные ожидаются в следующих вызовах. Завершение декодирования также является отдельным шагом. Если каждый раз создавать новый decoder, его состояние теряется, даже когда исходные bytes полностью корректны и в правильном порядке дошли до клиента.
Добавьте framing после декодера
Текст поступает в SSE parser, который собирает строки и события по правилам формата. Только готовый data payload передаётся JSON parser. Между этими слоями не следует делать допущение «один read — одно событие» или «одна строка — один полный объект».
Поток может содержать comments и несколько data-строк одного события. Их обработка определяется форматом, а не удобством регулярного выражения. При создании своего parser сохраните явное состояние незавершённой строки и текущего события, ограничив объём накопления для некорректного входа.
Составьте fixture с Unicode
Возьмите короткое синтетическое событие с латинским текстом, кириллическим символом и emoji. Сохраните исходные bytes и ожидаемый итоговый текст. Затем разрежьте поток в нескольких местах, включая границу внутри многобайтового символа, внутри data-префикса и между двумя соседними событиями.
Каждая нарезка должна давать одинаковую последовательность прикладных событий. Это проверяет независимость результата от транспорта. Для начала достаточно ограниченного набора cuts; полный перебор всех вариантов не нужен, если он резко увеличивает время проверки и не помогает найти конкретную ошибку реализации.
Не теряйте несколько событий
MDN о Streams API объясняет, что chunks имеют разные размеры и находятся в очередях чтения. Поэтому один крупный read может содержать сразу несколько готовых событий. Обработчик должен извлечь каждое из них, а остаток сохранить для следующего чтения.
Проверьте счётчик событий, порядок и итоговую конкатенацию content. Успешный последний JSON не подтверждает, что предыдущие события не были потеряны. Отдельно добавьте пустой read, обычное закрытие и обрыв с незавершённой строкой, сохранив различие между этими исходами.
Ограничьте вывод транспортным слоем
Прошедший fixture подтверждает decoding и framing на выбранном наборе. Он не проверяет правильность tool arguments, смысл finish_reason или качество генерации. Эти уровни должны получать уже корректно выделенные события и иметь собственные ожидания, чтобы одна ошибка не маскировалась другой.
Практика инкрементального чтения описана в клиенте OpenRouter, а первый серверный пример — в streaming vLLM. Границы bytes, текста, SSE и JSON остаются разными независимо от выбранного движка или маршрутизатора.