Skip to content

Commit e1bd626

Browse files
fix: add default HTTP request timeout to prevent indefinite hangs
Add default timeout of (30s connect, 300s read) to HttpClient.send_request(). When no explicit timeout is provided in request_kwargs, the default is injected before sending the request. This prevents requests.Session.send() from blocking indefinitely when a server stalls mid-response (e.g. after a 500 error retry). ConnectTimeout and ReadTimeout are already in TRANSIENT_EXCEPTIONS, so timeouts trigger automatic retries with exponential backoff. Co-Authored-By: alfredo.garcia@airbyte.io <freddy.garcia7.fg@gmail.com>
1 parent b183f80 commit e1bd626

2 files changed

Lines changed: 59 additions & 2 deletions

File tree

airbyte_cdk/sources/streams/http/http_client.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ def monkey_patched_get_item(self, key): # type: ignore # this interface is a co
8585
class HttpClient:
8686
_DEFAULT_MAX_RETRY: int = 5
8787
_DEFAULT_MAX_TIME: int = 60 * 10
88+
_DEFAULT_CONNECT_TIMEOUT: int = 30
89+
_DEFAULT_READ_TIMEOUT: int = 300
8890
_ACTIONS_TO_RETRY_ON = {
8991
ResponseAction.RETRY,
9092
ResponseAction.RATE_LIMITED,
@@ -586,11 +588,17 @@ def send_request(
586588
verify=request_kwargs.get("verify"),
587589
cert=request_kwargs.get("cert"),
588590
)
589-
request_kwargs = {**request_kwargs, **env_settings}
591+
mutable_request_kwargs: Dict[str, Any] = {**request_kwargs, **env_settings}
592+
593+
if "timeout" not in mutable_request_kwargs:
594+
mutable_request_kwargs["timeout"] = (
595+
self._DEFAULT_CONNECT_TIMEOUT,
596+
self._DEFAULT_READ_TIMEOUT,
597+
)
590598

591599
response: requests.Response = self._send_with_retry(
592600
request=request,
593-
request_kwargs=request_kwargs,
601+
request_kwargs=mutable_request_kwargs,
594602
log_formatter=log_formatter,
595603
exit_on_rate_limit=exit_on_rate_limit,
596604
)

unit_tests/sources/streams/http/test_http_client.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,3 +1059,52 @@ def update_response(*args, **kwargs):
10591059
assert mock_authenticator.access_token == "new_refreshed_token"
10601060
assert returned_response == valid_response
10611061
assert call_count == 2
1062+
1063+
1064+
def test_send_request_applies_default_timeout_when_not_provided(mocker):
1065+
http_client = test_http_client()
1066+
mocked_response = MagicMock(spec=requests.Response)
1067+
mocked_response.status_code = 200
1068+
mocked_response.headers = {}
1069+
mock_send = mocker.patch.object(requests.Session, "send", return_value=mocked_response)
1070+
1071+
http_client.send_request(
1072+
http_method="get",
1073+
url="https://test_base_url.com/v1/endpoint",
1074+
request_kwargs={},
1075+
)
1076+
1077+
assert mock_send.call_count == 1
1078+
call_kwargs = mock_send.call_args
1079+
# The timeout should be passed as part of the keyword arguments to session.send()
1080+
# session.send(request, **request_kwargs) unpacks request_kwargs, so timeout appears as a kwarg
1081+
assert call_kwargs.kwargs.get("timeout") == (
1082+
HttpClient._DEFAULT_CONNECT_TIMEOUT,
1083+
HttpClient._DEFAULT_READ_TIMEOUT,
1084+
) or call_kwargs[1].get("timeout") == (
1085+
HttpClient._DEFAULT_CONNECT_TIMEOUT,
1086+
HttpClient._DEFAULT_READ_TIMEOUT,
1087+
)
1088+
1089+
1090+
def test_send_request_respects_explicit_timeout(mocker):
1091+
http_client = test_http_client()
1092+
mocked_response = MagicMock(spec=requests.Response)
1093+
mocked_response.status_code = 200
1094+
mocked_response.headers = {}
1095+
mock_send = mocker.patch.object(requests.Session, "send", return_value=mocked_response)
1096+
1097+
custom_timeout = (10, 60)
1098+
http_client.send_request(
1099+
http_method="get",
1100+
url="https://test_base_url.com/v1/endpoint",
1101+
request_kwargs={"timeout": custom_timeout},
1102+
)
1103+
1104+
assert mock_send.call_count == 1
1105+
call_kwargs = mock_send.call_args
1106+
# The explicit timeout should be preserved, not overridden by the default
1107+
assert (
1108+
call_kwargs.kwargs.get("timeout") == custom_timeout
1109+
or call_kwargs[1].get("timeout") == custom_timeout
1110+
)

0 commit comments

Comments
 (0)