diff --git a/CHANGES/1014.misc.rst b/CHANGES/1014.misc.rst new file mode 100644 index 00000000..e29f8c21 --- /dev/null +++ b/CHANGES/1014.misc.rst @@ -0,0 +1 @@ +Added more detailed error when server response can't be deserialized. This feature will help to debug unexpected responses from the Server diff --git a/aiogram/client/session/base.py b/aiogram/client/session/base.py index ba337c94..6fa25dd5 100644 --- a/aiogram/client/session/base.py +++ b/aiogram/client/session/base.py @@ -7,7 +7,10 @@ from http import HTTPStatus from types import TracebackType from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable, Final, Optional, Type, Union, cast +from pydantic import ValidationError + from aiogram.exceptions import ( + ClientDecodeError, RestartingTelegram, TelegramAPIError, TelegramBadRequest, @@ -64,8 +67,19 @@ class BaseSession(abc.ABC): """ Check response status """ - json_data = self.json_loads(content) - response = method.build_response(json_data) + try: + json_data = self.json_loads(content) + except Exception as e: + # Handled error type can't be classified as specific error + # in due to decoder can be customized and raise any exception + + raise ClientDecodeError("Failed to decode object", e, content) + + try: + response = method.build_response(json_data) + except ValidationError as e: + raise ClientDecodeError("Failed to deserialize object", e, json_data) + if HTTPStatus.OK <= status_code <= HTTPStatus.IM_USED and response.ok: return response diff --git a/aiogram/exceptions.py b/aiogram/exceptions.py index c47dc7ce..a32c3ec6 100644 --- a/aiogram/exceptions.py +++ b/aiogram/exceptions.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Any, Optional from aiogram.methods import TelegramMethod from aiogram.methods.base import TelegramType @@ -104,3 +104,18 @@ class RestartingTelegram(TelegramServerError): class TelegramEntityTooLarge(TelegramNetworkError): url = "https://core.telegram.org/bots/api#sending-files" + + +class ClientDecodeError(AiogramError): + def __init__(self, message: str, original: Exception, data: Any) -> None: + self.message = message + self.original = original + self.data = data + + def __str__(self) -> str: + original_type = type(self.original) + return ( + f"{self.message}\n" + f"Caused from error: {original_type.__module__}.{original_type.__name__}: {self.original}\n" + f"Content: {self.data}" + ) 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 88a5e626..04de5259 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 @@ -8,6 +8,7 @@ from aiogram import Bot from aiogram.client.session.base import BaseSession, TelegramType from aiogram.client.telegram import PRODUCTION, TelegramAPIServer from aiogram.exceptions import ( + ClientDecodeError, RestartingTelegram, TelegramAPIError, TelegramBadRequest, @@ -183,6 +184,28 @@ class TestBaseSession: if error.url: assert error.url in string + def test_check_response_json_decode_error(self): + session = CustomSession() + method = DeleteMessage(chat_id=42, message_id=42) + + with pytest.raises(ClientDecodeError, match="JSONDecodeError"): + session.check_response( + method=method, + status_code=200, + content="is not a JSON object", + ) + + def test_check_response_validation_error(self): + session = CustomSession() + method = DeleteMessage(chat_id=42, message_id=42) + + with pytest.raises(ClientDecodeError, match="ValidationError"): + session.check_response( + method=method, + status_code=200, + content='{"ok": "test"}', + ) + async def test_make_request(self): session = CustomSession()