From cccb41d361ed3f3d3101ad13929922cb3142fa50 Mon Sep 17 00:00:00 2001 From: Thomas Lammers Date: Thu, 8 Oct 2026 11:29:49 +0200 Subject: [PATCH 1/2] fix(platform): retry requests that fail to connect A connection reset during the TLS handshake raised httpx.ConnectError, which the platform retry did not cover, so one dropped connection failed the whole call. A failed connect never sends the request, so it is safe to retry for any method. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019eqtRNhyPtxwyQUvKoK5Zy --- packages/uipath-platform/pyproject.toml | 2 +- .../src/uipath/platform/common/retry.py | 5 +- .../tests/services/test_base_service.py | 55 +++++++++++++++++++ .../tests/services/test_retry.py | 8 +++ packages/uipath-platform/uv.lock | 2 +- 5 files changed, 68 insertions(+), 4 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 091bd2284..be322ea1e 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.34" +version = "0.2.35" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/common/retry.py b/packages/uipath-platform/src/uipath/platform/common/retry.py index ff4b5a064..d229dd20a 100644 --- a/packages/uipath-platform/src/uipath/platform/common/retry.py +++ b/packages/uipath-platform/src/uipath/platform/common/retry.py @@ -6,7 +6,7 @@ import random -from httpx import ConnectTimeout, HTTPStatusError, Response, TimeoutException +from httpx import ConnectError, HTTPStatusError, Response, TimeoutException from tenacity import RetryCallState from ..errors import EnrichedException @@ -67,7 +67,8 @@ def extract_retry_after_from_chain(exception: BaseException) -> float | None: def is_retryable_platform_exception(exception: BaseException) -> bool: """Return True if the exception is transient and should be retried.""" - if isinstance(exception, (ConnectTimeout, TimeoutException)): + # A failed connect never sent the request, so any method is safe to retry. + if isinstance(exception, (ConnectError, TimeoutException)): return True if isinstance(exception, EnrichedException): return exception.status_code in RETRYABLE_STATUS_CODES diff --git a/packages/uipath-platform/tests/services/test_base_service.py b/packages/uipath-platform/tests/services/test_base_service.py index db39ed313..04c854d03 100644 --- a/packages/uipath-platform/tests/services/test_base_service.py +++ b/packages/uipath-platform/tests/services/test_base_service.py @@ -1,3 +1,4 @@ +import httpx import pytest from pytest_httpx import HTTPXMock @@ -232,6 +233,41 @@ def test_502_retried( assert response.status_code == 200 assert len(httpx_mock.get_requests()) == 2 + def test_connect_error_retried( + self, + httpx_mock: HTTPXMock, + service: BaseService, + base_url: str, + org: str, + tenant: str, + ): + url = self._url(base_url, org, tenant) + httpx_mock.add_exception( + httpx.ConnectError("[Errno 104] Connection reset by peer"), url=url + ) + httpx_mock.add_response(url=url, status_code=200, json={"ok": True}) + + response = service.request("POST", "/endpoint") + assert response.status_code == 200 + assert len(httpx_mock.get_requests()) == 2 + + def test_read_error_not_retried( + self, + httpx_mock: HTTPXMock, + service: BaseService, + base_url: str, + org: str, + tenant: str, + ): + url = self._url(base_url, org, tenant) + httpx_mock.add_exception( + httpx.ReadError("[Errno 104] Connection reset by peer"), url=url + ) + + with pytest.raises(httpx.ReadError): + service.request("POST", "/endpoint") + assert len(httpx_mock.get_requests()) == 1 + @pytest.mark.anyio async def test_429_retried_async( self, @@ -266,6 +302,25 @@ async def test_503_retried_async( assert response.status_code == 200 assert len(httpx_mock.get_requests()) == 2 + @pytest.mark.anyio + async def test_connect_error_retried_async( + self, + httpx_mock: HTTPXMock, + service: BaseService, + base_url: str, + org: str, + tenant: str, + ): + url = self._url(base_url, org, tenant) + httpx_mock.add_exception( + httpx.ConnectError("[Errno 104] Connection reset by peer"), url=url + ) + httpx_mock.add_response(url=url, status_code=200, json={"ok": True}) + + response = await service.request_async("POST", "/endpoint") + assert response.status_code == 200 + assert len(httpx_mock.get_requests()) == 2 + @pytest.mark.anyio async def test_400_not_retried_async( self, diff --git a/packages/uipath-platform/tests/services/test_retry.py b/packages/uipath-platform/tests/services/test_retry.py index ab893f042..86fd74093 100644 --- a/packages/uipath-platform/tests/services/test_retry.py +++ b/packages/uipath-platform/tests/services/test_retry.py @@ -149,6 +149,14 @@ def test_timeout_exception(self): err = httpx.TimeoutException("timed out") assert is_retryable_platform_exception(err) is True + def test_connect_error(self): + err = httpx.ConnectError("[Errno 104] Connection reset by peer") + assert is_retryable_platform_exception(err) is True + + def test_read_error_not_retryable(self): + err = httpx.ReadError("[Errno 104] Connection reset by peer") + assert is_retryable_platform_exception(err) is False + def test_enriched_408(self): http_err = _make_http_status_error(408) err = EnrichedException(http_err) diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 8d7cccdcc..b02fd2c40 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.34" +version = "0.2.35" source = { editable = "." } dependencies = [ { name = "anyio" }, From 16a613cb9f26adcbb96e9be35ec10a9abb9e6a81 Mon Sep 17 00:00:00 2001 From: Thomas Lammers Date: Thu, 8 Oct 2026 11:59:54 +0200 Subject: [PATCH 2/2] chore(uipath): lock uipath-platform 0.2.35 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019eqtRNhyPtxwyQUvKoK5Zy --- packages/uipath/uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index eb6d18ede..9887fc03a 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2762,7 +2762,7 @@ wheels = [ [[package]] name = "uipath-platform" -version = "0.2.34" +version = "0.2.35" source = { editable = "../uipath-platform" } dependencies = [ { name = "anyio" },