fix(webhook): avoid MissingGreenlet on expired ORM instance after commit
Release / release (push) Successful in 58s

Telegram webhook handler crashed with sqlalchemy.exc.MissingGreenlet
when processing any incoming message after committing the chat row:

    TelegramChat.bot_id == bot.id
                           ^^^^^^
    MissingGreenlet: greenlet_spawn has not been called

AsyncSession expires all instances on commit. Accessing bot.id/bot.token
after that triggers implicit lazy-load I/O from a sync attribute getter,
which can't enter the greenlet dispatcher → crash.

Fix: snapshot bot.id + bot.token to locals before commit, refresh the
ORM instance after a successful commit so handle_command() can still
use it, and route the remaining call sites through the snapshot
variables.
This commit is contained in:
2026-04-21 20:54:00 +03:00
parent 293614d667
commit 2eccbc7279
@@ -71,9 +71,15 @@ async def telegram_webhook(
# Auto-persist chat from incoming message
from_user = message.get("from", {})
msg_language = from_user.get("language_code", "")
# Snapshot bot identity before commit — AsyncSession expires instances
# on commit, and implicit lazy-load of `bot.id` / `bot.token` later would
# raise sqlalchemy.exc.MissingGreenlet.
bot_id = bot.id
bot_token = bot.token
try:
await save_chat_from_webhook(session, bot.id, chat_info, language_code=msg_language)
await save_chat_from_webhook(session, bot_id, chat_info, language_code=msg_language)
await session.commit()
await session.refresh(bot)
except Exception:
_LOGGER.warning("Failed to auto-save chat %s", chat_id, exc_info=True)
@@ -81,7 +87,7 @@ async def telegram_webhook(
if text.startswith("/"):
chat_row = (await session.exec(
select(TelegramChat).where(
TelegramChat.bot_id == bot.id,
TelegramChat.bot_id == bot_id,
TelegramChat.chat_id == chat_id,
)
)).first()
@@ -93,9 +99,9 @@ async def telegram_webhook(
if responses:
for resp in responses:
if resp.text:
await send_reply(bot.token, chat_id, resp.text, reply_to_message_id=message_id)
await send_reply(bot_token, chat_id, resp.text, reply_to_message_id=message_id)
if resp.media:
await send_media_group(bot.token, chat_id, resp.media, reply_to_message_id=message_id)
await send_media_group(bot_token, chat_id, resp.media, reply_to_message_id=message_id)
return {"ok": True}
return {"ok": True, "skipped": "not_a_command"}