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
Next Next commit
fix(uipath-platform): retry 500 responses for GET requests
GET is idempotent, so a bare 500 can be safely retried when the request
is a GET, even though it's left non-retryable for other methods since it
may indicate a non-idempotent, non-transient server bug.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
  • Loading branch information
chandhanshanth and claude committed Aug 7, 2026
commit 449f781b57e982f97679dec018c5a7bfd35487b0
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