Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions resend/async_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,32 @@ def __init__(
self.files = files
self.data = data
self._response_headers: Dict[str, str] = {}
self._response_status_code: Optional[int] = None

async def perform(self) -> Union[T, None]:
data = await self.make_request(url=f"{resend.api_url}{self.path}")

if isinstance(data, dict) and data.get("statusCode") not in (None, 200):
body_status_code = data.get("statusCode") if isinstance(data, dict) else None
error_code = (
self._response_status_code
if self._response_status_code is not None
and self._response_status_code >= 400
else body_status_code
)

if error_code not in (None, 200):
raise_for_code_and_type(
code=data.get("statusCode") or 500,
message=data.get("message", "Unknown error"),
error_type=data.get("name", "InternalServerError"),
code=error_code or 500,
message=(
data.get("message", "Unknown error")
if isinstance(data, dict)
else "Unknown error"
),
error_type=(
data.get("name", "InternalServerError")
if isinstance(data, dict)
else "InternalServerError"
),
headers=self._response_headers,
)

Expand Down Expand Up @@ -112,7 +129,7 @@ async def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]:
if self.data is not None:
kwargs["data"] = self.data

content, _status_code, resp_headers = await async_client.request(**kwargs)
content, status_code, resp_headers = await async_client.request(**kwargs)

# Safety net around the HTTP Client
except ResendError:
Expand All @@ -127,16 +144,22 @@ async def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]:

# Store response headers for later access
self._response_headers = dict(resp_headers)
self._response_status_code = status_code

# When the body is not usable JSON (CDN HTML, empty 5xx, proxies), the
# HTTP status is the only trustworthy signal. Keep it for 4xx/5xx;
# fall back to 500 if the status looks successful but the body is not.
error_code = status_code if status_code >= 400 else 500

content_type = {k.lower(): v for k, v in resp_headers.items()}.get(
"content-type", ""
)

if "application/json" not in content_type:
raise_for_code_and_type(
code=500,
code=error_code,
message=f"Expected JSON response but got: {content_type}",
error_type="InternalServerError",
error_type="application_error",
headers=self._response_headers,
)

Expand All @@ -149,8 +172,8 @@ async def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]:
return parsed_data
except json.JSONDecodeError:
raise_for_code_and_type(
code=500,
code=error_code,
message="Failed to decode JSON response",
error_type="InternalServerError",
error_type="application_error",
headers=self._response_headers,
)
41 changes: 32 additions & 9 deletions resend/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,32 @@ def __init__(
self.files = files
self.data = data
self._response_headers: Dict[str, str] = {}
self._response_status_code: Optional[int] = None

def perform(self) -> Union[T, None]:
data = self.make_request(url=f"{resend.api_url}{self.path}")

if isinstance(data, dict) and data.get("statusCode") not in (None, 200):
body_status_code = data.get("statusCode") if isinstance(data, dict) else None
error_code = (
self._response_status_code
if self._response_status_code is not None
and self._response_status_code >= 400
else body_status_code
)

if error_code not in (None, 200):
raise_for_code_and_type(
code=data.get("statusCode") or 500,
message=data.get("message", "Unknown error"),
error_type=data.get("name", "InternalServerError"),
code=error_code or 500,
message=(
data.get("message", "Unknown error")
if isinstance(data, dict)
else "Unknown error"
),
error_type=(
data.get("name", "InternalServerError")
if isinstance(data, dict)
else "InternalServerError"
),
headers=self._response_headers,
)

Expand Down Expand Up @@ -99,7 +116,7 @@ def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]:
if self.data is not None:
kwargs["data"] = self.data

content, _status_code, resp_headers = sync_client.request(**kwargs)
content, status_code, resp_headers = sync_client.request(**kwargs)

# Safety net around the HTTP Client
except Exception as e:
Expand All @@ -112,16 +129,22 @@ def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]:

# Store response headers for later access
self._response_headers = dict(resp_headers)
self._response_status_code = status_code

# When the body is not usable JSON (CDN HTML, empty 5xx, proxies), the
# HTTP status is the only trustworthy signal. Keep it for 4xx/5xx;
# fall back to 500 if the status looks successful but the body is not.
error_code = status_code if status_code >= 400 else 500

content_type = {k.lower(): v for k, v in resp_headers.items()}.get(
"content-type", ""
)

if "application/json" not in content_type:
raise_for_code_and_type(
code=500,
code=error_code,
message=f"Expected JSON response but got: {content_type}",
error_type="InternalServerError",
error_type="application_error",
headers=self._response_headers,
)

Expand All @@ -134,8 +157,8 @@ def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]:
return parsed_data
except json.JSONDecodeError:
raise_for_code_and_type(
code=500,
code=error_code,
message="Failed to decode JSON response",
error_type="InternalServerError",
error_type="application_error",
headers=self._response_headers,
)
24 changes: 2 additions & 22 deletions tests/emails_test.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
from unittest.mock import MagicMock, Mock
from unittest.mock import Mock

import resend
from resend import EmailsReceiving
from resend.exceptions import NoContentError, ResendError
from resend.exceptions import NoContentError
from tests.conftest import ResendBaseTest

# flake8: noqa


class TestResendEmail(ResendBaseTest):

def test_email_send_with_from(self) -> None:
self.set_mock_json(
{
Expand Down Expand Up @@ -68,25 +67,6 @@ def test_should_get_email_raise_exception_when_no_content(self) -> None:
email_id="4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
)

def test_email_response_html(self) -> None:
self.set_magic_mock_obj(
MagicMock(
status_code=200,
headers={"content-type": "text/html; charset=utf-8"},
text="<strong>hello, world!</strong>",
)
)
params: resend.Emails.SendParams = {
"to": "[email protected]",
"from": "[email protected]",
"subject": "subject",
"html": "html",
}
try:
_ = resend.Emails.send(params)
except ResendError as e:
assert e.message == "Failed to parse Resend API response. Please try again."

def test_update_email(self) -> None:
self.set_mock_json(
{
Expand Down
Loading
Loading