Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/urllib3/util/retry.py: 36%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import email
4import logging
5import random
6import re
7import time
8import typing
9import warnings
10from collections.abc import Collection
11from itertools import takewhile
12from types import TracebackType
14from ..exceptions import (
15 ConnectTimeoutError,
16 InvalidHeader,
17 MaxRetryError,
18 ProtocolError,
19 ProxyError,
20 ReadTimeoutError,
21 ResponseError,
22)
23from .util import reraise
25if typing.TYPE_CHECKING:
26 from typing_extensions import Self
28 from ..connectionpool import ConnectionPool
29 from ..response import BaseHTTPResponse
31log = logging.getLogger(__name__)
34# Data structure for representing the metadata of requests that result in a retry.
35class RequestHistory(typing.NamedTuple):
36 method: str | None
37 url: str | None
38 error: Exception | None
39 status: int | None
40 redirect_location: str | None
43class Retry:
44 """Retry configuration.
46 Each retry attempt will create a new Retry object with updated values, so
47 they can be safely reused.
49 Retries can be defined as a default for a pool:
51 .. code-block:: python
53 retries = Retry(connect=5, read=2, redirect=5)
54 http = PoolManager(retries=retries)
55 response = http.request("GET", "https://example.com/")
57 Or per-request (which overrides the default for the pool):
59 .. code-block:: python
61 response = http.request("GET", "https://example.com/", retries=Retry(10))
63 Retries can be disabled by passing ``False``:
65 .. code-block:: python
67 response = http.request("GET", "https://example.com/", retries=False)
69 Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless
70 retries are disabled, in which case the causing exception will be raised.
72 :param int total:
73 Total number of retries to allow. Takes precedence over other counts.
75 Set to ``None`` to remove this constraint and fall back on other
76 counts.
78 Set to ``0`` to fail on the first retry.
80 Set to ``False`` to disable and imply ``raise_on_redirect=False``.
82 :param int connect:
83 How many connection-related errors to retry on.
85 These are errors raised before the request is sent to the remote server,
86 which we assume has not triggered the server to process the request.
88 Set to ``0`` to fail on the first retry of this type.
90 :param int read:
91 How many times to retry on read errors.
93 These errors are raised after the request was sent to the server, so the
94 request may have side-effects.
96 Set to ``0`` to fail on the first retry of this type.
98 :param int redirect:
99 How many redirects to perform. Limit this to avoid infinite redirect
100 loops.
102 A redirect is a HTTP response with a status code 301, 302, 303, 307 or
103 308.
105 Set to ``0`` to fail on the first retry of this type.
107 Set to ``False`` to disable and imply ``raise_on_redirect=False``.
109 :param int status:
110 How many times to retry on bad status codes.
112 These are retries made on responses, where status code matches
113 ``status_forcelist``.
115 Set to ``0`` to fail on the first retry of this type.
117 :param int other:
118 How many times to retry on other errors.
120 Other errors are errors that are not connect, read, redirect or status errors.
121 These errors might be raised after the request was sent to the server, so the
122 request might have side-effects.
124 Set to ``0`` to fail on the first retry of this type.
126 If ``total`` is not set, it's a good idea to set this to 0 to account
127 for unexpected edge cases and avoid infinite retry loops.
129 :param Collection allowed_methods:
130 Set of uppercased HTTP method verbs that we should retry on.
132 By default, we only retry on methods which are considered to be
133 idempotent (multiple requests with the same parameters end with the
134 same state). See :attr:`Retry.DEFAULT_ALLOWED_METHODS`.
136 Set to a ``None`` value to retry on any verb.
138 :param Collection status_forcelist:
139 A set of integer HTTP status codes that we should force a retry on.
140 A retry is initiated if the request method is in ``allowed_methods``
141 and the response status code is in ``status_forcelist``.
143 By default, this is disabled with ``None``.
145 :param float backoff_factor:
146 A backoff factor to apply between attempts after the second try
147 (most errors are resolved immediately by a second try without a
148 delay). urllib3 will sleep for::
150 {backoff factor} * (2 ** ({number of previous retries}))
152 seconds. If `backoff_jitter` is non-zero, this sleep is extended by::
154 random.uniform(0, {backoff jitter})
156 seconds. For example, if the backoff_factor is 0.1, then :func:`Retry.sleep` will
157 sleep for [0.0s, 0.2s, 0.4s, 0.8s, ...] between retries. No backoff will ever
158 be longer than `backoff_max`.
160 By default, backoff is disabled (factor set to 0).
162 :param float backoff_max:
163 The maximum backoff time (in seconds) between retry attempts.
164 This value caps the computed backoff from `backoff_factor`.
166 :param float backoff_jitter:
167 Random jitter amount (in seconds) added to the computed backoff.
168 Jitter is sampled uniformly from `0` to `backoff_jitter`.
170 :param bool raise_on_redirect: Whether, if the number of redirects is
171 exhausted, to raise a MaxRetryError, or to return a response with a
172 response code in the 3xx range.
174 :param bool raise_on_status: Similar meaning to ``raise_on_redirect``:
175 whether we should raise an exception, or return a response,
176 if status falls in ``status_forcelist`` range and retries have
177 been exhausted.
179 :param tuple history: The history of the request encountered during
180 each call to :meth:`~Retry.increment`. The list is in the order
181 the requests occurred. Each list item is of class :class:`RequestHistory`.
183 :param bool respect_retry_after_header:
184 Whether to respect Retry-After header on status codes defined as
185 :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not.
187 :param Collection remove_headers_on_redirect:
188 Sequence of headers to remove from the request when a response
189 indicating a redirect is returned before firing off the redirected
190 request.
192 :param int retry_after_max: Number of seconds to allow as the maximum for
193 Retry-After headers. Defaults to :attr:`Retry.DEFAULT_RETRY_AFTER_MAX`.
194 Any Retry-After headers larger than this value will be limited to this
195 value.
196 """
198 #: Default methods to be used for ``allowed_methods``
199 DEFAULT_ALLOWED_METHODS = frozenset(
200 ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"]
201 )
203 #: Default status codes that trigger a retry when a Retry-After header is
204 #: present and :attr:`Retry.respect_retry_after_header` is enabled.
205 RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503])
207 #: Default headers to be used for ``remove_headers_on_redirect``
208 DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset(
209 ["Cookie", "Authorization", "Proxy-Authorization"]
210 )
212 #: Default maximum backoff time.
213 DEFAULT_BACKOFF_MAX = 120
215 # This is undocumented in the RFC. Setting to 6 hours matches other popular libraries.
216 #: Default maximum allowed value for Retry-After headers in seconds
217 DEFAULT_RETRY_AFTER_MAX: typing.Final[int] = 21600
219 # Backward compatibility; assigned outside of the class.
220 DEFAULT: typing.ClassVar[Retry]
222 def __init__(
223 self,
224 total: bool | int | None = 10,
225 connect: int | None = None,
226 read: int | None = None,
227 redirect: bool | int | None = None,
228 status: int | None = None,
229 other: int | None = None,
230 allowed_methods: typing.Collection[str] | None = DEFAULT_ALLOWED_METHODS,
231 status_forcelist: typing.Collection[int] | None = None,
232 backoff_factor: float = 0,
233 backoff_max: float = DEFAULT_BACKOFF_MAX,
234 raise_on_redirect: bool = True,
235 raise_on_status: bool = True,
236 history: tuple[RequestHistory, ...] | None = None,
237 respect_retry_after_header: bool = True,
238 remove_headers_on_redirect: typing.Collection[
239 str
240 ] = DEFAULT_REMOVE_HEADERS_ON_REDIRECT,
241 backoff_jitter: float = 0.0,
242 retry_after_max: int = DEFAULT_RETRY_AFTER_MAX,
243 ) -> None:
244 self.total = total
245 self.connect = connect
246 self.read = read
247 self.status = status
248 self.other = other
250 if redirect is False or total is False:
251 redirect = 0
252 raise_on_redirect = False
254 self.redirect = redirect
255 self.status_forcelist = status_forcelist or set()
256 if not allowed_methods and isinstance(allowed_methods, Collection):
257 warnings.warn(
258 "Using an empty collection for 'allowed_methods' option to "
259 "retry on any verb is deprecated and will skip retries for "
260 "all verbs in urllib3 v3.0. Instead use "
261 "Retry(..., allowed_methods=None).",
262 FutureWarning,
263 stacklevel=2,
264 )
265 self.allowed_methods = allowed_methods
266 self.backoff_factor = backoff_factor
267 self.backoff_max = backoff_max
268 self.retry_after_max = retry_after_max
269 self.raise_on_redirect = raise_on_redirect
270 self.raise_on_status = raise_on_status
271 self.history = history or ()
272 self.respect_retry_after_header = respect_retry_after_header
273 self.remove_headers_on_redirect = frozenset(
274 h.lower() for h in remove_headers_on_redirect
275 )
276 self.backoff_jitter = backoff_jitter
278 def new(self, **kw: typing.Any) -> Self:
279 params = dict(
280 total=self.total,
281 connect=self.connect,
282 read=self.read,
283 redirect=self.redirect,
284 status=self.status,
285 other=self.other,
286 allowed_methods=self.allowed_methods,
287 status_forcelist=self.status_forcelist,
288 backoff_factor=self.backoff_factor,
289 backoff_max=self.backoff_max,
290 retry_after_max=self.retry_after_max,
291 raise_on_redirect=self.raise_on_redirect,
292 raise_on_status=self.raise_on_status,
293 history=self.history,
294 remove_headers_on_redirect=self.remove_headers_on_redirect,
295 respect_retry_after_header=self.respect_retry_after_header,
296 backoff_jitter=self.backoff_jitter,
297 )
299 params.update(kw)
300 return type(self)(**params) # type: ignore[arg-type]
302 @classmethod
303 def from_int(
304 cls,
305 retries: Retry | bool | int | None,
306 redirect: bool | int | None = True,
307 default: Retry | bool | int | None = None,
308 ) -> Retry:
309 """Backwards-compatibility for the old retries format."""
310 if retries is None:
311 retries = default if default is not None else cls.DEFAULT
313 if isinstance(retries, Retry):
314 return retries
316 redirect = bool(redirect) and None
317 new_retries = cls(retries, redirect=redirect)
318 log.debug("Converted retries value: %r -> %r", retries, new_retries)
319 return new_retries
321 def get_backoff_time(self) -> float:
322 """Formula for computing the current backoff
324 :rtype: float
325 """
326 # We want to consider only the last consecutive errors sequence (Ignore redirects).
327 consecutive_errors_len = len(
328 list(
329 takewhile(lambda x: x.redirect_location is None, reversed(self.history))
330 )
331 )
332 if consecutive_errors_len <= 1:
333 return 0
335 backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1))
336 if self.backoff_jitter != 0.0:
337 backoff_value += random.random() * self.backoff_jitter
338 return float(max(0, min(self.backoff_max, backoff_value)))
340 def parse_retry_after(self, retry_after: str) -> float:
341 seconds: float
342 # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4
343 if re.match(r"^\s*[0-9]+\s*$", retry_after):
344 seconds = int(retry_after)
345 else:
346 retry_date_tuple = email.utils.parsedate_tz(retry_after)
347 if retry_date_tuple is None:
348 raise InvalidHeader(f"Invalid Retry-After header: {retry_after}")
350 retry_date = email.utils.mktime_tz(retry_date_tuple)
351 seconds = retry_date - time.time()
353 seconds = max(seconds, 0)
355 # Check the seconds do not exceed the specified maximum
356 if seconds > self.retry_after_max:
357 seconds = self.retry_after_max
359 return seconds
361 def get_retry_after(self, response: BaseHTTPResponse) -> float | None:
362 """Get the value of Retry-After in seconds."""
364 retry_after = response.headers.get("Retry-After")
366 if retry_after is None:
367 return None
369 return self.parse_retry_after(retry_after)
371 def sleep_for_retry(self, response: BaseHTTPResponse) -> bool:
372 retry_after = self.get_retry_after(response)
373 if retry_after:
374 time.sleep(retry_after)
375 return True
377 return False
379 def _sleep_backoff(self) -> None:
380 backoff = self.get_backoff_time()
381 if backoff <= 0:
382 return
383 time.sleep(backoff)
385 def sleep(self, response: BaseHTTPResponse | None = None) -> None:
386 """Sleep between retry attempts.
388 This method will respect a server's ``Retry-After`` response header
389 and sleep the duration of the time requested. If that is not present, it
390 will use an exponential backoff. By default, the backoff factor is 0 and
391 this method will return immediately.
392 """
394 if self.respect_retry_after_header and response:
395 slept = self.sleep_for_retry(response)
396 if slept:
397 return
399 self._sleep_backoff()
401 def _is_connection_error(self, err: Exception) -> bool:
402 """Errors when we're fairly sure that the server did not receive the
403 request, so it should be safe to retry.
404 """
405 if isinstance(err, ProxyError):
406 err = err.original_error
407 return isinstance(err, ConnectTimeoutError)
409 def _is_read_error(self, err: Exception) -> bool:
410 """Errors that occur after the request has been started, so we should
411 assume that the server began processing it.
412 """
413 return isinstance(err, (ReadTimeoutError, ProtocolError))
415 def _is_method_retryable(self, method: str) -> bool:
416 """Checks if a given HTTP method should be retried upon, depending if
417 it is included in the allowed_methods
418 """
419 if self.allowed_methods and method.upper() not in self.allowed_methods:
420 return False
421 return True
423 def is_retry(
424 self, method: str, status_code: int, has_retry_after: bool = False
425 ) -> bool:
426 """Is this method/status code retryable? (Based on allowlists and control
427 variables such as the number of total retries to allow, whether to
428 respect the Retry-After header, whether this header is present, and
429 whether the returned status code is on the list of status codes to
430 be retried upon on the presence of the aforementioned header)
431 """
432 if not self._is_method_retryable(method):
433 return False
435 if self.status_forcelist and status_code in self.status_forcelist:
436 return True
438 return bool(
439 self.total
440 and self.respect_retry_after_header
441 and has_retry_after
442 and (status_code in self.RETRY_AFTER_STATUS_CODES)
443 )
445 def is_exhausted(self) -> bool:
446 """Are we out of retries?"""
447 retry_counts = [
448 x
449 for x in (
450 self.total,
451 self.connect,
452 self.read,
453 self.redirect,
454 self.status,
455 self.other,
456 )
457 if x
458 ]
459 if not retry_counts:
460 return False
462 return min(retry_counts) < 0
464 def increment(
465 self,
466 method: str | None = None,
467 url: str | None = None,
468 response: BaseHTTPResponse | None = None,
469 error: Exception | None = None,
470 _pool: ConnectionPool | None = None,
471 _stacktrace: TracebackType | None = None,
472 ) -> Self:
473 """Return a new Retry object with incremented retry counters.
475 :param response: A response object, or None, if the server did not
476 return a response.
477 :type response: :class:`~urllib3.response.BaseHTTPResponse`
478 :param Exception error: An error encountered during the request, or
479 None if the response was received successfully.
481 :return: A new ``Retry`` object.
482 """
483 if self.total is False and error:
484 # Disabled, indicate to re-raise the error.
485 raise reraise(type(error), error, _stacktrace)
487 total = self.total
488 if total is not None:
489 total -= 1
491 connect = self.connect
492 read = self.read
493 redirect = self.redirect
494 status_count = self.status
495 other = self.other
496 cause = "unknown"
497 status = None
498 redirect_location = None
500 if error and self._is_connection_error(error):
501 # Connect retry?
502 if connect is False:
503 raise reraise(type(error), error, _stacktrace)
504 elif connect is not None:
505 connect -= 1
507 elif error and self._is_read_error(error):
508 # Read retry?
509 if read is False or method is None or not self._is_method_retryable(method):
510 raise reraise(type(error), error, _stacktrace)
511 elif read is not None:
512 read -= 1
514 elif error:
515 # Other retry?
516 if other is not None:
517 other -= 1
519 elif response and response.get_redirect_location():
520 # Redirect retry?
521 if redirect is not None:
522 redirect -= 1
523 cause = "too many redirects"
524 response_redirect_location = response.get_redirect_location()
525 if response_redirect_location:
526 redirect_location = response_redirect_location
527 status = response.status
529 else:
530 # Incrementing because of a server error like a 500 in
531 # status_forcelist and the given method is in the allowed_methods
532 cause = ResponseError.GENERIC_ERROR
533 if response and response.status:
534 if status_count is not None:
535 status_count -= 1
536 cause = ResponseError.SPECIFIC_ERROR.format(status_code=response.status)
537 status = response.status
539 history = self.history + (
540 RequestHistory(method, url, error, status, redirect_location),
541 )
543 new_retry = self.new(
544 total=total,
545 connect=connect,
546 read=read,
547 redirect=redirect,
548 status=status_count,
549 other=other,
550 history=history,
551 )
553 if new_retry.is_exhausted():
554 reason = error or ResponseError(cause)
555 raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type]
557 log.debug("Incremented Retry for (url='%s'): %r", url, new_retry)
559 return new_retry
561 def __repr__(self) -> str:
562 return (
563 f"{type(self).__name__}(total={self.total}, connect={self.connect}, "
564 f"read={self.read}, redirect={self.redirect}, status={self.status})"
565 )
568# For backwards compatibility (equivalent to pre-v1.9):
569Retry.DEFAULT = Retry(3)