Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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