From ddd6aa842397bcc465f38567542d9f1657ff727d Mon Sep 17 00:00:00 2001 From: faizahmad-khan Date: Tue, 21 Jul 2026 11:15:08 +0530 Subject: [PATCH 1/2] feat: implement retry logic for GitHub secondary rate limits and add per-file commit delays --- backend/github_service.py | 47 +++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/backend/github_service.py b/backend/github_service.py index 7458469..1988d35 100644 --- a/backend/github_service.py +++ b/backend/github_service.py @@ -4,6 +4,7 @@ """ import base64 +import time from diff_service import apply_diff from github import Github, GithubException @@ -11,6 +12,37 @@ class GitHubService: + def _execute_with_retry(self, func, *args, **kwargs): + """ + Executes a GitHub API call with retry logic for Secondary Rate Limits (Abuse limits). + Catches 403 Forbidden caused by rapid sequential requests and respects Retry-After headers. + """ + max_retries = 3 + for attempt in range(max_retries): + try: + return func(*args, **kwargs) + except GithubException as e: + is_rate_limit = False + if e.status == 403: + msg = str(e.data).lower() if e.data else "" + if "secondary rate" in msg or "abuse" in msg or "rate limit" in msg: + is_rate_limit = True + + if is_rate_limit and attempt < max_retries - 1: + retry_after = 60 + if hasattr(e, "headers") and e.headers and "retry-after" in e.headers: + try: + retry_after = int(e.headers["retry-after"]) + except ValueError: + pass + print( + f"[github_service] Secondary rate limit hit. " + f"Retrying after {retry_after}s (attempt {attempt + 1}/{max_retries})..." + ) + time.sleep(retry_after) + continue + raise + def validate_token_and_repo(self, token: str, repo_name: str) -> dict: """ Validates a PAT/OAuth token and checks access to the specified repo. @@ -145,7 +177,11 @@ def commit_files( conflict_files: list[str] = [] committed_ids: list[str] = [] - for staged in staged_files: + for idx, staged in enumerate(staged_files): + if idx > 0: + # Proactively sleep 1s between file commits per GitHub's integrator best practices + time.sleep(1) + filepath = staged["filepath"] stored_base_sha = staged["base_sha"] is_binary = staged.get("is_binary", False) @@ -168,7 +204,8 @@ def commit_files( if file_id: committed_ids.append(file_id) continue - result = repo.delete_file( + result = self._execute_with_retry( + repo.delete_file, path=filepath, message=commit_message, sha=current_sha, @@ -201,14 +238,16 @@ def commit_files( # --- Commit to GitHub --------------------------------------------------------- if not exists_on_gh: - result = repo.create_file( + result = self._execute_with_retry( + repo.create_file, path=filepath, message=commit_message, content=content_to_commit, branch=branch, ) else: - result = repo.update_file( + result = self._execute_with_retry( + repo.update_file, path=filepath, message=commit_message, content=content_to_commit, From a3c6b72e38e94f550d8623358ef23d83c3ab97a9 Mon Sep 17 00:00:00 2001 From: faizahmad-khan Date: Tue, 21 Jul 2026 11:26:20 +0530 Subject: [PATCH 2/2] fix(backend): run synchronous commit_files via run_in_threadpool in commit_direct Avoid blocking the FastAPI async event loop during sequential commit sleeps and retry delays. --- backend/routes/staged_files.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/routes/staged_files.py b/backend/routes/staged_files.py index e565a80..a1b67a4 100644 --- a/backend/routes/staged_files.py +++ b/backend/routes/staged_files.py @@ -9,6 +9,7 @@ import channel_logger from auth import require_api_key from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi.concurrency import run_in_threadpool from github_service import github_service from pydantic import BaseModel from supabase_service import ( @@ -156,7 +157,8 @@ async def commit_direct(payload: DirectCommitPayload, telegram_id: str = Depends detail="No repo detected. Save a file so GitPhone can auto-detect your repo.", ) - result = github_service.commit_files( + result = await run_in_threadpool( + github_service.commit_files, token=user["github_token"], repo_name=repo, branch=branch,