66from pydantic import BaseModel , Field , ValidationError , validate_call
77from requests import PreparedRequest , Response
88from requests .adapters import HTTPAdapter
9+ from requests .exceptions import ConnectionError
910from requests .sessions import Session
11+ from urllib3 import Retry
1012from vonage_http_client .auth import Auth
1113from vonage_http_client .errors import (
1214 AuthenticationError ,
@@ -90,7 +92,9 @@ def __init__(
9092 self ._adapter = HTTPAdapter (
9193 pool_connections = self ._http_client_options .pool_connections ,
9294 pool_maxsize = self ._http_client_options .pool_maxsize ,
93- max_retries = self ._http_client_options .max_retries ,
95+ max_retries = Retry (
96+ total = self ._http_client_options .max_retries , backoff_factor = 0.1
97+ ),
9498 )
9599 self ._session .mount ('https://' , self ._adapter )
96100
@@ -216,6 +220,30 @@ def make_request(
216220 sent_data_type : Literal ['json' , 'form' , 'query_params' ] = 'json' ,
217221 token : Optional [str ] = None ,
218222 ):
223+ """Make an HTTP request to the specified host. This method will automatically
224+ handle retries in the event of a connection error caused by a RemoteDisconnect
225+ exception.
226+
227+ It will retry the amount of times equal to the maximum number of connections
228+ allowed in a connection pool. I.e., assuming if all connections in a given pool
229+ are in use but the TCP connections to the Vonage host have failed, it will retry
230+ the amount of times equal to the maximum number of connections in the pool.
231+
232+ Args:
233+ request_type (str): The type of request to make (GET, POST, PATCH, PUT, DELETE).
234+ host (str): The host to make the request to.
235+ request_path (str, optional): The path to make the request to.
236+ params (dict, optional): The parameters to send with the request.
237+ auth_type (str, optional): The type of authentication to use with the request.
238+ sent_data_type (str, optional): The type of data being sent with the request.
239+ token (str, optional): The token to use for OAuth2 authentication.
240+
241+ Returns:
242+ dict: The response data from the request.
243+
244+ Raises:
245+ ConnectionError: If the request fails after the maximum number of retries.
246+ """
219247 url = f'https://{ host } { request_path } '
220248 logger .debug (
221249 f'{ request_type } request to { url } , with data: { params } ; headers: { self ._headers } '
@@ -248,8 +276,23 @@ def make_request(
248276 elif sent_data_type == 'form' :
249277 request_params ['data' ] = params
250278
251- with self ._session .request (** request_params ) as response :
252- return self ._parse_response (response )
279+ max_retries = self ._http_client_options .pool_maxsize or 10
280+ attempt = 0
281+ while attempt < max_retries :
282+ try :
283+ with self ._session .request (** request_params ) as response :
284+ return self ._parse_response (response )
285+ except ConnectionError as e :
286+ logger .debug (f'Connection Error: { e } ' )
287+ if 'RemoteDisconnected' in str (e .args ):
288+ attempt += 1
289+ if attempt >= max_retries :
290+ raise e
291+ logger .debug (
292+ f'ConnectionError caused by RemoteDisconnected exception. Retrying request, attempt { attempt + 1 } of { max_retries } '
293+ )
294+ else :
295+ raise e
253296
254297 def download_file_stream (self , url : str , file_path : str ) -> bytes :
255298 """Download a file from a URL and save it to a local file. This method streams the
@@ -272,6 +315,8 @@ def download_file_stream(self, url: str, file_path: str) -> bytes:
272315 )
273316 try :
274317 with self ._session .get (url , headers = headers , stream = True ) as response :
318+ if response .status_code >= 400 :
319+ self ._parse_response (response )
275320 with open (file_path , 'wb' ) as f :
276321 for chunk in response .iter_content (chunk_size = 4096 ):
277322 f .write (chunk )
@@ -296,8 +341,6 @@ def _parse_response(self, response: Response) -> Union[dict, None]:
296341 try :
297342 return response .json ()
298343 except JSONDecodeError :
299- if hasattr (response .headers , 'Content-Type' ):
300- return response .content
301344 return None
302345 if response .status_code >= 400 :
303346 content_type = response .headers ['Content-Type' ].split (';' , 1 )[0 ]
0 commit comments