Skip to content

🧹 Refactor _retry_request to improve readability#796

Open
abhimehro wants to merge 1 commit into
mainfrom
refactor-retry-request
Open

🧹 Refactor _retry_request to improve readability#796
abhimehro wants to merge 1 commit into
mainfrom
refactor-retry-request

Conversation

@abhimehro
Copy link
Copy Markdown
Owner

@abhimehro abhimehro commented May 14, 2026

🎯 What: The _retry_request function in api_client.py was too long and complex. The logic inside the try/except block was extracted into smaller helper functions (_get_error_hint, _log_debug_response_content, _handle_rate_limit, _check_client_error).
💡 Why: This improves the readability, modularity, and maintainability of api_client.py. It also resolves the "Bumpy Road Ahead" / "Brain Method" code smell by reducing cyclomatic complexity.
Verification: I ran uv tool run ruff format ., uv tool run ruff check ., uv run pytest, and uv tool run pre-commit run --all-files. All checks passed.
Result: A simplified _retry_request function, making it easier to parse and debug without altering its underlying logic.


Open in Devin Review

Copilot AI review requested due to automatic review settings May 14, 2026 13:24
@trunk-io
Copy link
Copy Markdown

trunk-io Bot commented May 14, 2026

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

Copy link
Copy Markdown

@codescene-delta-analysis codescene-delta-analysis Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Health Improved (1 files improve in Code Health)

Gates Passed
6 Quality Gates Passed

See analysis details in CodeScene

View Improvements
File Code Health Impact Categories Improved
api_client.py 7.43 → 9.10 Complex Method, Complex Conditional, Bumpy Road Ahead, Overall Code Complexity, Deep, Nested Complexity

Quality Gate Profile: Pay Down Tech Debt
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Open in Devin Review

Comment thread api_client.py
if attempt < max_retries - 1:
time.sleep(wait_seconds)
return True
raise e # Max retries exceeded
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: raise e vs bare raise changes traceback display

The extracted helpers _handle_rate_limit (line 273) and _check_client_error (line 288) use raise e instead of the original bare raise. In Python 3, bare raise inside an except block preserves the original traceback, while raise e in a called function creates a new traceback starting from the raise e site. The exception object and its __context__ chain are preserved, so the behavior is functionally identical — but stack traces in logs or crash reports will now point to _handle_rate_limit:273 or _check_client_error:288 instead of directly to the _retry_request except block. This is a cosmetic difference that could slightly complicate debugging but is not a bug.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread api_client.py
Comment on lines +261 to +275
wait_seconds: int | None = None
with contextlib.suppress(ValueError):
wait_seconds = int(retry_after)

if wait_seconds is not None:
log.warning(
f"Rate limited (429). Server requests {wait_seconds}s wait "
f"(attempt {attempt + 1}/{max_retries})"
)
if attempt < max_retries - 1:
time.sleep(wait_seconds)
return True
raise e # Max retries exceeded

return False
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: 429 with non-integer Retry-After falls through correctly

When the server sends a 429 with a Retry-After header that cannot be parsed as an integer (e.g. an HTTP-date format like Wed, 21 Oct 2015 07:28:00 GMT), _handle_rate_limit suppresses the ValueError from int(), wait_seconds stays None, and the function returns False. This causes the code to fall through to _check_client_error, which skips 429 (due to code != 429), and then to the standard exponential backoff retry. This matches the old behavior exactly. However, note that HTTP-date format Retry-After headers are never honored — the code only supports integer (seconds) format. This is a pre-existing limitation, not introduced by this PR.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread api_client.py
Comment on lines +218 to +289
def _get_error_hint(e: Exception) -> str:
"""Generate actionable error hints for logged exceptions."""
if isinstance(e, httpx.TimeoutException):
return f" | hint: {_TIMEOUT_HINT}"
if isinstance(e, httpx.ConnectError):
return f" | hint: {_CONNECT_ERROR_HINT}"
if (
isinstance(e, httpx.HTTPStatusError)
and hasattr(e, "response")
and e.response is not None
and e.response.status_code >= 500
):
return f" | hint: {_SERVER_ERROR_HINT}"
return ""


def _log_debug_response_content(e: Exception) -> None:
"""Log response content at debug level if available."""
if (
hasattr(e, "response")
and getattr(e, "response", None) is not None
and log.isEnabledFor(logging.DEBUG)
):
log.debug(f"Response content: {_sanitize_fn(e.response.text)}")


def _handle_rate_limit(
e: httpx.HTTPStatusError, attempt: int, max_retries: int
) -> bool:
"""
Handle 429 Too Many Requests by waiting if Retry-After is present.

Returns:
True if the request should be retried immediately (after sleeping).
False if no Retry-After header was found (should use default backoff).
"""
if e.response.status_code != 429:
return False

retry_after = e.response.headers.get("Retry-After")
if not retry_after:
return False

wait_seconds: int | None = None
with contextlib.suppress(ValueError):
wait_seconds = int(retry_after)

if wait_seconds is not None:
log.warning(
f"Rate limited (429). Server requests {wait_seconds}s wait "
f"(attempt {attempt + 1}/{max_retries})"
)
if attempt < max_retries - 1:
time.sleep(wait_seconds)
return True
raise e # Max retries exceeded

return False


def _check_client_error(e: httpx.HTTPStatusError) -> None:
"""Raise exception for 4xx client errors (excluding 429) without retrying."""
code = e.response.status_code
if 400 <= code < 500 and code != 429:
hint = _4XX_HINTS.get(code, "")
hint_suffix = f" | hint: {hint}" if hint else ""
log.warning(
f"API request failed with HTTP {code}{hint_suffix}: {_sanitize_fn(e)}"
)
_log_debug_response_content(e)
raise e

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Extracted helpers not added to __all__

The four new helper functions (_get_error_hint, _log_debug_response_content, _handle_rate_limit, _check_client_error) are not exported in __all__ at api_client.py:36-54. This is consistent with the existing convention — they are underscore-prefixed internal helpers not intended for use by main.py. All existing __all__ entries are either public constants or functions directly called by main.py. So this is correct.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants