From c2598ccd62fb9de0561c7c837fb518f836200eba Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 23 Aug 2026 07:06:02 +0000 Subject: [PATCH 1/2] test: fail on JSONDecodeError chain from empty 429 (#108) Add a red-on-purpose pytest that hits get_agent_me through the real BandLink REST client with an ALB-style empty 429 body. Asserts the raised ApiError has no JSONDecodeError in __cause__/__context__ and no JSONDecodeError in the formatted traceback. Co-authored-by: Alexander Nikitin --- tests/platform/test_rest_empty_429.py | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/platform/test_rest_empty_429.py diff --git a/tests/platform/test_rest_empty_429.py b/tests/platform/test_rest_empty_429.py new file mode 100644 index 000000000..5865352a8 --- /dev/null +++ b/tests/platform/test_rest_empty_429.py @@ -0,0 +1,43 @@ +"""Empty HTTP 429 bodies must raise a clean ApiError (GitHub #108). + +AWS ALB rate-limit responses often have ``Content-Length: 0``. The Fern +raw client still calls ``_response.json()`` for that status, then wraps +the resulting ``JSONDecodeError`` in ``ApiError`` without ``from None``. +Python therefore prints both tracebacks. + +Interception is the maintained ``pytest-httpx`` ``httpx_mock`` fixture on +the real ``BandLink`` REST client, matching ``test_link_credentials.py``. +``max_retries=0`` isolates the decode/chaining bug from Fern 429 retries. +""" + +from __future__ import annotations + +import traceback +from json.decoder import JSONDecodeError + +import pytest +from band_rest.core.api_error import ApiError +from pytest_httpx import HTTPXMock + +from band.platform.link import BandLink + + +async def test_empty_429_raises_api_error_without_json_decode_chain( + httpx_mock: HTTPXMock, +) -> None: + httpx_mock.add_response(status_code=429, content=b"") + + link = BandLink(agent_id="agent-1", api_key="test-key") + with pytest.raises(ApiError) as exc_info: + await link.rest.agent_api_identity.get_agent_me( + request_options={"max_retries": 0}, + ) + + error = exc_info.value + formatted = "".join(traceback.format_exception(error)) + assert error.status_code == 429 + assert not isinstance(error.__cause__, JSONDecodeError) + assert error.__suppress_context__ or not isinstance( + error.__context__, JSONDecodeError + ) + assert "JSONDecodeError" not in formatted From 3e1bce399a7538c18ea1c15caed8e41d1f92dbdf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 23 Aug 2026 07:06:48 +0000 Subject: [PATCH 2/2] test: include chained traceback in empty-429 assertion Surface the dual JSONDecodeError/ApiError traceback in the pytest failure message so the #108 symptom is visible in the red run. Co-authored-by: Alexander Nikitin --- tests/platform/test_rest_empty_429.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/platform/test_rest_empty_429.py b/tests/platform/test_rest_empty_429.py index 5865352a8..722090a05 100644 --- a/tests/platform/test_rest_empty_429.py +++ b/tests/platform/test_rest_empty_429.py @@ -39,5 +39,5 @@ async def test_empty_429_raises_api_error_without_json_decode_chain( assert not isinstance(error.__cause__, JSONDecodeError) assert error.__suppress_context__ or not isinstance( error.__context__, JSONDecodeError - ) + ), formatted assert "JSONDecodeError" not in formatted