Description
retry_request in src/agentos/channels/_util.py:297 catches (httpx.ConnectError, httpx.ReadTimeout), but httpx.ConnectTimeout (DNS/TLS handshake timeout) is not a subclass of httpx.ConnectError — it inherits from httpx.TimeoutException via a completely separate hierarchy:
ConnectError → NetworkError → TransportError
ConnectTimeout → TimeoutException → TransportError
When a channel API call (Slack, Discord, Telegram, webhook) times out during DNS resolution or TLS negotiation, the exception passes through retry_request without any retry/backoff — the caller gets an unhandled crash instead of a resilient retry.
Similarly, WriteTimeout and PoolTimeout (both TimeoutException subclasses) are also not caught.
Additionally, the Retry-After header is parsed via a bare float() call (_util.py:287). Per RFC 7231 §7.1.3, Retry-After may contain an HTTP-date string (e.g. Wed, 21 Oct 2026 07:28:00 GMT) instead of delay-seconds. A non-numeric value causes a ValueError crash in the retry loop.
Steps to Reproduce
import httpx
# ConnectTimeout is NOT caught by (ConnectError, ReadTimeout)
try:
raise httpx.ConnectTimeout("dns timed out")
except (httpx.ConnectError, httpx.ReadTimeout):
print("caught") # never reached
except httpx.TimeoutException:
print("missed by retry_request") # this fires
Expected Behavior
All transient timeout types should be retried with exponential backoff, and non-numeric Retry-After values should fall back to the standard backoff delay instead of crashing.
Environment
- Python 3.12
- httpx (current pinned version)
Description
retry_requestinsrc/agentos/channels/_util.py:297catches(httpx.ConnectError, httpx.ReadTimeout), buthttpx.ConnectTimeout(DNS/TLS handshake timeout) is not a subclass ofhttpx.ConnectError— it inherits fromhttpx.TimeoutExceptionvia a completely separate hierarchy:ConnectError→NetworkError→TransportErrorConnectTimeout→TimeoutException→TransportErrorWhen a channel API call (Slack, Discord, Telegram, webhook) times out during DNS resolution or TLS negotiation, the exception passes through
retry_requestwithout any retry/backoff — the caller gets an unhandled crash instead of a resilient retry.Similarly,
WriteTimeoutandPoolTimeout(bothTimeoutExceptionsubclasses) are also not caught.Additionally, the
Retry-Afterheader is parsed via a barefloat()call (_util.py:287). Per RFC 7231 §7.1.3,Retry-Aftermay contain an HTTP-date string (e.g.Wed, 21 Oct 2026 07:28:00 GMT) instead of delay-seconds. A non-numeric value causes aValueErrorcrash in the retry loop.Steps to Reproduce
Expected Behavior
All transient timeout types should be retried with exponential backoff, and non-numeric
Retry-Aftervalues should fall back to the standard backoff delay instead of crashing.Environment