Handle empty external reply story in updates (#1587)

This commit is contained in:
latand 2026-02-10 22:28:59 +02:00
parent 1708980ceb
commit 58993e0e5e
4 changed files with 84 additions and 1 deletions

1
CHANGES/1587.bugfix.rst Normal file
View file

@ -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.

View file

@ -2,6 +2,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from pydantic import model_validator
from .base import TelegramObject from .base import TelegramObject
if TYPE_CHECKING: if TYPE_CHECKING:
@ -37,6 +39,18 @@ class ExternalReplyInfo(TelegramObject):
Source: https://core.telegram.org/bots/api#externalreplyinfo 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: MessageOriginUnion
"""Origin of the message replied to by the given message""" """Origin of the message replied to by the given message"""
chat: Chat | None = None chat: Chat | None = None

View file

@ -26,7 +26,7 @@ from aiogram.exceptions import (
TelegramServerError, TelegramServerError,
TelegramUnauthorizedError, 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 import UNSET_PARSE_MODE, LinkPreviewOptions, User
from aiogram.types.base import UNSET_DISABLE_WEB_PAGE_PREVIEW, UNSET_PROTECT_CONTENT from aiogram.types.base import UNSET_DISABLE_WEB_PAGE_PREVIEW, UNSET_PROTECT_CONTENT
from tests.mocked_bot import MockedBot from tests.mocked_bot import MockedBot
@ -227,6 +227,52 @@ class TestBaseSession:
content='{"ok": "test"}', 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): async def test_make_request(self):
session = CustomSession() session = CustomSession()

View file

@ -1016,6 +1016,28 @@ class TestAllMessageTypesTested:
class TestMessage: 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( @pytest.mark.parametrize(
"message,content_type", "message,content_type",
MESSAGES_AND_CONTENT_TYPES, MESSAGES_AND_CONTENT_TYPES,