Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 89 additions & 28 deletions razorpay/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import json
import requests
import warnings
import random
import time

from types import ModuleType

Expand Down Expand Up @@ -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):
Expand All @@ -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 = []

Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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):
"""
Expand Down
Loading