Skip to content

Commit 7a45b77

Browse files
fix(http): retry transient gateway status failures (#199)
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> Co-authored-by: n-papaioannou <n.papaioannou@ime.life>
1 parent 2deef95 commit 7a45b77

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

‎ifixai/providers/http.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88

99
from ifixai.core.types import ChatMessage, ProviderConfig, RetrievedSource
1010
from ifixai.providers.base import (
11+
RETRYABLE_HTTP_STATUS_CODES,
1112
ChatProvider,
1213
ProviderAuthError,
1314
ProviderConnectionError,
15+
ProviderOverloadedError,
1416
ProviderRateLimitError,
1517
ProviderResponseError,
1618
ProviderTimeoutError,
@@ -137,7 +139,7 @@ async def send_message(
137139
await asyncio.sleep(2**attempt)
138140
continue
139141
raise
140-
except (ProviderConnectionError, ProviderTimeoutError) as exc:
142+
except (ProviderConnectionError, ProviderTimeoutError, ProviderOverloadedError) as exc:
141143
last_error = exc
142144
if attempt < config.max_retries:
143145
await asyncio.sleep(2**attempt)
@@ -174,6 +176,13 @@ async def _send_request(
174176
endpoint=endpoint,
175177
details="HTTP 429: rate limited",
176178
)
179+
if resp.status in RETRYABLE_HTTP_STATUS_CODES:
180+
body = await resp.text()
181+
raise ProviderOverloadedError(
182+
provider="http",
183+
endpoint=endpoint,
184+
details=f"HTTP {resp.status}: {scrub_secrets(body[:500])}",
185+
)
177186
if resp.status >= 400:
178187
body = await resp.text()
179188
raise ProviderResponseError(

‎ifixai/tests/providers/test_http_response_shapes.py‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,48 @@ async def retrieve(_request):
6262
finally:
6363
await provider.aclose()
6464
await runner.cleanup()
65+
66+
67+
@pytest.mark.asyncio
68+
@pytest.mark.parametrize("status", [408, 500, 502, 503, 504, 400])
69+
async def test_http_gateway_failures_follow_retry_contract(monkeypatch, status):
70+
from ifixai.providers.base import ProviderOverloadedError, is_fatal_provider_error
71+
72+
seen = []
73+
async def chat(request):
74+
seen.append(await request.json())
75+
if len(seen) == 1:
76+
return aiohttp.web.json_response({"error": {"message": "owned gateway unavailable"}}, status=status)
77+
return aiohttp.web.json_response({"choices": [{"message": {"content": "recovered reply"}}]})
78+
79+
async def no_delay(_seconds):
80+
pass
81+
82+
monkeypatch.setattr("ifixai.providers.http.asyncio.sleep", no_delay)
83+
app = aiohttp.web.Application()
84+
app.router.add_post("/v1/chat/completions", chat)
85+
runner = aiohttp.web.AppRunner(app)
86+
await runner.setup()
87+
site = aiohttp.web.TCPSite(runner, "127.0.0.1", 0)
88+
await site.start()
89+
url = f"http://127.0.0.1:{site._server.sockets[0].getsockname()[1]}/v1"
90+
provider = HttpProvider()
91+
try:
92+
config = ProviderConfig(provider="http", endpoint=url, max_retries=1)
93+
if status == 400:
94+
with pytest.raises(ProviderResponseError):
95+
await provider.send_message([ChatMessage(content="hello")], config)
96+
assert len(seen) == 1
97+
else:
98+
assert await provider.send_message([ChatMessage(content="hello")], config) == "recovered reply"
99+
assert len(seen) == 2
100+
seen.clear()
101+
config.max_retries = 0
102+
expected = ProviderResponseError if status == 400 else ProviderOverloadedError
103+
with pytest.raises(expected) as caught:
104+
await provider.send_message([ChatMessage(content="hello")], config)
105+
assert not is_fatal_provider_error(caught.value)
106+
assert len(seen) == 1
107+
finally:
108+
await provider.aclose()
109+
await runner.cleanup()

0 commit comments

Comments
 (0)