Техническое руководство
Отмена SSE streaming AI API: как закрыть поток без утечек и дублей
Отмена SSE streaming AI API нужна не для того, чтобы «убить» любой медленный ответ, а чтобы приложение честно остановило больше не нужный поток, освободило ресурсы и не выдало неполный текст за завершённый результат. Для RussiaAPI безопасный путь начинается на сервере: браузер отменяет собственный запрос к вашему backend, а backend отменяет разрешённую исходящую операцию и фиксирует её состояние без раскрытия секретов.
RUSSIAAPI_API_KEY; не передавайте внешние ключи, cookie, пароли, коды подтверждения или лишние персональные данные.Разделите отмену интерфейса и отмену операции
Кнопка «Стоп» в интерфейсе означает, что конкретному пользователю больше не нужен поток. Это ещё не доказательство, что удалённая операция не была принята, не завершится позже или не оставила учётное событие. Поэтому frontend не должен обращаться к AI endpoint с ключом напрямую. Он сообщает вашему серверу внутренний operation ID, а сервер сопоставляет его с владельцем, маршрутом и текущим controller.
Для текстового streaming сценария полезны как минимум четыре локальных состояния: queued, streaming, cancelled и completed. Если соединение оборвалось, добавьте unknown, а не подменяйте его completed. UI может показать «генерация остановлена» только после того, как backend записал отмену своей доставки. Это не обещание отмены у любого внешнего поставщика и не основание повторять запрос без решения пользователя.
Задайте границу данных в потоке
Поток может содержать частичный текст, диагностические поля и внутренние маркеры. Перед пересылкой SSE на клиент определите allowlist событий: например, только текстовые delta и явный done. Не проксируйте заголовки, идентификаторы чужой инфраструктуры, служебные сообщения или необработанные ошибки. При отмене не сохраняйте частичный результат по умолчанию: для некоторых продуктов он полезен как черновик, но это должно быть отдельным согласованным правилом хранения.
Ограничьте время жизни соединения и размер буфера. Если клиент медленный, серверу нельзя бесконечно копить chunks в памяти. Backpressure, небольшой буфер и закрытие неактивного соединения лучше, чем скрытая деградация всей очереди. Общие основы server-side streaming и дедлайнов разобраны в руководстве по SSE; проверяйте доступные параметры на текущем маршруте, а не по старому примеру.
Не превращайте AbortSignal в бесконтрольный retry
AbortSignal завершает ожидание в вашем процессе, но не делает последующий повтор безопасным автоматически. Если до отмены сервер успел получить часть ответа, новый запрос может дать другой текст, потратить новый бюджет и усложнить пользовательский опыт. Для read-only генерации предложите пользователю явную кнопку «Создать заново». Для операций с побочным эффектом сначала потребуются идемпотентность, проверка статуса и отдельный продуктовый контракт.
Локально классифицируйте AbortError отдельно от timeout и сетевой ошибки. Отмена по инициативе пользователя не должна повышать аварийный алерт; timeout может быть полезным сигналом производительности; 429 требует уменьшить параллельность. Такой разбор облегчает наблюдаемость и не маскирует ошибки приложения. Для безопасной диагностики сохраняйте request ID без тела запроса по практике request ID.
Пример server-side маршрута
Ниже Node.js 18+ пример демонстрирует серверный прокси для одного текстового потока. Он принимает только собственный ключ из окружения, связывает отмену клиента с исходящим controller и не передаёт секрет в браузер. Название модели берётся из server-side конфигурации: до запуска подтвердите его в своём текущем каталоге. Пример показывает форму обработки, а не гарантирует конкретные возможности любой модели.
export async function streamToClient({ prompt, clientSignal, write }) {
if (!process.env.RUSSIAAPI_API_KEY || typeof prompt !== 'string' || prompt.length > 4_000) {
throw new Error('invalid_request');
}
const controller = new AbortController();
const cancel = () => controller.abort(new Error('client_cancelled'));
clientSignal.addEventListener('abort', cancel, { once: true });
try {
const upstream = await fetch('https://russiaapi.com/v1/chat/completions', {
method: 'POST',
headers: { authorization: `Bearer ${process.env.RUSSIAAPI_API_KEY}`, 'content-type': 'application/json' },
body: JSON.stringify({ model: process.env.RUSSIAAPI_TEXT_MODEL, stream: true, messages: [{ role: 'user', content: prompt }] }),
signal: controller.signal
});
if (!upstream.ok || !upstream.body) throw new Error(`upstream_${upstream.status}`);
for await (const chunk of upstream.body) write(chunk);
return { state: 'completed' };
} catch (error) {
return { state: controller.signal.aborted ? 'cancelled' : 'unknown' };
} finally {
clientSignal.removeEventListener('abort', cancel);
}
}Проверьте синтаксис через node --check. В production добавьте аутентификацию пользователя, rate limit на свой маршрут, ограничение длины prompt и наблюдение за незакрытыми controller. Не логируйте body или заголовки только ради отладки.
Проверьте завершение ресурсов
После отмены освобождайте reader, закрывайте SSE-ответ и удаляйте controller из внутренней карты. Делайте это в finally, потому что обрыв может произойти до первого chunk, в середине JSON-строки или после нормального done. Метрика «активные потоки» должна уменьшаться в каждом пути. Иначе тесты будут выглядеть успешными, а длительная нагрузка постепенно исчерпает память или соединения.
Полезный негативный набор включает отмену до первого chunk, отмену после нескольких delta, закрытие вкладки, timeout исходящего запроса и ошибку сериализации. В каждом случае проверяйте, что пользователь не видит ложное «готово», ключ не попал в ответ, а повтор не был отправлен автоматически. Ограниченные тайм-ауты и действия после них описаны в материале о timeout.
Минимальный операционный контур
Для любого сценария заранее определите владельца операции, внутренний идентификатор, допустимый срок ожидания и событие, после которого результат считается подтверждённым. В журнале достаточно хранить время, статус, безопасный request ID, тип операции и версию вашего адаптера. Полный prompt, ответ пользователя, заголовок Authorization и временные ссылки не нужны для базовой диагностики и часто создают лишний риск.
Проверяйте изменения на обезличенном тестовом наборе и с отдельным собственным ключом, ограниченным бюджетом и правами. Нельзя по единичному удачному вызову делать вывод, что все модели, параметры, цены или возможности доступны постоянно. Перед расширением трафика сверяйте текущий каталог, права проекта, условия обработки данных и фактические сигналы своего приложения.
Ошибки 400, 401, 403, 429, timeout и 5xx требуют разных действий. Не скрывайте их бесконечным retry, не меняйте модель молча и не используйте интеграцию для обхода законов, санкций, региональных или платформенных ограничений. Если состояние операции после timeout неизвестно, сначала проверьте своё внутреннее хранилище, а затем выполняйте только явно разрешённый и ограниченный шаг.
Проверьте сценарий в RussiaAPI
Создайте собственный тестовый ключ в консоли, сверьте текущий каталог моделей и выполните обезличенный server-side smoke test. Расширяйте нагрузку и доступ только после измеримой проверки.
FAQ
Отменяет ли закрытие SSE вкладки саму генерацию?
Не обязательно. Закрытие вкладки прекращает связь с вашим приложением, но внешний запрос мог быть принят или завершиться позже. Сервер должен отдельно записать состояние своей доставки и не выдавать неполный текст за подтверждённый результат.
Нужно ли автоматически повторять поток после отмены?
Нет. Отмена пользователя, timeout и сетевая ошибка означают разные вещи. Автоматический повтор может добавить расход и другой результат. Для текстовой операции лучше дать пользователю явный выбор, а для побочных действий сначала проверить состояние.
Можно ли хранить частичный ответ?
Только если это явно соответствует продуктовой политике, доступам и сроку хранения. Сохраняйте минимально необходимое, помечайте черновик как незавершённый и не включайте в логи ключи, заголовки или чужие служебные данные.