Skip to content

Add retry and rate-limit handling to GitHubClient#15

Merged
Keramikus-97 merged 2 commits into
mainfrom
devin/1780993838-github-retry-rate-limit
Jun 9, 2026
Merged

Add retry and rate-limit handling to GitHubClient#15
Keramikus-97 merged 2 commits into
mainfrom
devin/1780993838-github-retry-rate-limit

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

GitHubClient now transparently retries transient failures and honors GitHub's rate-limit signals, instead of raising on the first non-2xx/transport error. This makes the client resilient to the 429/403 rate limits and 5xx/network blips that are common against the GitHub API.

_request gained a retry loop:

retry_transient = method.upper() in IDEMPOTENT_METHODS  # GET/HEAD/OPTIONS/PUT/DELETE
while True:
    try:
        resp = await self._client.request(...)
    except httpx.TransportError:
        retry-with-backoff if retry_transient & attempts left, else re-raise
    if self._is_rate_limited(resp):      # 429, or 403 + (X-RateLimit-Remaining: 0 | Retry-After)
        retry-with-delay (any method) or raise RateLimitError when exhausted
    if resp.status_code in {500,502,503,504}:
        retry-with-backoff if retry_transient & attempts left, else raise GitHubAPIError
    # other 4xx -> raise immediately (no retry); 204 -> None; else resp.json()

Idempotency: transient errors (5xx/transport) are retried only for idempotent methods so a non-idempotent POST (e.g. create_issue_comment) is never duplicated. Rate-limit responses are retried for all methods, since a rate-limited request was rejected before taking effect.

Retry delay precedence: Retry-After header → X-RateLimit-Reset (when X-RateLimit-Remaining == 0) → exponential backoff with full jitter, all capped at max_backoff.

New/changed interfaces:

  • RateLimitError(GitHubAPIError) carrying parsed retry_after / reset_at (or None when headers are absent/unparseable). Exported from the package.
  • GitHubClient(..., max_retries=3, backoff_factor=0.5, max_backoff=60.0, sleep=None). sleep is injectable so tests run instantly and deterministically (defaults to asyncio.sleep).
  • GitHubClient.from_config(config, sleep=None) builds a client from Config.
  • Config gains max_retries / backoff_factor, read from OPENCODE_MAX_RETRIES / OPENCODE_BACKOFF_FACTOR with safe fallbacks/clamping.

Behavior note: a 403 without a rate-limit signal is treated as a normal error (raised immediately, not retried), so permission errors are not masked.

Testing

  • ruff check . clean, pyright src clean.
  • pytest — 143 passed; total coverage 98.8% (github_client.py 99%).
  • New tests cover: success-first-try (no sleep), 5xx retry-then-success, 5xx/transport exhaustion, 429 retry + RateLimitError on exhaustion, Retry-After honored, X-RateLimit-Reset-driven delay, 403-rate-limit vs non-rate-limit 403, backoff cap, max_retries=0, invalid-header fallbacks, 204 → None, and idempotency (POST not retried on 5xx/transport, POST retried on 429).
  • Existing github_client tests updated only to inject the no-op sleep via the client fixture (assertions unchanged).

Link to Devin session: https://app.devin.ai/sessions/bee4c1ccb6884995a794d0e8e454ed27
Requested by: @Keramikus-97

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

User devin-ai-integration[bot] does not have write permissions

github run

@Keramikus-97 Keramikus-97 marked this pull request as ready for review June 9, 2026 12:16
@Keramikus-97 Keramikus-97 merged commit 7ed83e8 into main Jun 9, 2026
3 checks passed
@Keramikus-97 Keramikus-97 deleted the devin/1780993838-github-retry-rate-limit branch June 9, 2026 13:53
@Keramikus-97 Keramikus-97 restored the devin/1780993838-github-retry-rate-limit branch June 9, 2026 13:55
@Keramikus-97 Keramikus-97 deleted the devin/1780993838-github-retry-rate-limit branch June 9, 2026 13:55
@Keramikus-97 Keramikus-97 restored the devin/1780993838-github-retry-rate-limit branch June 9, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant