diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index eab210d34..b12e287e3 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.17" +version = "0.2.18" 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..fa1ce0640 100644 --- a/packages/uipath-platform/src/uipath/platform/common/retry.py +++ b/packages/uipath-platform/src/uipath/platform/common/retry.py @@ -5,6 +5,7 @@ """ import random +from http import HTTPMethod from httpx import ConnectTimeout, HTTPStatusError, Response, TimeoutException from tenacity import RetryCallState @@ -12,6 +13,7 @@ from ..errors import EnrichedException RETRYABLE_STATUS_CODES: frozenset[int] = frozenset({408, 429, 502, 503, 504, 524}) +RETRYABLE_STATUS_CODES_ON_GET_ONLY: frozenset[int] = frozenset({500}) NON_RETRYABLE_STATUS_CODES: frozenset[int] = frozenset({400, 401, 403, 404, 413, 422}) @@ -70,7 +72,13 @@ def is_retryable_platform_exception(exception: BaseException) -> bool: if isinstance(exception, (ConnectTimeout, TimeoutException)): return True if isinstance(exception, EnrichedException): - return exception.status_code in RETRYABLE_STATUS_CODES + if exception.status_code in RETRYABLE_STATUS_CODES: + return True + if ( + exception.status_code in RETRYABLE_STATUS_CODES_ON_GET_ONLY + and exception.http_method.upper() == HTTPMethod.GET + ): + return True return False diff --git a/packages/uipath-platform/tests/services/test_retry.py b/packages/uipath-platform/tests/services/test_retry.py index ab893f042..7f9e00c99 100644 --- a/packages/uipath-platform/tests/services/test_retry.py +++ b/packages/uipath-platform/tests/services/test_retry.py @@ -1,3 +1,5 @@ +from http import HTTPMethod + import httpx from tenacity import Future, RetryCallState, Retrying @@ -116,7 +118,9 @@ def test_negative_retry_after_ignored(self): def _make_http_status_error( - status_code: int, retry_after: str | None = None + status_code: int, + retry_after: str | None = None, + method: HTTPMethod = HTTPMethod.GET, ) -> httpx.HTTPStatusError: headers = {} if retry_after is not None: @@ -124,7 +128,7 @@ def _make_http_status_error( response = httpx.Response( status_code=status_code, headers=headers, - request=httpx.Request("GET", "https://example.com"), + request=httpx.Request(method, "https://example.com"), ) return httpx.HTTPStatusError( message=f"{status_code}", request=response.request, response=response @@ -184,11 +188,24 @@ def test_enriched_400_not_retryable(self): err = EnrichedException(http_err) assert is_retryable_platform_exception(err) is False - def test_enriched_500_not_retryable(self): - http_err = _make_http_status_error(500) + def test_enriched_500_post_not_retryable(self): + http_err = _make_http_status_error(500, method=HTTPMethod.POST) err = EnrichedException(http_err) assert is_retryable_platform_exception(err) is False + def test_enriched_500_get_retryable(self): + http_err = _make_http_status_error(500, method=HTTPMethod.GET) + err = EnrichedException(http_err) + assert is_retryable_platform_exception(err) is True + + def test_enriched_500_get_lowercase_retryable(self): + # httpx.Request normalizes method casing itself, so set http_method + # directly to exercise our own case-insensitive comparison. + http_err = _make_http_status_error(500, method=HTTPMethod.GET) + err = EnrichedException(http_err) + err.http_method = "get" + assert is_retryable_platform_exception(err) is True + def test_raw_http_error_not_matched(self): err = _make_http_status_error(429) assert is_retryable_platform_exception(err) is False