33import random
44import re
55from email .utils import parsedate_to_datetime
6+ from typing import Optional
67
78from kiota_abstractions .request_option import RequestOption
89from opentelemetry .semconv .attributes .http_attributes import HTTP_RESPONSE_STATUS_CODE
@@ -42,16 +43,18 @@ class RetryHandler(BaseMiddleware):
4243 ['HEAD' , 'GET' , 'POST' , 'PUT' , 'PATCH' , 'DELETE' , 'OPTIONS' , 'QUERY' ]
4344 )
4445
45- def __init__ (self , options : RetryHandlerOption = RetryHandlerOption () ) -> None :
46+ def __init__ (self , options : Optional [ RetryHandlerOption ] = None ) -> None :
4647 super ().__init__ ()
4748 self .allowed_methods : frozenset [str ] = self .DEFAULT_ALLOWED_METHODS
4849 self .backoff_factor : float = self .DEFAULT_BACKOFF_FACTOR
4950 self .backoff_max : int = self .MAXIMUM_BACKOFF
50- self .options = options
51- self .respect_retry_after_header : bool = self .options .DEFAULT_SHOULD_RETRY # type:ignore
51+ self .options = RetryHandlerOption () if options is None else options
52+ self .respect_retry_after_header : bool = self .options .DEFAULT_SHOULD_RETRY
5253 self .retry_on_status_codes : set [int ] = self .DEFAULT_RETRY_STATUS_CODES
5354
54- async def send (self , request : httpx .Request , transport : httpx .AsyncBaseTransport ):
55+ async def send (
56+ self , request : httpx .Request , transport : httpx .AsyncBaseTransport
57+ ) -> httpx .Response :
5558 """
5659 Sends the http request object to the next middleware or retries the request if necessary.
5760 """
@@ -88,6 +91,7 @@ async def send(self, request: httpx.Request, transport: httpx.AsyncBaseTransport
8891 request .headers .update ({RETRY_ATTEMPT : f'{ retry_count } ' })
8992 _retry_span .set_attribute (HTTP_RESPONSE_STATUS_CODE , response .status_code )
9093 _retry_span .set_attribute ('http.request.resend_count' , retry_count )
94+ _retry_span .end ()
9195 continue
9296 _retry_span .end ()
9397 break
@@ -107,11 +111,15 @@ def _get_current_options(self, request: httpx.Request) -> RetryHandlerOption:
107111 """
108112 request_options = request .extensions .get (REQUEST_OPTIONS_KEY )
109113 if request_options :
110- current_options = request_options .get (RetryHandlerOption .get_key (), self .options )
114+ current_options : RetryHandlerOption = request_options .get (
115+ RetryHandlerOption .get_key (), self .options
116+ )
111117 return current_options
112118 return self .options
113119
114- def should_retry (self , request , options , response ):
120+ def should_retry (
121+ self , request : httpx .Request , options : RetryHandlerOption , response : httpx .Response
122+ ) -> bool :
115123 """
116124 Determines whether the request should be retried
117125 Checks if the request method is in allowed methods
@@ -121,10 +129,9 @@ def should_retry(self, request, options, response):
121129 return False
122130 if not self ._is_request_payload_buffered (request ):
123131 return False
124- val = options .max_retry and (response .status_code in self .retry_on_status_codes )
125- return val
132+ return options .max_retry > 0 and response .status_code in self .retry_on_status_codes
126133
127- def _is_method_retryable (self , request ) :
134+ def _is_method_retryable (self , request : httpx . Request ) -> bool :
128135 """
129136 Checks if a given request should be retried upon, depending on
130137 whether the HTTP method is in the set of allowed methods
@@ -133,7 +140,7 @@ def _is_method_retryable(self, request):
133140 return False
134141 return True
135142
136- def _is_request_payload_buffered (self , request ) :
143+ def _is_request_payload_buffered (self , request : httpx . Request ) -> bool :
137144 """
138145 Checks if the request payload is buffered/rewindable.
139146 Payloads with forward only streams will return false and have the responses
@@ -145,15 +152,20 @@ def _is_request_payload_buffered(self, request):
145152 return False
146153 return True
147154
148- def check_retry_valid (self , retry_count , options ) :
155+ def check_retry_valid (self , retry_count : int , options : RetryHandlerOption ) -> bool :
149156 """
150157 Check that the max retries limit has not been hit
151158 """
152159 if retry_count < options .max_retry :
153160 return True
154161 return False
155162
156- def get_delay_time (self , retry_count , response = None , delay = RetryHandlerOption .DEFAULT_DELAY ):
163+ def get_delay_time (
164+ self ,
165+ retry_count : int ,
166+ response : Optional [httpx .Response ] = None ,
167+ delay : float = RetryHandlerOption .DEFAULT_DELAY
168+ ) -> float :
157169 """Get the time in seconds to delay before the next attempt.
158170
159171 A ``Retry-After`` response header that parses to more than zero seconds is
@@ -167,37 +179,41 @@ def get_delay_time(self, retry_count, response=None, delay=RetryHandlerOption.DE
167179 return retry_after
168180 return self ._get_delay_time_exp_backoff (retry_count , delay )
169181
170- def _get_delay_time_exp_backoff (self , retry_count , delay ) :
182+ def _get_delay_time_exp_backoff (self , retry_count : int , delay : float ) -> float :
171183 """
172184 Get time in seconds to delay between retry attempts based on an exponential
173185 backoff value.
174186 """
175- exp_backoff_value = self .backoff_factor * + (2 ** (retry_count - 1 ))
187+ exp_backoff_value : float = self .backoff_factor * + (2 ** (retry_count - 1 ))
176188 backoff_value = exp_backoff_value + (random .randint (0 , 1000 ) / 1000 ) + delay
177189
178190 backoff = min (self .backoff_max , backoff_value )
179191 return backoff
180192
181- def _get_retry_after (self , response ) :
193+ def _get_retry_after (self , response : Optional [ httpx . Response ]) -> Optional [ float ] :
182194 """
183195 Check if retry-after is specified in the response header and get the value
184196 """
197+ if response is None :
198+ return None
185199 retry_after = response .headers .get ("retry-after" )
186200 if retry_after :
187201 return self ._parse_retry_after (retry_after )
188202 return None
189203
190- def _parse_retry_after (self , retry_after ) :
204+ def _parse_retry_after (self , retry_after : str ) -> Optional [ float ] :
191205 """
192- Helper to parse Retry-After and get value in seconds.
206+ Helper to parse Retry-After and get value in seconds. Returns None when the
207+ value is neither a number of seconds nor an HTTP date.
193208 """
209+ if re .match (r"\d+,\d+$" , retry_after ):
210+ retry_after = retry_after .split ("," )[0 ]
194211 try :
195- retry_after = retry_after .split ("," )[0 ] if re .match (
196- r"\d+,\d+$" , retry_after
197- ) else retry_after
198- delay = int (retry_after )
212+ return max (0 , int (retry_after ))
199213 except ValueError :
200- # Not an integer? Try HTTP date
214+ pass
215+ try :
201216 retry_date = parsedate_to_datetime (retry_after )
202- delay = (retry_date - datetime .datetime .now (retry_date .tzinfo )).total_seconds ()
203- return max (0 , delay )
217+ except ValueError :
218+ return None
219+ return max (0.0 , (retry_date - datetime .datetime .now (retry_date .tzinfo )).total_seconds ())
0 commit comments