Duplicate check
AgentOS version or commit
2026.9.1
Area
Channel integration
Reproduction steps
Define a mock endpoint or handler that returns HTTP 429 with Retry-After:
python
import asyncio, httpx
from agentos.channels._util import retry_request
async def mock_endpoint(*args, **kwargs):
return httpx.Response(429, headers={"Retry-After": "0.1"}, json={"error": "rate_limited"})
async def main():
resp = await retry_request(mock_endpoint, max_retries=1, base_delay=0.1)
asyncio.run(main())
Observe the exception raised when retries are exhausted.
Alternatively, supply an RFC 7231 / RFC 9110 compliant HTTP-date header:
python
headers = {"Retry-After": "Wed, 21 Oct 2026 07:28:00 GMT"}
Expected behavior
When retries are exhausted on a 429 response, retry_request should return the httpx.Response object (matching how 5xx statuses behave) so callers (Slack, Discord, Webhook delivery) can access the status code, response headers, and downstream error body.
Retry-After should safely parse both integer/float seconds and HTTP-date strings without throwing an unhandled ValueError.
Actual behavior
On the final retry attempt (attempt == max_retries), the 429 branch executes await asyncio.sleep(retry_after); continue without checking attempt < max_retries. The loop terminates and falls through to raise last_exc or RuntimeError("retry_request exhausted"). Because last_exc is None, a bare RuntimeError is raised and the response is permanently discarded.
If a server responds with an HTTP-date format in Retry-After, float(resp.headers.get("Retry-After")) raises an unhandled ValueError: could not convert string to float: 'Wed, 21 Oct 2026 07:28:00 GMT', crashing immediately on attempt 0.
Environment
Windows 10 / Linux / macOS, Python 3.12+ (tested on Python 3.14.3 with uv)
Duplicate check
AgentOS version or commit
2026.9.1
Area
Channel integration
Reproduction steps
Define a mock endpoint or handler that returns HTTP 429 with Retry-After:
python
import asyncio, httpx
from agentos.channels._util import retry_request
async def mock_endpoint(*args, **kwargs):
return httpx.Response(429, headers={"Retry-After": "0.1"}, json={"error": "rate_limited"})
async def main():
resp = await retry_request(mock_endpoint, max_retries=1, base_delay=0.1)
asyncio.run(main())
Observe the exception raised when retries are exhausted.
Alternatively, supply an RFC 7231 / RFC 9110 compliant HTTP-date header:
python
headers = {"Retry-After": "Wed, 21 Oct 2026 07:28:00 GMT"}
Expected behavior
When retries are exhausted on a 429 response, retry_request should return the httpx.Response object (matching how 5xx statuses behave) so callers (Slack, Discord, Webhook delivery) can access the status code, response headers, and downstream error body.
Retry-After should safely parse both integer/float seconds and HTTP-date strings without throwing an unhandled ValueError.
Actual behavior
On the final retry attempt (attempt == max_retries), the 429 branch executes await asyncio.sleep(retry_after); continue without checking attempt < max_retries. The loop terminates and falls through to raise last_exc or RuntimeError("retry_request exhausted"). Because last_exc is None, a bare RuntimeError is raised and the response is permanently discarded.
If a server responds with an HTTP-date format in Retry-After, float(resp.headers.get("Retry-After")) raises an unhandled ValueError: could not convert string to float: 'Wed, 21 Oct 2026 07:28:00 GMT', crashing immediately on attempt 0.
Environment
Windows 10 / Linux / macOS, Python 3.12+ (tested on Python 3.14.3 with uv)