Skip to content

Commit e3a12d7

Browse files
more cleanup
1 parent 7823022 commit e3a12d7

File tree

2 files changed

+25
-53
lines changed

2 files changed

+25
-53
lines changed

tests/test_http_client_retry.py

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -54,32 +54,28 @@ def mock_request(*args, **kwargs):
5454
assert mock_sleep.call_count == 2
5555

5656
def test_retries_on_429_rate_limit(self, sync_http_client, retry_config, monkeypatch):
57-
"""Test that 429 errors trigger retry."""
57+
"""Test that 429 errors do NOT trigger retry (consistent with workos-node)."""
5858
call_count = 0
5959

6060
def mock_request(*args, **kwargs):
6161
nonlocal call_count
6262
call_count += 1
63-
if call_count < 2:
64-
return httpx.Response(
65-
status_code=429,
66-
headers={"Retry-After": "0.1"},
67-
json={"error": "Rate limit exceeded"}
68-
)
69-
return httpx.Response(status_code=200, json={"success": True})
63+
return httpx.Response(
64+
status_code=429,
65+
headers={"Retry-After": "0.1"},
66+
json={"error": "Rate limit exceeded"}
67+
)
7068

7169
monkeypatch.setattr(sync_http_client._client, "request", MagicMock(side_effect=mock_request))
7270

73-
with patch("time.sleep") as mock_sleep:
74-
response = sync_http_client.request("test/path", retry_config=retry_config)
71+
with pytest.raises(BadRequestException):
72+
sync_http_client.request("test/path", retry_config=retry_config)
7573

76-
assert call_count == 2
77-
assert response == {"success": True}
78-
# Verify Retry-After header was respected
79-
mock_sleep.assert_called_once_with(0.1)
74+
# Should only be called once (no retries on 429)
75+
assert call_count == 1
8076

8177
def test_no_retry_on_400_error(self, sync_http_client, monkeypatch):
82-
"""Test that 4xx errors (except 429) don't retry (no retry_config passed)."""
78+
"""Test that 4xx errors don't retry (no retry_config passed)."""
8379
call_count = 0
8480

8581
def mock_request(*args, **kwargs):
@@ -345,33 +341,29 @@ async def mock_request(*args, **kwargs):
345341

346342
@pytest.mark.asyncio
347343
async def test_retries_on_429_rate_limit(self, async_http_client, retry_config, monkeypatch):
348-
"""Test that 429 errors trigger retry."""
344+
"""Test that 429 errors do NOT trigger retry (consistent with workos-node)."""
349345
call_count = 0
350346

351347
async def mock_request(*args, **kwargs):
352348
nonlocal call_count
353349
call_count += 1
354-
if call_count < 2:
355-
return httpx.Response(
356-
status_code=429,
357-
headers={"Retry-After": "0.1"},
358-
json={"error": "Rate limit exceeded"}
359-
)
360-
return httpx.Response(status_code=200, json={"success": True})
350+
return httpx.Response(
351+
status_code=429,
352+
headers={"Retry-After": "0.1"},
353+
json={"error": "Rate limit exceeded"}
354+
)
361355

362356
monkeypatch.setattr(async_http_client._client, "request", AsyncMock(side_effect=mock_request))
363357

364-
with patch("asyncio.sleep") as mock_sleep:
365-
response = await async_http_client.request("test/path", retry_config=retry_config)
358+
with pytest.raises(BadRequestException):
359+
await async_http_client.request("test/path", retry_config=retry_config)
366360

367-
assert call_count == 2
368-
assert response == {"success": True}
369-
# Verify Retry-After header was respected
370-
mock_sleep.assert_called_once_with(0.1)
361+
# Should only be called once (no retries on 429)
362+
assert call_count == 1
371363

372364
@pytest.mark.asyncio
373365
async def test_no_retry_on_400_error(self, async_http_client, monkeypatch):
374-
"""Test that 4xx errors (except 429) don't retry (no retry_config passed)."""
366+
"""Test that 4xx errors don't retry (no retry_config passed)."""
375367
call_count = 0
376368

377369
async def mock_request(*args, **kwargs):

workos/utils/_base_http_client.py

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434

3535
DEFAULT_REQUEST_TIMEOUT = 25
3636

37+
# Status codes that should trigger a retry (consistent with workos-node)
38+
RETRY_STATUS_CODES = [408, 500, 502, 504]
3739

3840
@dataclass
3941
class RetryConfig:
@@ -43,13 +45,11 @@ class RetryConfig:
4345
max_delay: float = 30.0 # seconds
4446
jitter: float = 0.25 # 25% jitter
4547

46-
4748
ParamsType = Optional[Mapping[str, Any]]
4849
HeadersType = Optional[Dict[str, str]]
4950
JsonType = Optional[Union[Mapping[str, Any], Sequence[Any]]]
5051
ResponseJson = Mapping[Any, Any]
5152

52-
5353
class PreparedRequest(TypedDict):
5454
method: str
5555
url: str
@@ -213,30 +213,10 @@ def _handle_response(self, response: httpx.Response) -> ResponseJson:
213213

214214
def _is_retryable_error(self, response: httpx.Response) -> bool:
215215
"""Determine if an error should be retried."""
216-
status_code = response.status_code
217-
218-
# Retry on 5xx server errors
219-
if 500 <= status_code < 600:
220-
return True
221-
222-
# Retry on 429 rate limit
223-
if status_code == 429:
224-
return True
225-
226-
# Do NOT retry 4xx client errors (except 429)
227-
return False
216+
return response.status_code in RETRY_STATUS_CODES
228217

229218
def _get_retry_delay(self, attempt: int, response: httpx.Response, retry_config: RetryConfig) -> float:
230219
"""Calculate delay with exponential backoff and jitter."""
231-
# Check for Retry-After header on 429 responses
232-
if response.status_code == 429:
233-
retry_after = response.headers.get("Retry-After")
234-
if retry_after:
235-
try:
236-
return float(retry_after)
237-
except ValueError:
238-
pass # Fall through to exponential backoff
239-
240220
# Exponential backoff: base_delay * 2^attempt
241221
delay = retry_config.base_delay * (2 ** attempt)
242222

0 commit comments

Comments
 (0)