Skip to content

Commit 9638ff1

Browse files
eeshsaxenasjrl
andauthored
fix: honor custom timeout on every attempt in request_with_retry (#11998)
Co-authored-by: Sebastian Husch Lee <10526848+sjrl@users.noreply.github.com>
1 parent 07b5ef4 commit 9638ff1

3 files changed

Lines changed: 63 additions & 2 deletions

File tree

haystack/utils/requests_utils.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ def request_with_retry(
6161
if status_codes_to_retry is None:
6262
status_codes_to_retry = [408, 418, 429, 503]
6363

64+
# Pop `timeout` once, before the retry loop.
65+
timeout = kwargs.pop("timeout", 10)
66+
6467
@retry(
6568
reraise=True,
6669
wait=wait_exponential(),
@@ -70,7 +73,6 @@ def request_with_retry(
7073
after=after_log(logger, logging.DEBUG),
7174
)
7275
def run() -> httpx.Response:
73-
timeout = kwargs.pop("timeout", 10)
7476
with httpx.Client() as client:
7577
res = client.request(**kwargs, timeout=timeout)
7678

@@ -171,6 +173,9 @@ async def example_5xx():
171173
if status_codes_to_retry is None:
172174
status_codes_to_retry = [408, 418, 429, 503]
173175

176+
# Pop `timeout` once, before the retry loop.
177+
timeout = kwargs.pop("timeout", 10)
178+
174179
@retry(
175180
reraise=True,
176181
wait=wait_exponential(),
@@ -180,7 +185,6 @@ async def example_5xx():
180185
after=after_log(logger, logging.DEBUG),
181186
)
182187
async def run() -> httpx.Response:
183-
timeout = kwargs.pop("timeout", 10)
184188
async with httpx.AsyncClient() as client:
185189
res = await client.request(**kwargs, timeout=timeout)
186190

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
``request_with_retry`` and ``async_request_with_retry`` now honor a custom ``timeout`` on every
5+
retry attempt. Previously the ``timeout`` was consumed on the first attempt and any subsequent
6+
retry silently fell back to the default of 10 seconds.

test/utils/test_requests_utils.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,30 @@ def raise_for_status():
121121
assert mock_request.call_count == 2
122122
mock_sleep.assert_called()
123123

124+
def test_request_with_retry_custom_timeout_preserved_across_retries(self):
125+
"""A custom timeout must be honored on every attempt, not just the first one.
126+
127+
Regression test: ``timeout`` used to be popped from the shared ``kwargs`` inside the
128+
retried inner function, so after the first attempt it was gone and every subsequent
129+
retry silently fell back to the default of 10 seconds.
130+
"""
131+
with patch("time.sleep", return_value=None):
132+
success_response = httpx.Response(status_code=200, request=httpx.Request("GET", "https://example.com"))
133+
134+
with patch("httpx.Client.request") as mock_request:
135+
# First attempt fails with a retryable error, second attempt succeeds.
136+
mock_request.side_effect = [
137+
httpx.RequestError("boom", request=httpx.Request("GET", "https://example.com")),
138+
success_response,
139+
]
140+
141+
response = request_with_retry(method="GET", url="https://example.com", attempts=2, timeout=5)
142+
143+
assert response == success_response
144+
assert mock_request.call_count == 2
145+
# Both the initial attempt and the retry must use the caller's timeout, not the default.
146+
assert [call.kwargs["timeout"] for call in mock_request.call_args_list] == [5, 5]
147+
124148

125149
class TestAsyncRequestWithRetry:
126150
@pytest.mark.asyncio
@@ -234,3 +258,30 @@ def raise_for_status():
234258
assert response == success_response
235259
assert mock_request.call_count == 2
236260
mock_sleep.assert_called()
261+
262+
@pytest.mark.asyncio
263+
async def test_async_request_with_retry_custom_timeout_preserved_across_retries(self):
264+
"""A custom timeout must be honored on every attempt, not just the first one.
265+
266+
Regression test: ``timeout`` used to be popped from the shared ``kwargs`` inside the
267+
retried inner function, so after the first attempt it was gone and every subsequent
268+
retry silently fell back to the default of 10 seconds.
269+
"""
270+
with patch("asyncio.sleep", return_value=None):
271+
success_response = httpx.Response(status_code=200, request=httpx.Request("GET", "https://example.com"))
272+
273+
with patch("httpx.AsyncClient.request") as mock_request:
274+
# First attempt fails with a retryable error, second attempt succeeds.
275+
mock_request.side_effect = [
276+
httpx.RequestError("boom", request=httpx.Request("GET", "https://example.com")),
277+
success_response,
278+
]
279+
280+
response = await async_request_with_retry(
281+
method="GET", url="https://example.com", attempts=2, timeout=5
282+
)
283+
284+
assert response == success_response
285+
assert mock_request.call_count == 2
286+
# Both the initial attempt and the retry must use the caller's timeout, not the default.
287+
assert [call.kwargs["timeout"] for call in mock_request.call_args_list] == [5, 5]

0 commit comments

Comments
 (0)