[Python] Zoho CRM: Getting "too many requests" even when caching access token until expiry

Discussão em 'Python' iniciado por Stack, Abril 27, 2025.

  1. Stack

    Stack Membro Participativo

    I’m integrating Zoho CRM into my FastAPI backend and handling OAuth2 tokens like this:

    1. I use a refresh_token to generate an access_token.
    2. I cache the access_token and only request a new one when it’s close to expiring (around 60 seconds before actual expiry).
    3. I'm using httpx.AsyncClient for async HTTP calls and retry with exponential backoff.

    Here's a simplified version of my code:

    class ZohoCRMClient:
    _access_token = None
    _token_expiry = None

    def __init__(self):
    self.crm_base_url = settings.CRM_BASE_URL
    self.org_id = settings.ORG_ID
    self.client_id = settings.CLIENT_ID
    self.client_secret = settings.CLIENT_SECRET
    self.refresh_token = settings.REFRESH_TOKEN
    self.token_url = settings.TOKEN_URL
    self.client = httpx.AsyncClient()

    async def get_access_token(self, retries: int = 10, backoff_factor: float = 1.5):
    # Reuse valid token
    if (
    self._access_token
    and self._token_expiry
    and self._token_expiry > datetime.utcnow()
    ):
    logger.info("Using cached Zoho access token")
    return self._access_token

    # Retry loop with backoff
    for attempt in range(retries):
    try:
    payload = {
    "grant_type": "refresh_token",
    "client_id": self.client_id,
    "client_secret": self.client_secret,
    "refresh_token": self.refresh_token
    }
    response = await self.client.post(self.token_url, data=payload)

    if response.status_code == 200:
    data = response.json()
    if "access_token" in data:
    self._access_token = data["access_token"]
    expires_in = int(data.get("expires_in", 3600))
    self._token_expiry = datetime.utcnow() + timedelta(seconds=expires_in - 60)
    logger.info(
    f"Zoho access token retrieved successfully, expires at {self._token_expiry}"
    )
    return self._access_token
    else:
    logger.error(f"Missing access token in response: {data}")
    raise HTTPException(status_code=500, detail="Access token not found in response")

    elif response.status_code == 429:
    logger.warning(f"[Rate Limit] Zoho token request rate-limited (attempt {attempt + 1})")

    else:
    logger.error(f"Token error {response.status_code}: {response.text}")
    raise HTTPException(status_code=response.status_code, detail=f"Token error: {response.text}")

    except Exception as e:
    logger.warning(f"Attempt {attempt + 1} failed to fetch token: {str(e)}")

    await asyncio.sleep(backoff_factor * (2 ** attempt)) # Exponential backoff

    logger.error("Failed to fetch Zoho access token after all retries")
    raise HTTPException(status_code=500, detail="Failed to fetch Zoho access token after retries")

    async def __aenter__(self):
    return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
    await self.client.aclose()

    @staticmethod
    def handle_response(response: httpx.Response) -> dict:
    """Handle and parse Zoho API response."""
    try:
    response_data = response.json()

    if response.status_code == 404:
    raise ExternalAPIException("Zoho API endpoint not found")

    if response.status_code == 429:
    raise ExternalAPIException("Zoho API rate limit exceeded")

    if response.status_code >= 400:
    error_message = response_data.get("message", "Unknown error")
    raise ExternalAPIException(f"Zoho API error: {error_message}")

    if "data" not in response_data:
    raise ExternalAPIException("Invalid response format from Zoho API")

    return response_data["data"]

    except httpx.HTTPError as e:
    logger.error(f"Zoho API error: {str(e)}")
    raise ExternalAPIException(f"API error: {str(e)}")


    Despite caching the token and only refreshing when needed, I'm still seeing errors like:

    Token error 400: {"error_description":"You have made too many requests continuously. Please try again after some time.","error":"Access Denied","status":"failure"}


    Here is the example of full error:

    src.external.zoho_crm.client:get_access_token:140 - Token error 400: {"error_description":"You have made too many requests continuously. Please try again after some time.","error":"Access Denied","status":"failure"}
    2025-04-24 19:24:14.676 | WARNING | src.external.zoho_crm.client:get_access_token:144 - Attempt 1 failed to fetch token: 400: Token error: {"error_description":"You have made too many requests continuously. Please try again after some time.","error":"Access Denied","status":"failure"}


    • Why would I still be hitting the rate limit if I'm caching the token properly?


    • Could it be a concurrency issue (like multiple requests trying to refresh token at the same time)?


    • Should I add locking or some other mechanism to ensure only one token refresh happens at a time?

    Any advice or solution would be really appreciated!

    Continue reading...

Compartilhe esta Página