From 58993e0e5ee8e31480d176f00e034e3d5adc71cd Mon Sep 17 00:00:00 2001 From: latand Date: Tue, 10 Feb 2026 22:28:59 +0200 Subject: [PATCH] Handle empty external reply story in updates (#1587) --- CHANGES/1587.bugfix.rst | 1 + aiogram/types/external_reply_info.py | 14 ++++++ .../test_session/test_base_session.py | 48 ++++++++++++++++++- tests/test_api/test_types/test_message.py | 22 +++++++++ 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 CHANGES/1587.bugfix.rst diff --git a/CHANGES/1587.bugfix.rst b/CHANGES/1587.bugfix.rst new file mode 100644 index 00000000..eb3ec19c --- /dev/null +++ b/CHANGES/1587.bugfix.rst @@ -0,0 +1 @@ +Fixed deserialization for malformed Bot API updates where :code:`external_reply.story` is an empty object, treating it as missing data instead of crashing polling. diff --git a/aiogram/types/external_reply_info.py b/aiogram/types/external_reply_info.py index 1b8797b0..db0f6c73 100644 --- a/aiogram/types/external_reply_info.py +++ b/aiogram/types/external_reply_info.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any +from pydantic import model_validator + from .base import TelegramObject if TYPE_CHECKING: @@ -37,6 +39,18 @@ class ExternalReplyInfo(TelegramObject): Source: https://core.telegram.org/bots/api#externalreplyinfo """ + @model_validator(mode="before") + @classmethod + def handle_empty_story(cls, values: Any) -> Any: + if not isinstance(values, dict): + return values + + if values.get("story") == {}: + values = values.copy() + values["story"] = None + + return values + origin: MessageOriginUnion """Origin of the message replied to by the given message""" chat: Chat | None = None diff --git a/tests/test_api/test_client/test_session/test_base_session.py b/tests/test_api/test_client/test_session/test_base_session.py index 46d6bbc7..6f1b3546 100644 --- a/tests/test_api/test_client/test_session/test_base_session.py +++ b/tests/test_api/test_client/test_session/test_base_session.py @@ -26,7 +26,7 @@ from aiogram.exceptions import ( TelegramServerError, TelegramUnauthorizedError, ) -from aiogram.methods import DeleteMessage, GetMe, TelegramMethod +from aiogram.methods import DeleteMessage, GetMe, GetUpdates, TelegramMethod from aiogram.types import UNSET_PARSE_MODE, LinkPreviewOptions, User from aiogram.types.base import UNSET_DISABLE_WEB_PAGE_PREVIEW, UNSET_PROTECT_CONTENT from tests.mocked_bot import MockedBot @@ -227,6 +227,52 @@ class TestBaseSession: content='{"ok": "test"}', ) + def test_check_response_get_updates_with_empty_external_reply_story(self): + session = CustomSession() + bot = MockedBot() + method = GetUpdates() + + response = session.check_response( + bot=bot, + method=method, + status_code=200, + content=json.dumps( + { + "ok": True, + "result": [ + { + "update_id": 1, + "message": { + "message_id": 42, + "date": int(datetime.datetime.now().timestamp()), + "chat": {"id": 42, "type": "private"}, + "from": {"id": 42, "is_bot": False, "first_name": "Test"}, + "text": "test", + "external_reply": { + "origin": { + "type": "user", + "sender_user": { + "id": 43, + "is_bot": False, + "first_name": "Sender", + }, + "date": int(datetime.datetime.now().timestamp()), + }, + "story": {}, + }, + }, + } + ], + } + ), + ) + + assert len(response.result) == 1 + update = response.result[0] + assert update.message is not None + assert update.message.external_reply is not None + assert update.message.external_reply.story is None + async def test_make_request(self): session = CustomSession() diff --git a/tests/test_api/test_types/test_message.py b/tests/test_api/test_types/test_message.py index 3bb8d029..0287b364 100644 --- a/tests/test_api/test_types/test_message.py +++ b/tests/test_api/test_types/test_message.py @@ -1016,6 +1016,28 @@ class TestAllMessageTypesTested: class TestMessage: + def test_model_validate_external_reply_with_empty_story(self): + message = Message.model_validate( + { + "message_id": 42, + "date": int(datetime.datetime.now().timestamp()), + "chat": {"id": 42, "type": "private"}, + "from": {"id": 42, "is_bot": False, "first_name": "Test"}, + "text": "test", + "external_reply": { + "origin": { + "type": "user", + "sender_user": {"id": 43, "is_bot": False, "first_name": "Sender"}, + "date": int(datetime.datetime.now().timestamp()), + }, + "story": {}, + }, + } + ) + + assert message.external_reply is not None + assert message.external_reply.story is None + @pytest.mark.parametrize( "message,content_type", MESSAGES_AND_CONTENT_TYPES,