From 488c49b15c27131ff6537b72f88fe26c2006b0eb Mon Sep 17 00:00:00 2001 From: ptiurin Date: Tue, 7 Oct 2025 16:21:39 +0100 Subject: [PATCH 1/2] chore(NoTicket): Improve plaintext error handling --- src/firebolt/utils/util.py | 6 ++++++ tests/unit/async_db/test_cursor.py | 23 +++++++++++++++++++++++ tests/unit/db/test_cursor.py | 23 +++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/src/firebolt/utils/util.py b/src/firebolt/utils/util.py index d296055f0d1..a57fc7370e9 100644 --- a/src/firebolt/utils/util.py +++ b/src/firebolt/utils/util.py @@ -19,6 +19,7 @@ from firebolt.utils.exception import ( ConfigurationError, + FireboltError, FireboltStructuredError, ) @@ -171,6 +172,10 @@ def raise_error_from_response(resp: Response) -> None: resp (Response): HTTP response """ to_raise = None + # If error is Text - raise as is + if "text/plain" in resp.headers.get("Content-Type", ""): + raise FireboltError(resp.text) + # If error is Json - parse it and raise try: decoded = resp.json() if "errors" in decoded and len(decoded["errors"]) > 0: @@ -186,6 +191,7 @@ def raise_error_from_response(resp: Response) -> None: raise to_raise # Raise status error if no error info was found in the body + # This error does not contain the response body resp.raise_for_status() diff --git a/tests/unit/async_db/test_cursor.py b/tests/unit/async_db/test_cursor.py index d26345d1631..5acc449e3c4 100644 --- a/tests/unit/async_db/test_cursor.py +++ b/tests/unit/async_db/test_cursor.py @@ -1505,3 +1505,26 @@ async def test_unsupported_paramstyle_raises(cursor: Cursor) -> None: await cursor.execute("SELECT 1") finally: db.paramstyle = original_paramstyle + + +async def test_cursor_plaintext_error( + httpx_mock: HTTPXMock, + cursor: Cursor, + query_url: str, +): + """Test handling of plaintext error responses from the server.""" + httpx_mock.add_callback( + lambda *args, **kwargs: Response( + status_code=codes.NOT_FOUND, + text="Plaintext error message", + headers={"Content-Type": "text/plain"}, + ), + url=query_url, + ) + with raises(FireboltError) as excinfo: + await cursor.execute("select * from t") + + assert cursor._state == CursorState.ERROR + assert "Plaintext error message" in str( + excinfo.value + ), "Invalid error message for plaintext error response" diff --git a/tests/unit/db/test_cursor.py b/tests/unit/db/test_cursor.py index e6325b6a5ed..d36e3565cc7 100644 --- a/tests/unit/db/test_cursor.py +++ b/tests/unit/db/test_cursor.py @@ -1391,3 +1391,26 @@ def test_unsupported_paramstyle_raises(cursor): cursor.execute("SELECT 1") finally: db.paramstyle = original_paramstyle + + +def test_cursor_plaintext_error( + httpx_mock: HTTPXMock, + cursor: Cursor, + query_url: str, +): + """Test handling of plaintext error responses from the server.""" + httpx_mock.add_callback( + lambda *args, **kwargs: Response( + status_code=codes.NOT_FOUND, + text="Plaintext error message", + headers={"Content-Type": "text/plain"}, + ), + url=query_url, + ) + with raises(FireboltError) as excinfo: + cursor.execute("select * from t") + + assert cursor._state == CursorState.ERROR + assert "Plaintext error message" in str( + excinfo.value + ), "Invalid error message for plaintext error response" From f6721da5f250f2ba4fb7ac23803f33dbdf2f9f53 Mon Sep 17 00:00:00 2001 From: ptiurin Date: Tue, 7 Oct 2025 17:29:28 +0100 Subject: [PATCH 2/2] fix core tests --- tests/integration/dbapi/async/V2/test_queries_async.py | 4 ++-- tests/integration/dbapi/sync/V2/test_queries.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/dbapi/async/V2/test_queries_async.py b/tests/integration/dbapi/async/V2/test_queries_async.py index e2042e41834..7da9da0e02a 100644 --- a/tests/integration/dbapi/async/V2/test_queries_async.py +++ b/tests/integration/dbapi/async/V2/test_queries_async.py @@ -11,7 +11,7 @@ from firebolt.client.auth.base import Auth from firebolt.common._types import ColType from firebolt.common.row_set.types import Column -from firebolt.utils.exception import FireboltStructuredError +from firebolt.utils.exception import FireboltError, FireboltStructuredError from tests.integration.dbapi.conftest import LONG_SELECT_DEFAULT_V2 from tests.integration.dbapi.utils import assert_deep_eq @@ -341,7 +341,7 @@ async def test_multi_statement_query(connection: Connection) -> None: async def test_set_invalid_parameter(connection: Connection): async with connection.cursor() as c: assert len(c._set_parameters) == 0 - with raises((OperationalError, FireboltStructuredError)) as e: + with raises((OperationalError, FireboltError)) as e: await c.execute("SET some_invalid_parameter = 1") assert "Unknown setting" in str(e.value) or "query param not allowed" in str( diff --git a/tests/integration/dbapi/sync/V2/test_queries.py b/tests/integration/dbapi/sync/V2/test_queries.py index 9b415cef88d..36f22d39f3b 100644 --- a/tests/integration/dbapi/sync/V2/test_queries.py +++ b/tests/integration/dbapi/sync/V2/test_queries.py @@ -11,7 +11,7 @@ from firebolt.common._types import ColType from firebolt.common.row_set.types import Column from firebolt.db import Binary, Connection, Cursor, OperationalError, connect -from firebolt.utils.exception import FireboltStructuredError +from firebolt.utils.exception import FireboltError, FireboltStructuredError from tests.integration.dbapi.conftest import LONG_SELECT_DEFAULT_V2 from tests.integration.dbapi.utils import assert_deep_eq @@ -341,7 +341,7 @@ def test_multi_statement_query(connection: Connection) -> None: def test_set_invalid_parameter(connection: Connection): with connection.cursor() as c: assert len(c._set_parameters) == 0 - with raises((OperationalError, FireboltStructuredError)) as e: + with raises((OperationalError, FireboltError)) as e: c.execute("set some_invalid_parameter = 1") assert "Unknown setting" in str(e.value) or "query param not allowed" in str( e.value