diff --git a/razorpay/client.py b/razorpay/client.py index 338558d..6d80e52 100644 --- a/razorpay/client.py +++ b/razorpay/client.py @@ -2,6 +2,8 @@ import json import requests import warnings +import random +import time from types import ModuleType @@ -34,7 +36,11 @@ class Client: """Razorpay client class""" DEFAULTS = { - 'base_url': URL.BASE_URL + 'base_url': URL.BASE_URL, + 'max_retries': 5, + 'initial_delay': 1, + 'max_delay': 60, + 'jitter': 0.25 } def __init__(self, session=None, auth=None, **options): @@ -48,6 +54,11 @@ def __init__(self, session=None, auth=None, **options): self.cert_path = file_dir + '/ca-bundle.crt' self.base_url = self._set_base_url(**options) + self.max_retries = options.get('max_retries', self.DEFAULTS['max_retries']) + self.initial_delay = options.get('initial_delay', self.DEFAULTS['initial_delay']) + self.max_delay = options.get('max_delay', self.DEFAULTS['max_delay']) + self.jitter = options.get('jitter', self.DEFAULTS['jitter']) + self.retry_enabled = False self.app_details = [] @@ -66,6 +77,12 @@ def _set_base_url(self, **options): base_url = options['base_url'] del(options['base_url']) + # Remove retry options from options if they exist + options.pop('max_retries', None) + options.pop('initial_delay', None) + options.pop('max_delay', None) + options.pop('jitter', None) + return base_url def _update_user_agent_header(self, options): @@ -128,16 +145,19 @@ def set_app_details(self, app_details): def get_app_details(self): return self.app_details + def enable_retry(self, retry_enabled=False): + self.retry_enabled = retry_enabled + def request(self, method, path, **options): """ - Dispatches a request to the Razorpay HTTP API + Dispatches a request to the Razorpay HTTP API with retry mechanism """ options = self._update_user_agent_header(options) # Determine authentication type use_public_auth = options.pop('use_public_auth', False) auth_to_use = self.auth - + if use_public_auth: # For public auth, use key_id only if self.auth and isinstance(self.auth, tuple) and len(self.auth) >= 1: @@ -151,31 +171,72 @@ def request(self, method, path, **options): options['headers']['X-Razorpay-Device-Mode'] = device_mode url = "{}{}".format(self.base_url, path) - - response = getattr(self.session, method)(url, auth=auth_to_use, - verify=self.cert_path, - **options) - if ((response.status_code >= HTTP_STATUS_CODE.OK) and - (response.status_code < HTTP_STATUS_CODE.REDIRECT)): - return json.dumps({}) if(response.status_code==204) else response.json() - else: - msg = "" - code = "" - json_response = response.json() - if 'error' in json_response: - if 'description' in json_response['error']: - msg = json_response['error']['description'] - if 'code' in json_response['error']: - code = str(json_response['error']['code']) - - if str.upper(code) == ERROR_CODE.BAD_REQUEST_ERROR: - raise BadRequestError(msg) - elif str.upper(code) == ERROR_CODE.GATEWAY_ERROR: - raise GatewayError(msg) - elif str.upper(code) == ERROR_CODE.SERVER_ERROR: # nosemgrep : python.lang.maintainability.useless-ifelse.useless-if-body - raise ServerError(msg) - else: - raise ServerError(msg) + + delay_seconds = self.initial_delay + + # If retry is not enabled, set max attempts to 1 + max_attempts = self.max_retries if self.retry_enabled else 1 + + for attempt in range(max_attempts): + try: + response = getattr(self.session, method)(url, auth=auth_to_use, + verify=self.cert_path, + **options) + + if ((response.status_code >= HTTP_STATUS_CODE.OK) and + (response.status_code < HTTP_STATUS_CODE.REDIRECT)): + return json.dumps({}) if(response.status_code==204) else response.json() + else: + msg = "" + code = "" + json_response = response.json() + if 'error' in json_response: + if 'description' in json_response['error']: + msg = json_response['error']['description'] + if 'code' in json_response['error']: + code = str(json_response['error']['code']) + + if str.upper(code) == ERROR_CODE.BAD_REQUEST_ERROR: + raise BadRequestError(msg) + elif str.upper(code) == ERROR_CODE.GATEWAY_ERROR: + raise GatewayError(msg) + elif str.upper(code) == ERROR_CODE.SERVER_ERROR: # nosemgrep : python.lang.maintainability.useless-ifelse.useless-if-body + raise ServerError(msg) + else: + raise ServerError(msg) + + except requests.exceptions.ConnectionError as e: + if self.retry_enabled and attempt < max_attempts - 1: # Don't sleep on the last attempt + # Apply exponential backoff with jitter + jitter_value = random.uniform(-self.jitter, self.jitter) + jittered_delay = delay_seconds * (1 + jitter_value) + # Cap the delay at max_delay + actual_delay = min(jittered_delay, self.max_delay) + + print(f"ConnectionError: {e}. Retrying in {actual_delay:.2f} seconds... (Attempt {attempt + 1}/{max_attempts})") + time.sleep(actual_delay) + delay_seconds *= 2 # Exponential backoff for next attempt + else: + print(f"Connection failed." + (f" Max retries ({max_attempts}) exceeded." if self.retry_enabled else "")) + raise + except requests.exceptions.Timeout as e: + if self.retry_enabled and attempt < max_attempts - 1: # Don't sleep on the last attempt + # Apply exponential backoff with jitter + jitter_value = random.uniform(-self.jitter, self.jitter) + jittered_delay = delay_seconds * (1 + jitter_value) + # Cap the delay at max_delay + actual_delay = min(jittered_delay, self.max_delay) + + print(f"Timeout: {e}. Retrying in {actual_delay:.2f} seconds... (Attempt {attempt + 1}/{max_attempts})") + time.sleep(actual_delay) + delay_seconds *= 2 # Exponential backoff for next attempt + else: + print(f"Request timed out." + (f" Max retries ({max_attempts}) exceeded." if self.retry_enabled else "")) + raise + except requests.exceptions.RequestException as e: + # For other request exceptions, don't retry + print(f"Request error occurred: {e}") + raise def get(self, path, params, **options): """