@@ -53,6 +53,83 @@ def mock_request(*args, **kwargs):
5353 # Verify sleep was called with exponential backoff
5454 assert mock_sleep .call_count == 2
5555
56+ def test_retries_on_408_error (self , sync_http_client , retry_config , monkeypatch ):
57+ """Test that 408 (Request Timeout) errors trigger retry."""
58+ call_count = 0
59+
60+ def mock_request (* args , ** kwargs ):
61+ nonlocal call_count
62+ call_count += 1
63+ if call_count < 2 :
64+ return httpx .Response (status_code = 408 , json = {"error" : "Request timeout" })
65+ return httpx .Response (status_code = 200 , json = {"success" : True })
66+
67+ monkeypatch .setattr (sync_http_client ._client , "request" , MagicMock (side_effect = mock_request ))
68+
69+ with patch ("time.sleep" ) as mock_sleep :
70+ response = sync_http_client .request ("test/path" , retry_config = retry_config )
71+
72+ assert call_count == 2
73+ assert response == {"success" : True }
74+ assert mock_sleep .call_count == 1
75+
76+ def test_retries_on_502_error (self , sync_http_client , retry_config , monkeypatch ):
77+ """Test that 502 (Bad Gateway) errors trigger retry."""
78+ call_count = 0
79+
80+ def mock_request (* args , ** kwargs ):
81+ nonlocal call_count
82+ call_count += 1
83+ if call_count < 3 :
84+ return httpx .Response (status_code = 502 , json = {"error" : "Bad gateway" })
85+ return httpx .Response (status_code = 200 , json = {"success" : True })
86+
87+ monkeypatch .setattr (sync_http_client ._client , "request" , MagicMock (side_effect = mock_request ))
88+
89+ with patch ("time.sleep" ) as mock_sleep :
90+ response = sync_http_client .request ("test/path" , retry_config = retry_config )
91+
92+ assert call_count == 3
93+ assert response == {"success" : True }
94+ assert mock_sleep .call_count == 2
95+
96+ def test_retries_on_504_error (self , sync_http_client , retry_config , monkeypatch ):
97+ """Test that 504 (Gateway Timeout) errors trigger retry."""
98+ call_count = 0
99+
100+ def mock_request (* args , ** kwargs ):
101+ nonlocal call_count
102+ call_count += 1
103+ if call_count < 2 :
104+ return httpx .Response (status_code = 504 , json = {"error" : "Gateway timeout" })
105+ return httpx .Response (status_code = 200 , json = {"success" : True })
106+
107+ monkeypatch .setattr (sync_http_client ._client , "request" , MagicMock (side_effect = mock_request ))
108+
109+ with patch ("time.sleep" ) as mock_sleep :
110+ response = sync_http_client .request ("test/path" , retry_config = retry_config )
111+
112+ assert call_count == 2
113+ assert response == {"success" : True }
114+ assert mock_sleep .call_count == 1
115+
116+ def test_no_retry_on_503_error (self , sync_http_client , retry_config , monkeypatch ):
117+ """Test that 503 (Service Unavailable) errors do NOT trigger retry (not in RETRY_STATUS_CODES)."""
118+ call_count = 0
119+
120+ def mock_request (* args , ** kwargs ):
121+ nonlocal call_count
122+ call_count += 1
123+ return httpx .Response (status_code = 503 , json = {"error" : "Service unavailable" })
124+
125+ monkeypatch .setattr (sync_http_client ._client , "request" , MagicMock (side_effect = mock_request ))
126+
127+ with pytest .raises (ServerException ):
128+ sync_http_client .request ("test/path" , retry_config = retry_config )
129+
130+ # Should only be called once (no retries on 503)
131+ assert call_count == 1
132+
56133 def test_retries_on_429_rate_limit (self , sync_http_client , retry_config , monkeypatch ):
57134 """Test that 429 errors do NOT trigger retry (consistent with workos-node)."""
58135 call_count = 0
@@ -339,6 +416,87 @@ async def mock_request(*args, **kwargs):
339416 # Verify sleep was called with exponential backoff
340417 assert mock_sleep .call_count == 2
341418
419+ @pytest .mark .asyncio
420+ async def test_retries_on_408_error (self , async_http_client , retry_config , monkeypatch ):
421+ """Test that 408 (Request Timeout) errors trigger retry."""
422+ call_count = 0
423+
424+ async def mock_request (* args , ** kwargs ):
425+ nonlocal call_count
426+ call_count += 1
427+ if call_count < 2 :
428+ return httpx .Response (status_code = 408 , json = {"error" : "Request timeout" })
429+ return httpx .Response (status_code = 200 , json = {"success" : True })
430+
431+ monkeypatch .setattr (async_http_client ._client , "request" , AsyncMock (side_effect = mock_request ))
432+
433+ with patch ("asyncio.sleep" ) as mock_sleep :
434+ response = await async_http_client .request ("test/path" , retry_config = retry_config )
435+
436+ assert call_count == 2
437+ assert response == {"success" : True }
438+ assert mock_sleep .call_count == 1
439+
440+ @pytest .mark .asyncio
441+ async def test_retries_on_502_error (self , async_http_client , retry_config , monkeypatch ):
442+ """Test that 502 (Bad Gateway) errors trigger retry."""
443+ call_count = 0
444+
445+ async def mock_request (* args , ** kwargs ):
446+ nonlocal call_count
447+ call_count += 1
448+ if call_count < 3 :
449+ return httpx .Response (status_code = 502 , json = {"error" : "Bad gateway" })
450+ return httpx .Response (status_code = 200 , json = {"success" : True })
451+
452+ monkeypatch .setattr (async_http_client ._client , "request" , AsyncMock (side_effect = mock_request ))
453+
454+ with patch ("asyncio.sleep" ) as mock_sleep :
455+ response = await async_http_client .request ("test/path" , retry_config = retry_config )
456+
457+ assert call_count == 3
458+ assert response == {"success" : True }
459+ assert mock_sleep .call_count == 2
460+
461+ @pytest .mark .asyncio
462+ async def test_retries_on_504_error (self , async_http_client , retry_config , monkeypatch ):
463+ """Test that 504 (Gateway Timeout) errors trigger retry."""
464+ call_count = 0
465+
466+ async def mock_request (* args , ** kwargs ):
467+ nonlocal call_count
468+ call_count += 1
469+ if call_count < 2 :
470+ return httpx .Response (status_code = 504 , json = {"error" : "Gateway timeout" })
471+ return httpx .Response (status_code = 200 , json = {"success" : True })
472+
473+ monkeypatch .setattr (async_http_client ._client , "request" , AsyncMock (side_effect = mock_request ))
474+
475+ with patch ("asyncio.sleep" ) as mock_sleep :
476+ response = await async_http_client .request ("test/path" , retry_config = retry_config )
477+
478+ assert call_count == 2
479+ assert response == {"success" : True }
480+ assert mock_sleep .call_count == 1
481+
482+ @pytest .mark .asyncio
483+ async def test_no_retry_on_503_error (self , async_http_client , retry_config , monkeypatch ):
484+ """Test that 503 (Service Unavailable) errors do NOT trigger retry (not in RETRY_STATUS_CODES)."""
485+ call_count = 0
486+
487+ async def mock_request (* args , ** kwargs ):
488+ nonlocal call_count
489+ call_count += 1
490+ return httpx .Response (status_code = 503 , json = {"error" : "Service unavailable" })
491+
492+ monkeypatch .setattr (async_http_client ._client , "request" , AsyncMock (side_effect = mock_request ))
493+
494+ with pytest .raises (ServerException ):
495+ await async_http_client .request ("test/path" , retry_config = retry_config )
496+
497+ # Should only be called once (no retries on 503)
498+ assert call_count == 1
499+
342500 @pytest .mark .asyncio
343501 async def test_retries_on_429_rate_limit (self , async_http_client , retry_config , monkeypatch ):
344502 """Test that 429 errors do NOT trigger retry (consistent with workos-node)."""
0 commit comments