Skip to content

Commit e9105b3

Browse files
committed
fix: harden response parsing and add missing None guards
- Guard int() on x-stack-actual-status header against malformed values in _parse_response and both retry loops (ValueError falls back to response.status_code) - Validate response.json() returns dict before calling .get() on known-error body (non-dict JSON falls back to empty dict) - Add None guard to get_email_delivery_stats before model_validate (matches pattern used by get_user, get_team, get_item)
1 parent 131bb89 commit e9105b3

2 files changed

Lines changed: 21 additions & 4 deletions

File tree

sdks/implementations/python/src/stack_auth/_app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,6 +1369,8 @@ def get_email_delivery_stats(self) -> EmailDeliveryInfo:
13691369
An :class:`EmailDeliveryInfo` with delivery counts and statuses.
13701370
"""
13711371
data = self._client.request("GET", "/emails/delivery-stats")
1372+
if data is None:
1373+
return EmailDeliveryInfo(delivered=0, bounced=0, complained=0, total=0)
13721374
return EmailDeliveryInfo.model_validate(data)
13731375

13741376
# -- data vault ----------------------------------------------------------
@@ -2707,6 +2709,8 @@ async def get_email_delivery_stats(self) -> EmailDeliveryInfo:
27072709
An :class:`EmailDeliveryInfo` with delivery counts and statuses.
27082710
"""
27092711
data = await self._client.request("GET", "/emails/delivery-stats")
2712+
if data is None:
2713+
return EmailDeliveryInfo(delivered=0, bounced=0, complained=0, total=0)
27102714
return EmailDeliveryInfo.model_validate(data)
27112715

27122716
# -- data vault ----------------------------------------------------------

sdks/implementations/python/src/stack_auth/_client.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,20 @@ def _parse_response(self, response: httpx.Response) -> tuple[int, dict[str, Any]
7878
"""
7979
# Determine real status
8080
actual_status_header = response.headers.get("x-stack-actual-status")
81-
actual_status = int(actual_status_header) if actual_status_header else response.status_code
81+
if actual_status_header:
82+
try:
83+
actual_status = int(actual_status_header)
84+
except ValueError:
85+
actual_status = response.status_code
86+
else:
87+
actual_status = response.status_code
8288

8389
# Known-error dispatch
8490
known_error = response.headers.get("x-stack-known-error")
8591
if known_error:
8692
try:
87-
body = response.json()
93+
parsed = response.json()
94+
body = parsed if isinstance(parsed, dict) else {}
8895
except Exception:
8996
body = {}
9097
raise StackAuthError.from_response(
@@ -170,7 +177,10 @@ def request(
170177

171178
# Check for 429 via x-stack-actual-status
172179
actual_status_hdr = resp.headers.get("x-stack-actual-status")
173-
actual_status = int(actual_status_hdr) if actual_status_hdr else resp.status_code
180+
try:
181+
actual_status = int(actual_status_hdr) if actual_status_hdr else resp.status_code
182+
except ValueError:
183+
actual_status = resp.status_code
174184

175185
# 429 retries apply to ALL methods (including POST/PATCH).
176186
# Unlike network errors, a 429 guarantees the server did NOT
@@ -251,7 +261,10 @@ async def request(
251261
)
252262

253263
actual_status_hdr = resp.headers.get("x-stack-actual-status")
254-
actual_status = int(actual_status_hdr) if actual_status_hdr else resp.status_code
264+
try:
265+
actual_status = int(actual_status_hdr) if actual_status_hdr else resp.status_code
266+
except ValueError:
267+
actual_status = resp.status_code
255268

256269
# 429 retries apply to ALL methods (including POST/PATCH).
257270
# Unlike network errors, a 429 guarantees the server did NOT

0 commit comments

Comments
 (0)