Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/uipath-platform/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
10 changes: 9 additions & 1 deletion packages/uipath-platform/src/uipath/platform/common/retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
"""

import random
from http import HTTPMethod

from httpx import ConnectTimeout, HTTPStatusError, Response, TimeoutException
from tenacity import RetryCallState

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})


Expand Down Expand Up @@ -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


Expand Down
25 changes: 21 additions & 4 deletions packages/uipath-platform/tests/services/test_retry.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from http import HTTPMethod

import httpx
from tenacity import Future, RetryCallState, Retrying

Expand Down Expand Up @@ -116,15 +118,17 @@ 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:
headers["retry-after"] = retry_after
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
Expand Down Expand Up @@ -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
Expand Down
Loading