11import asyncio
2+ import random
3+ import time
24from types import TracebackType
35from typing import Optional , Type , Union
46
1315 JsonType ,
1416 ParamsType ,
1517 ResponseJson ,
18+ RetryConfig ,
1619)
1720from workos .utils .request_helper import REQUEST_METHOD_GET
1821
@@ -38,6 +41,7 @@ def __init__(
3841 client_id : str ,
3942 version : str ,
4043 timeout : Optional [int ] = None ,
44+ retry_config : Optional [RetryConfig ] = None ,
4145 # If no custom transport is provided, let httpx use the default
4246 # so we don't overwrite environment configurations like proxies
4347 transport : Optional [httpx .BaseTransport ] = None ,
@@ -48,6 +52,7 @@ def __init__(
4852 client_id = client_id ,
4953 version = version ,
5054 timeout = timeout ,
55+ retry_config = retry_config ,
5156 )
5257 self ._client = SyncHttpxClientWrapper (
5358 base_url = base_url ,
@@ -110,8 +115,38 @@ def request(
110115 headers = headers ,
111116 exclude_default_auth_headers = exclude_default_auth_headers ,
112117 )
113- response = self ._client .request (** prepared_request_parameters )
114- return self ._handle_response (response )
118+
119+ last_exception = None
120+
121+ for attempt in range (self ._retry_config .max_retries + 1 ):
122+ try :
123+ response = self ._client .request (** prepared_request_parameters )
124+
125+ # Check if we should retry based on status code
126+ if attempt < self ._retry_config .max_retries and self ._is_retryable_error (response ):
127+ delay = self ._get_retry_delay (attempt , response )
128+ time .sleep (delay )
129+ continue
130+
131+ # No retry needed or max retries reached
132+ return self ._handle_response (response )
133+
134+ except Exception as exc :
135+ last_exception = exc
136+ if attempt < self ._retry_config .max_retries and self ._should_retry_exception (exc ):
137+ delay = self ._retry_config .base_delay * (2 ** attempt )
138+ delay = min (delay , self ._retry_config .max_delay )
139+ jitter_amount = delay * self ._retry_config .jitter * random .random ()
140+ time .sleep (delay + jitter_amount )
141+ continue
142+ raise
143+
144+ # Should not reach here, but raise last exception if we do
145+ if last_exception is not None :
146+ raise last_exception
147+
148+ # Fallback: this should never happen
149+ raise RuntimeError ("Unexpected state in retry logic" )
115150
116151
117152class AsyncHttpxClientWrapper (httpx .AsyncClient ):
@@ -138,6 +173,7 @@ def __init__(
138173 client_id : str ,
139174 version : str ,
140175 timeout : Optional [int ] = None ,
176+ retry_config : Optional [RetryConfig ] = None ,
141177 # If no custom transport is provided, let httpx use the default
142178 # so we don't overwrite environment configurations like proxies
143179 transport : Optional [httpx .AsyncBaseTransport ] = None ,
@@ -148,6 +184,7 @@ def __init__(
148184 client_id = client_id ,
149185 version = version ,
150186 timeout = timeout ,
187+ retry_config = retry_config ,
151188 )
152189 self ._client = AsyncHttpxClientWrapper (
153190 base_url = base_url ,
@@ -207,8 +244,38 @@ async def request(
207244 headers = headers ,
208245 exclude_default_auth_headers = exclude_default_auth_headers ,
209246 )
210- response = await self ._client .request (** prepared_request_parameters )
211- return self ._handle_response (response )
247+
248+ last_exception = None
249+
250+ for attempt in range (self ._retry_config .max_retries + 1 ):
251+ try :
252+ response = await self ._client .request (** prepared_request_parameters )
253+
254+ # Check if we should retry based on status code
255+ if attempt < self ._retry_config .max_retries and self ._is_retryable_error (response ):
256+ delay = self ._get_retry_delay (attempt , response )
257+ await asyncio .sleep (delay )
258+ continue
259+
260+ # No retry needed or max retries reached
261+ return self ._handle_response (response )
262+
263+ except Exception as exc :
264+ last_exception = exc
265+ if attempt < self ._retry_config .max_retries and self ._should_retry_exception (exc ):
266+ delay = self ._retry_config .base_delay * (2 ** attempt )
267+ delay = min (delay , self ._retry_config .max_delay )
268+ jitter_amount = delay * self ._retry_config .jitter * random .random ()
269+ await asyncio .sleep (delay + jitter_amount )
270+ continue
271+ raise
272+
273+ # Should not reach here, but raise last exception if we do
274+ if last_exception is not None :
275+ raise last_exception
276+
277+ # Fallback: this should never happen
278+ raise RuntimeError ("Unexpected state in retry logic" )
212279
213280
214281HTTPClient = Union [AsyncHTTPClient , SyncHTTPClient ]
0 commit comments