From c2394d9a43ebfab82619efd99927c70b5c40322e Mon Sep 17 00:00:00 2001 From: Bortlesboat <169967362+Bortlesboat@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:56:30 -0400 Subject: [PATCH 1/3] fix: preserve SDK errors for invalid JSON byte encodings --- resend/async_request.py | 2 +- resend/request.py | 2 +- tests/request_test.py | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/resend/async_request.py b/resend/async_request.py index 2634c15..c647ac9 100644 --- a/resend/async_request.py +++ b/resend/async_request.py @@ -170,7 +170,7 @@ async def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]: parsed_data["http_headers"] = dict(self._response_headers) # For list responses, return as-is (lists can't have headers key) return parsed_data - except json.JSONDecodeError: + except (json.JSONDecodeError, UnicodeDecodeError): raise_for_code_and_type( code=error_code, message="Failed to decode JSON response", diff --git a/resend/request.py b/resend/request.py index e7e2783..56d27c1 100644 --- a/resend/request.py +++ b/resend/request.py @@ -155,7 +155,7 @@ def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]: parsed_data["http_headers"] = dict(self._response_headers) # For list responses, return as-is (lists can't have headers key) return parsed_data - except json.JSONDecodeError: + except (json.JSONDecodeError, UnicodeDecodeError): raise_for_code_and_type( code=error_code, message="Failed to decode JSON response", diff --git a/tests/request_test.py b/tests/request_test.py index 56e2b3d..d2ab8ea 100644 --- a/tests/request_test.py +++ b/tests/request_test.py @@ -10,6 +10,46 @@ from resend.version import get_version +@pytest.mark.parametrize("content", [b'"\xff"', b'\xff\xfe{', b'not-json']) +@pytest.mark.parametrize("status_code", [200, 429, 502]) +class TestResponseDecodingErrors: + def test_sync_preserves_status_and_headers( + self, content: bytes, status_code: int + ) -> None: + headers = {"content-type": "application/json", "retry-after": "2"} + response = Mock(content=content, status_code=status_code, headers=headers) + req = request.Request[Dict[str, Any]]("/emails", {}, "get") + + with patch("resend.http_client_requests.requests.request", return_value=response): + with pytest.raises(ResendError) as error: + req.perform() + + assert error.value.code == (status_code if status_code >= 400 else 500) + assert error.value.headers == headers + assert error.value.message == "Failed to decode JSON response" + + @pytest.mark.asyncio + async def test_async_preserves_status_and_headers( + self, content: bytes, status_code: int + ) -> None: + from resend.async_request import AsyncRequest + from resend.http_client_httpx import HTTPXClient + + headers = {"content-type": "application/json", "retry-after": "2"} + response = Mock(content=content, status_code=status_code, headers=headers) + req = AsyncRequest[Dict[str, Any]]("/emails", {}, "get") + + with patch("resend.default_async_http_client", HTTPXClient()): + with patch("httpx.AsyncClient.request", new_callable=AsyncMock) as send: + send.return_value = response + with pytest.raises(ResendError) as error: + await req.perform() + + assert error.value.code == (status_code if status_code >= 400 else 500) + assert error.value.headers == headers + assert error.value.message == "Failed to decode JSON response" + + class TestResendRequest(unittest.TestCase): @patch("resend.http_client_requests.requests.request") @patch("resend.api_key", new="test_key") From eba77a81232c4506a9e1593947ae2ab79ef735de Mon Sep 17 00:00:00 2001 From: Gabriel Miranda Date: Tue, 8 Sep 2026 11:03:03 -0300 Subject: [PATCH 2/3] bump Signed-off-by: Gabriel Miranda --- resend/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resend/version.py b/resend/version.py index ddfd28c..917f42b 100644 --- a/resend/version.py +++ b/resend/version.py @@ -1,4 +1,4 @@ -__version__ = "2.43.0" +__version__ = "2.43.1" def get_version() -> str: From d9fa06e3b03d2a430e9cdbfcd477cadb60bcb5ca Mon Sep 17 00:00:00 2001 From: Bortlesboat <169967362+Bortlesboat@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:17:14 -0400 Subject: [PATCH 3/3] test: run response decoding coroutine without a marker --- tests/request_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/request_test.py b/tests/request_test.py index d2ab8ea..cde36de 100644 --- a/tests/request_test.py +++ b/tests/request_test.py @@ -1,3 +1,4 @@ +import asyncio import unittest from typing import Any, Dict from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -28,8 +29,7 @@ def test_sync_preserves_status_and_headers( assert error.value.headers == headers assert error.value.message == "Failed to decode JSON response" - @pytest.mark.asyncio - async def test_async_preserves_status_and_headers( + def test_async_preserves_status_and_headers( self, content: bytes, status_code: int ) -> None: from resend.async_request import AsyncRequest @@ -43,7 +43,7 @@ async def test_async_preserves_status_and_headers( with patch("httpx.AsyncClient.request", new_callable=AsyncMock) as send: send.return_value = response with pytest.raises(ResendError) as error: - await req.perform() + asyncio.run(req.perform()) assert error.value.code == (status_code if status_code >= 400 else 500) assert error.value.headers == headers