Add RateLimitError and handle rate limiting in GitLab and GitHub services (#8003)

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
This commit is contained in:
Rohit Malhotra 2025-04-22 12:30:41 -04:00 committed by GitHub
parent 8f3ff1210e
commit 039fe295a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 57 additions and 39 deletions

View File

@ -5,9 +5,7 @@ from typing import Any
import httpx
from pydantic import SecretStr
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.service_types import (
AuthenticationError,
BaseGitService,
GitService,
ProviderType,
@ -45,6 +43,10 @@ class GitHubService(BaseGitService, GitService):
if base_domain:
self.BASE_URL = f'https://{base_domain}/api/v3'
@property
def provider(self) -> str:
return ProviderType.GITHUB.value
async def _get_github_headers(self) -> dict:
"""Retrieve the GH Token from settings store to construct the headers."""
if not self.token:
@ -100,15 +102,9 @@ class GitHubService(BaseGitService, GitService):
return response.json(), headers
except httpx.HTTPStatusError as e:
if e.response.status_code == 401:
raise AuthenticationError('Invalid Github token')
logger.warning(f'Status error on GH API: {e}')
raise UnknownException('Unknown error')
raise self.handle_http_status_error(e)
except httpx.HTTPError as e:
logger.warning(f'HTTP error on GH API: {e}')
raise UnknownException('Unknown error')
raise self.handle_http_error(e)
async def get_user(self) -> User:
url = f'{self.BASE_URL}/user'
@ -264,15 +260,9 @@ class GitHubService(BaseGitService, GitService):
return dict(result)
except httpx.HTTPStatusError as e:
if e.response.status_code == 401:
raise AuthenticationError('Invalid Github token')
logger.warning(f'Status error on GH API: {e}')
raise UnknownException('Unknown error')
raise self.handle_http_status_error(e)
except httpx.HTTPError as e:
logger.warning(f'HTTP error on GH API: {e}')
raise UnknownException('Unknown error')
raise self.handle_http_error(e)
async def get_suggested_tasks(self) -> list[SuggestedTask]:
"""Get suggested tasks for the authenticated user across all repositories.

View File

@ -4,9 +4,7 @@ from typing import Any
import httpx
from pydantic import SecretStr
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.service_types import (
AuthenticationError,
BaseGitService,
GitService,
ProviderType,
@ -24,6 +22,7 @@ class GitLabService(BaseGitService, GitService):
GRAPHQL_URL = 'https://gitlab.com/api/graphql'
token: SecretStr = SecretStr('')
refresh = False
def __init__(
self,
@ -44,6 +43,10 @@ class GitLabService(BaseGitService, GitService):
self.BASE_URL = f'https://{base_domain}/api/v4'
self.GRAPHQL_URL = f'https://{base_domain}/api/graphql'
@property
def provider(self) -> str:
return ProviderType.GITLAB.value
async def _get_gitlab_headers(self) -> dict[str, Any]:
"""
Retrieve the GitLab Token to construct the headers
@ -100,15 +103,9 @@ class GitLabService(BaseGitService, GitService):
return response.json(), headers
except httpx.HTTPStatusError as e:
if e.response.status_code == 401:
raise AuthenticationError('Invalid GitLab token')
logger.warning(f'Status error on GL API: {e}')
raise UnknownException('Unknown error')
raise self.handle_http_status_error(e)
except httpx.HTTPError as e:
logger.warning(f'HTTP error on GL API: {e}')
raise UnknownException('Unknown error')
raise self.handle_http_error(e)
async def execute_graphql_query(self, query: str, variables: dict[str, Any]) -> Any:
"""
@ -156,15 +153,9 @@ class GitLabService(BaseGitService, GitService):
return result.get('data')
except httpx.HTTPStatusError as e:
if e.response.status_code == 401:
raise AuthenticationError('Invalid GitLab token')
logger.warning(f'Status error on GL API: {e}')
raise UnknownException('Unknown error')
raise self.handle_http_status_error(e)
except httpx.HTTPError as e:
logger.warning(f'HTTP error on GL API: {e}')
raise UnknownException('Unknown error')
raise self.handle_http_error(e)
async def get_user(self) -> User:
url = f'{self.BASE_URL}/user'

View File

@ -1,9 +1,11 @@
from abc import ABC, abstractmethod
from enum import Enum
from typing import Protocol
from typing import Any, Protocol
from httpx import AsyncClient
from httpx import AsyncClient, HTTPError, HTTPStatusError
from pydantic import BaseModel, SecretStr
from openhands.core.logger import openhands_logger as logger
from openhands.server.types import AppMode
@ -58,12 +60,31 @@ class UnknownException(ValueError):
pass
class RateLimitError(ValueError):
"""Raised when the git provider's API rate limits are exceeded."""
pass
class RequestMethod(Enum):
POST = 'post'
GET = 'get'
class BaseGitService:
class BaseGitService(ABC):
@property
def provider(self) -> str:
raise NotImplementedError('Subclasses must implement the provider property')
# Method used to satisfy mypy for abstract class definition
@abstractmethod
async def _make_request(
self,
url: str,
params: dict | None = None,
method: RequestMethod = RequestMethod.GET,
) -> tuple[Any, dict]: ...
async def execute_request(
self,
client: AsyncClient,
@ -76,6 +97,22 @@ class BaseGitService:
return await client.post(url, headers=headers, json=params)
return await client.get(url, headers=headers, params=params)
def handle_http_status_error(
self, e: HTTPStatusError
) -> AuthenticationError | RateLimitError | UnknownException:
if e.response.status_code == 401:
return AuthenticationError(f'Invalid {self.provider} token')
elif e.response.status_code == 429:
logger.warning(f'Rate limit exceeded on {self.provider} API: {e}')
return RateLimitError('GitHub API rate limit exceeded')
logger.warning(f'Status error on {self.provider} API: {e}')
return UnknownException('Unknown error')
def handle_http_error(self, e: HTTPError) -> UnknownException:
logger.warning(f'HTTP error on {self.provider} API: {e}')
return UnknownException('Unknown error')
class GitService(Protocol):
"""Protocol defining the interface for Git service providers"""