Skip to content

Commit 04d997d

Browse files
fix(client): map a connect timeout to ConnectTimeout, not Timeout
requests.ConnectTimeout is both a ConnectionError and a Timeout. Mapping httpx.ConnectTimeout to a plain Timeout — which is all httpx's own hierarchy implies — stops every caller that catches the connection family from catching a connect timeout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014f9oEEYspPH4fmPULTnLkJ
1 parent c1ab0af commit 04d997d

2 files changed

Lines changed: 28 additions & 6 deletions

File tree

src/unstract/api_deployments/client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
# `requests` remains a dependency for its exception classes. Downstream code
2020
# catches ConnectionError and Timeout by name around these calls, and the httpx
2121
# equivalents are not subclasses, so they are translated at the transport seam.
22-
from requests.exceptions import ConnectionError, Timeout
22+
from requests.exceptions import ConnectionError, ConnectTimeout, Timeout
2323
from tenacity import (
2424
RetryCallState,
2525
Retrying,
@@ -47,6 +47,10 @@ def _translate_transport_errors(fn, *args, **kwargs):
4747
"""
4848
try:
4949
return fn(*args, **kwargs)
50+
except httpx.ConnectTimeout as e:
51+
# ConnectTimeout is both a ConnectionError and a Timeout; the plain
52+
# Timeout httpx implies would stop matching half the callers.
53+
raise ConnectTimeout(str(e)) from e
5054
except httpx.TimeoutException as e:
5155
raise Timeout(str(e)) from e
5256
except httpx.ConnectError as e:

tests/test_compat.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import httpx
2020
import pytest
2121
import requests
22-
from requests.exceptions import ConnectionError, Timeout
22+
from requests.exceptions import ConnectionError, ConnectTimeout, Timeout
2323

2424
from unstract.api_deployments.client import (
2525
_EXECUTE_SEND_ONLY,
@@ -103,7 +103,7 @@ def _requests_response(status_code=200, json_data=None, text=None):
103103
@pytest.mark.parametrize(
104104
("raised", "expected"),
105105
[
106-
(httpx.ConnectTimeout("connect timed out"), Timeout),
106+
(httpx.ConnectTimeout("connect timed out"), ConnectTimeout),
107107
(httpx.ReadTimeout("read timed out"), Timeout),
108108
(httpx.WriteTimeout("write timed out"), Timeout),
109109
(httpx.PoolTimeout("pool timed out"), Timeout),
@@ -117,9 +117,9 @@ def _requests_response(status_code=200, json_data=None, text=None):
117117
def test_transport_errors_are_translated(raised, expected):
118118
"""Callers catch the ``requests`` classes; httpx's are not subclasses.
119119
120-
``ConnectTimeout`` is the case that makes ordering load-bearing: it is a
121-
timeout, not a ``ConnectError``, and matching on connection first would
122-
mislabel it.
120+
``ConnectTimeout`` is the case that makes ordering load-bearing, and it is
121+
also both a ``ConnectionError`` and a ``Timeout`` — the plain ``Timeout``
122+
that httpx's hierarchy implies would stop matching half the callers.
123123
"""
124124
client = _client()
125125
with patch.object(
@@ -129,6 +129,24 @@ def test_transport_errors_are_translated(raised, expected):
129129
client._send("get", "/anything")
130130

131131

132+
def test_a_connect_timeout_is_still_a_connection_error():
133+
client = _client()
134+
with patch.object(
135+
client._transport.get_httpx_client(),
136+
"request",
137+
side_effect=httpx.ConnectTimeout("connect timed out"),
138+
):
139+
with pytest.raises(ConnectionError):
140+
client._send("get", "/anything")
141+
with patch.object(
142+
client._transport.get_httpx_client(),
143+
"request",
144+
side_effect=httpx.ConnectTimeout("connect timed out"),
145+
):
146+
with pytest.raises(Timeout):
147+
client._send("get", "/anything")
148+
149+
132150
def test_translated_errors_keep_the_original_cause():
133151
client = _client()
134152
original = httpx.ConnectError("refused")

0 commit comments

Comments
 (0)