Error handling
HTTP errors от bot-API
401 UnauthorizedБитый / отсутствующий / неизвестный токен. Логируйте и алертите
владельца (не retry). SDK перехватывает и raise'ит BotAPIError(401).
Suspended / revoked / deleted бот возвращает 403 Forbidden
(detail="bot is suspended" и т.п.), а не 401. 401 — только про сам токен.
403 ForbiddenВозможные причины:
- Missing scope (
"missing scope: messages.send") - Channel не в
allowed_channel_ids - Bot suspended владельцем
Не retry — требуется вмешательство владельца.
404 Not FoundУказан несуществующий resource (например, удалённое сообщение). Логируйте и proceed.
422 Unprocessable EntityValidation. Баг в коде бота — fix и redeploy.
429 Too Many RequestsRate limit. Уважение Retry-After. SDK делает auto-respect.
5xx Server ErrorСерверная проблема. SDK не ретраит 5xx — сразу поднимает
BotAPIError; повтор/обработка на стороне вашего кода. Ретраится
только 429 (с уважением Retry-After, иначе фикс. пауза 1.0s,
до 3 попыток). При устойчивом повторении — алерт через
status.reasonspace.ru.
Webhook errors
Всегда возвращайте 2xx, даже если внутренний handler упал. Иначе получится retry-storm и затем suspension.
@bot.on("message.created")
async def handler(event):
try:
await do_stuff(event)
except Exception as e:
logger.exception("handler failed", exc_info=e)
# НЕ raise — SDK вернёт 200 ОК наружу
SDK ловит exceptions внутри handler'а автоматически (try/except в
цикле обработчиков Bot._dispatch) и всё равно отвечает 200.
Дополнительная обработка не нужна.
Когда возвращать 4xx/5xx из webhook
Только если требуется retry:
- 5xx — бэк ретраит с exponential backoff (1s, 5s, 30s, 5min, 30min)
Возврат 410 Gone из вашего webhook не отключает доставку навсегда.
Он засчитывается как обычная неудачная доставка (лог bot_webhook_gone,
failure_count++); 5 неудач подряд → авто-suspend webhook'а на 24 часа.
Постоянного отключения по 410 нет.
Idempotency
Один event может прилететь несколько раз. SDK имеет встроенный
delivery-cache по X-Reasonspace-Delivery (LRU 1024 по умолчанию,
настраивается через Bot(max_dedup=...)), но при наличии side-effects
(запись в ваше хранилище, external API) — добавьте собственный
uniqueness check:
@bot.on("message.created")
async def handler(event):
if await store.exists(f"processed:{event.id}"):
return
try:
await process(event)
await store.set(f"processed:{event.id}", True, ex=86400)
except Exception:
# Не set'им processed — будет retry
raise
Network failures (исходящие вызовы)
Сетевые ошибки SDK не оборачивает в BotAPIError — httpx
пробрасывает httpx.RequestError / httpx.TimeoutException как есть.
Конвенции status_code == 0 в SDK нет; ловите сетевые ошибки отдельно
от BotAPIError:
import httpx
try:
await bot.send_message(channel_id, "hi", space_id=space_id)
except httpx.TimeoutException:
# Network/timeout — retry с backoff
await asyncio.sleep(1)
await bot.send_message(channel_id, "hi", space_id=space_id)
except httpx.RequestError as e:
# Прочие сетевые ошибки (DNS, connect) — retry/алерт по контексту
logger.warning("network error: %s", e)
except BotAPIError as e:
if e.status_code == 401:
# Token issue — алерт, не retry
raise
else:
# Лог и решение по контексту
logger.warning("send failed: %s", e)
Graceful shutdown
import signal
def _signal_handler(signum, frame):
logger.info("shutting down")
# Очистка voice sessions, файлов и т. п.
asyncio.create_task(cleanup())
signal.signal(signal.SIGTERM, _signal_handler)
signal.signal(signal.SIGINT, _signal_handler)
Особенно важно для voice-ботов: при join бот получает токен и
координаты голосовой комнаты —
POST /api/bot/spaces/{space_id}/voice/{channel_id}/join отдаёт ровно
token / url / room / ttl_seconds. По соглашению бот только
публикует звук (publish-only). Выход из комнаты делается отключением
голосового клиента на стороне бота; REST-метода «notify left» в
платформе нет.
Дальше
Логирование, метрики, alerting.