WebSocket в FastAPI: что проверять на ревью
WebSocket-эндпоинты в FastAPI проходят ревью хуже HTTP-роутов: нет привычного цикла запрос-ответ, нет автоматических middleware для аутентификации, а утечка соединений вылезает только под нагрузкой. Разбираем конкретные точки, на которые стоит смотреть при ревью, — от жизненного цикла соединения до backpressure.

Почему WebSocket-код проскальзывает через ревью
HTTP-роут в FastAPI — вещь компактная: функция получает запрос, возвращает ответ, зависимости прокидываются через Depends, валидация срабатывает на Pydantic-модели. Ревьюеру достаточно пробежать глазами пять-десять строк. WebSocket-эндпоинт устроен иначе: соединение живёт неопределённо долго, данные текут в обе стороны, а любая ошибка в цикле чтения-записи убивает не один запрос, а весь канал. При этом FastAPI (Starlette под капотом) даёт минимальный каркас — остальное на авторе. Вот конкретные точки, где чаще всего прячутся проблемы.
Жизненный цикл соединения
Accept и ранний выход
Первая строка эндпоинта — await websocket.accept(). Если до вызова accept вы бросаете исключение или делаете return, Starlette закрывает соединение кодом 1006 (abnormal closure), а клиент не получает ни кода ошибки, ни причины. На ревью проверяйте:
- Вся логика авторизации, проверки лимитов и валидации query-параметров выполняется до
accept— но при отказе соединение закрывается явно черезawait websocket.close(code=4001, reason="unauthorized"), а не через голыйreturn. - Если
acceptвызван, за ним обязательно следует блокtry/finally, гарантирующийcloseи освобождение ресурсов.
Цикл чтения: while True без тормозов
Типичный паттерн:
# FastAPI 0.110+ / Starlette 0.37+
@app.websocket("/ws")
async def ws_endpoint(websocket: WebSocket):
await websocket.accept()
try:
while True:
data = await websocket.receive_text()
# обработка
await websocket.send_text(f"echo: {data}")
except WebSocketDisconnect:
pass
finally:
# очистка: убрать из пула, закрыть подписки
pass
На что смотреть при ревью:
- Ловится ли
WebSocketDisconnect? Без этого при обрыве связи цикл бросит исключение, аfinallyможет не отработать предсказуемо, если исключение перехватится выше. - Есть ли тайм-аут на чтение?
receive_text()ждёт бесконечно. Клиент, который открыл соединение и молчит, держит корутину и слот в event loop навсегда. Решение —asyncio.wait_forс разумным тайм-аутом или ping/pong на уровне приложения. - Ограничен ли размер сообщения? Starlette и Uvicorn по умолчанию не ограничивают размер WebSocket-фрейма на уровне приложения. Один клиент может отправить сообщение в десятки мегабайт, и оно окажется в памяти целиком. Ограничение задаётся через параметры сервера (в Uvicorn —
--ws-max-size, по умолчанию 16 МБ) либо проверяется в коде после получения.
Аутентификация: Depends здесь работает не так
В WebSocket-эндпоинтах FastAPI Depends формально работает, но HTTPException, брошенный внутри зависимости, не превращается в HTTP-ответ — соединение просто обрывается с кодом 1006. На ревью это выглядит так: автор скопировал зависимость из HTTP-роута, всё «работает» в happy path, а при невалидном токене клиент видит молчаливый обрыв без кода и причины.
Если зависимость для WebSocket бросаетHTTPException, Starlette закроет соединение без внятного кода ошибки. Авторизация для WebSocket должна закрывать соединение явно, черезwebsocket.close(code=...), а не через исключения из HTTP-мира.
Что проверять:
- Зависимость принимает параметр типа
WebSocket, а неRequest. - При отказе вызывается
await websocket.close(code=4003)— коды 4000–4999 зарезервированы для приложений согласно RFC 6455. - Токен передаётся через query-параметр или первое сообщение, а не через заголовок
Authorization— браузерный WebSocket API не позволяет задавать произвольные заголовки при рукопожатии.
Backpressure и отправка в группу
Частая ситуация: сервер рассылает обновления нескольким подключённым клиентам. Код выглядит безобидно — цикл по списку соединений, await connection.send_text(payload). Проблема: если один клиент читает медленно, send_text блокирует корутину, и остальные клиенты ждут.
| Подход к рассылке | Плюс | Риск |
|---|---|---|
Последовательный await send | Простой код, легко читается | Медленный клиент блокирует всех |
asyncio.gather по списку | Параллельная отправка | Одно исключение без return_exceptions=True убивает всю группу |
| Очередь на клиента + отдельная задача записи | Изоляция, контроль переполнения | Сложнее, нужно следить за временем жизни задач |
На ревью спрашивайте: что произойдёт, если один из ста клиентов перестанет читать? Если ответ — «все встанут» — это баг, даже если сейчас клиентов три.
Управление задачами и утечки
WebSocket-эндпоинт нередко создаёт фоновые задачи: подписка на Redis Pub/Sub, чтение из Kafka, периодический heartbeat. Классическая утечка — задача создана через asyncio.create_task, но не отменена при закрытии соединения.
Минимальный чек-лист для ревью:
- Каждая задача, созданная внутри эндпоинта, сохранена в переменную или множество.
- В блоке
finallyвсе задачи отменяются черезtask.cancel()и ожидаются черезawait asyncio.gather(*tasks, return_exceptions=True). - Внутри задачи есть обработка
asyncio.CancelledError— иначе отмена может пройти незаметно, а ресурс (подключение к брокеру, курсор БД) останется открытым. - Если используется
asyncio.TaskGroup(Python 3.11+), убедитесь, что при выходе из контекстного менеджера соединение тоже закрывается корректно.
Сноска: Backpressure — механизм, при котором медленный потребитель данных сигнализирует быстрому производителю о необходимости замедлиться. В контексте WebSocket это означает, что сервер не должен бесконтрольно набивать буфер отправки клиенту, который не успевает читать.
Тестируемость
FastAPI предоставляет TestClient с поддержкой WebSocket через контекстный менеджер with client.websocket_connect("/ws") as ws. На ревью проверяйте:
- Есть ли тест на happy path (подключение, обмен, корректное закрытие).
- Есть ли тест на отказ аутентификации — клиент должен получить конкретный код закрытия, а не молчаливый обрыв.
- Есть ли тест на отправку невалидных данных — эндпоинт не должен падать, а должен закрывать соединение с информативным кодом или отправлять сообщение об ошибке.
- Тайм-ауты в тестах: тест, который ждёт сообщение бесконечно, повиснет в CI и не скажет, что именно сломалось.
Чек-лист для ревью
Ниже — сводка, которую удобно держать перед глазами при проверке пулл-реквеста с WebSocket-эндпоинтом:
acceptвызывается явно; отказ — черезclose(code=...), а не через исключение.- Цикл чтения обёрнут в
try/except WebSocketDisconnect/finally. - Есть тайм-аут на
receiveили механизм ping/pong. - Размер входящего сообщения ограничен — на уровне сервера или приложения.
- Зависимости не бросают
HTTPException. - Токен передаётся не через заголовок (для браузерных клиентов).
- Рассылка не блокируется медленным клиентом.
- Фоновые задачи отменяются в
finally. - Тесты покрывают отказ, невалидные данные и корректное закрытие.
Что дальше
После того как эндпоинт прошёл ревью, имеет смысл измерить поведение под нагрузкой — в частности, сколько одновременных соединений держит один воркер Uvicorn и при каком количестве начинает расти задержка отправки. Инструменты вроде websocat или artillery позволяют открыть тысячи соединений и отправлять сообщения по расписанию. Отдельная тема — масштабирование на несколько процессов: Uvicorn с --workers > 1 не разделяет состояние между воркерами, и broadcast через список в памяти перестаёт работать. Для этого нужен внешний брокер (Redis Pub/Sub, каналы в PostgreSQL), но это уже за рамками ревью одного эндпоинта.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Starlette WebSocket documentation, FastAPI — WebSockets
Частые вопросы
Работает ли Depends в WebSocket-эндпоинтах FastAPI?
Почему клиент видит код 1006 вместо моего кода ошибки?
Как защититься от клиента, который открывает соединение и молчит?
Что произойдёт, если фоновая задача не отменена после отключения клиента?
Как тестировать WebSocket-эндпоинт, если он зависит от Redis Pub/Sub?
Можно ли масштабировать WebSocket-эндпоинт на несколько воркеров Uvicorn?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.