Skip to content

Commit e748538

Browse files
test: cover end-to-end retry on HTTP 504
Adds TestRetryOn504EndToEnd which drives a real request through RetryableHTTPTransport / RetryableAsyncHTTPTransport and asserts the underlying call fires max_retries times before the final 504 response is returned. The existing tests only covered configuration/wiring, not the actual retry loop behavior against a real httpx.Response. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 647549e commit e748538

1 file changed

Lines changed: 82 additions & 0 deletions

File tree

tests/core/features/test_retry.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import logging
44
from unittest.mock import MagicMock, patch
55

6+
import httpx
7+
import pytest
8+
9+
from uipath.llm_client.httpx_client import UiPathHttpxAsyncClient, UiPathHttpxClient
610
from uipath.llm_client.utils.exceptions import (
711
UiPathBadGatewayError,
812
UiPathGatewayTimeoutError,
@@ -19,6 +23,8 @@
1923
RetryConfig,
2024
)
2125

26+
_NO_DELAY_CONFIG: RetryConfig = {"initial_delay": 0, "max_delay": 0, "jitter": 0}
27+
2228

2329
class TestDefaultRetryOnExceptions:
2430
"""Pins the default retry set so HTTP 408/429/502/503/504/529 stay retryable."""
@@ -263,3 +269,79 @@ def test_with_logger_adds_before_sleep(self):
263269
result = _build_retryer(max_retries=2, retry_config=None, logger=logger)
264270
assert result is not None
265271
assert result.before_sleep is not None
272+
273+
274+
class TestRetryOn504EndToEnd:
275+
"""End-to-end coverage that a 504 response actually triggers the retry loop.
276+
277+
The other test classes verify the *configuration* (default exception set,
278+
retryer wiring, wait math). These tests drive a real request through
279+
``RetryableHTTPTransport.handle_request`` / ``handle_async_request`` and
280+
assert the underlying call fires ``max_retries`` times before the final 504
281+
response is returned.
282+
"""
283+
284+
def test_sync_504_retries_max_retries_times_then_returns_response(self):
285+
calls = 0
286+
287+
def fake_504(self: httpx.HTTPTransport, request: httpx.Request) -> httpx.Response:
288+
nonlocal calls
289+
calls += 1
290+
return httpx.Response(504, content=b"{}", request=request)
291+
292+
client = UiPathHttpxClient(
293+
base_url="https://example.com",
294+
max_retries=3,
295+
retry_config=_NO_DELAY_CONFIG,
296+
)
297+
try:
298+
with patch.object(httpx.HTTPTransport, "handle_request", fake_504):
299+
response = client.post("/anything", json={})
300+
finally:
301+
client.close()
302+
303+
assert calls == 3
304+
assert response.status_code == 504
305+
306+
def test_sync_max_retries_zero_makes_exactly_one_call(self):
307+
calls = 0
308+
309+
def fake_504(self: httpx.HTTPTransport, request: httpx.Request) -> httpx.Response:
310+
nonlocal calls
311+
calls += 1
312+
return httpx.Response(504, content=b"{}", request=request)
313+
314+
client = UiPathHttpxClient(base_url="https://example.com", max_retries=0)
315+
try:
316+
with patch.object(httpx.HTTPTransport, "handle_request", fake_504):
317+
response = client.post("/anything", json={})
318+
finally:
319+
client.close()
320+
321+
assert calls == 1
322+
assert response.status_code == 504
323+
324+
@pytest.mark.asyncio
325+
async def test_async_504_retries_max_retries_times_then_returns_response(self):
326+
calls = 0
327+
328+
async def fake_504(
329+
self: httpx.AsyncHTTPTransport, request: httpx.Request
330+
) -> httpx.Response:
331+
nonlocal calls
332+
calls += 1
333+
return httpx.Response(504, content=b"{}", request=request)
334+
335+
client = UiPathHttpxAsyncClient(
336+
base_url="https://example.com",
337+
max_retries=3,
338+
retry_config=_NO_DELAY_CONFIG,
339+
)
340+
try:
341+
with patch.object(httpx.AsyncHTTPTransport, "handle_async_request", fake_504):
342+
response = await client.post("/anything", json={})
343+
finally:
344+
await client.aclose()
345+
346+
assert calls == 3
347+
assert response.status_code == 504

0 commit comments

Comments
 (0)