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). In production services,
161 setting a non-zero factor (e.g. 0.5 to 1.0) along with `backoff_jitter` is
162 recommended to prevent overloading servers with immediate retries.
164 :param float backoff_max:
165 The maximum backoff time (in seconds) between retry attempts.
166 This value caps the computed backoff from `backoff_factor`.
168 :param float backoff_jitter:
169 Random jitter amount (in seconds) added to the computed backoff.
170 Jitter is sampled uniformly from `0` to `backoff_jitter`.
172 :param bool raise_on_redirect: Whether, if the number of redirects is
173 exhausted, to raise a MaxRetryError, or to return a response with a
174 response code in the 3xx range.
176 :param bool raise_on_status: Similar meaning to ``raise_on_redirect``:
177 whether we should raise an exception, or return a response,
178 if status falls in ``status_forcelist`` range and retries have
179 been exhausted.
181 :param tuple history: The history of the request encountered during
182 each call to :meth:`~Retry.increment`. The list is in the order
183 the requests occurred. Each list item is of class :class:`RequestHistory`.
185 :param bool respect_retry_after_header:
186 Whether to respect Retry-After header on status codes defined as
187 :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not.
189 :param Collection remove_headers_on_redirect:
190 Sequence of headers to remove from the request when a response
191 indicating a redirect is returned before firing off the redirected
192 request.
194 :param int retry_after_max: Number of seconds to allow as the maximum for
195 Retry-After headers. Defaults to :attr:`Retry.DEFAULT_RETRY_AFTER_MAX`.
196 Any Retry-After headers larger than this value will be limited to this
197 value.
198 """
200 #: Default methods to be used for ``allowed_methods``
201 DEFAULT_ALLOWED_METHODS = frozenset(
202 ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"]
203 )
205 #: Default status codes that trigger a retry when a Retry-After header is
206 #: present and ``respect_retry_after_header`` is enabled.
207 RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503])
209 #: Default headers to be used for ``remove_headers_on_redirect``
210 DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset(
211 ["Cookie", "Authorization", "Proxy-Authorization"]
212 )
214 #: Default maximum backoff time.
215 DEFAULT_BACKOFF_MAX = 120
217 # This is undocumented in the RFC. Setting to 6 hours matches other popular libraries.
218 #: Default maximum allowed value for Retry-After headers in seconds
219 DEFAULT_RETRY_AFTER_MAX: typing.Final[int] = 21600
221 # Backward compatibility; assigned outside of the class.
222 DEFAULT: typing.ClassVar[Retry]
224 def __init__(
225 self,
226 total: bool | int | None = 10,
227 connect: int | None = None,
228 read: int | None = None,
229 redirect: bool | int | None = None,
230 status: int | None = None,
231 other: int | None = None,
232 allowed_methods: typing.Collection[str] | None = DEFAULT_ALLOWED_METHODS,
233 status_forcelist: typing.Collection[int] | None = None,
234 backoff_factor: float = 0,
235 backoff_max: float = DEFAULT_BACKOFF_MAX,
236 raise_on_redirect: bool = True,
237 raise_on_status: bool = True,
238 history: tuple[RequestHistory, ...] | None = None,
239 respect_retry_after_header: bool = True,
240 remove_headers_on_redirect: typing.Collection[
241 str
242 ] = DEFAULT_REMOVE_HEADERS_ON_REDIRECT,
243 backoff_jitter: float = 0.0,
244 retry_after_max: int = DEFAULT_RETRY_AFTER_MAX,
245 ) -> None:
246 self.total = total
247 self.connect = connect
248 self.read = read
249 self.status = status
250 self.other = other
252 if redirect is False or total is False:
253 redirect = 0
254 raise_on_redirect = False
256 self.redirect = redirect
257 self.status_forcelist = status_forcelist or set()
258 if not allowed_methods and isinstance(allowed_methods, Collection):
259 warnings.warn(
260 "Using an empty collection for 'allowed_methods' option to "
261 "retry on any verb is deprecated and will skip retries for "
262 "all verbs in urllib3 v3.0. Instead use "
263 "Retry(..., allowed_methods=None).",
264 FutureWarning,
265 stacklevel=2,
266 )
267 self.allowed_methods = allowed_methods
268 self.backoff_factor = backoff_factor
269 self.backoff_max = backoff_max
270 self.retry_after_max = retry_after_max
271 self.raise_on_redirect = raise_on_redirect
272 self.raise_on_status = raise_on_status
273 self.history = history or ()
274 self.respect_retry_after_header = respect_retry_after_header
275 self.remove_headers_on_redirect = frozenset(
276 h.lower() for h in remove_headers_on_redirect
277 )
278 self.backoff_jitter = backoff_jitter
280 def new(self, **kw: typing.Any) -> Self:
281 params = dict(
282 total=self.total,
283 connect=self.connect,
284 read=self.read,
285 redirect=self.redirect,
286 status=self.status,
287 other=self.other,
288 allowed_methods=self.allowed_methods,
289 status_forcelist=self.status_forcelist,
290 backoff_factor=self.backoff_factor,
291 backoff_max=self.backoff_max,
292 retry_after_max=self.retry_after_max,
293 raise_on_redirect=self.raise_on_redirect,
294 raise_on_status=self.raise_on_status,
295 history=self.history,
296 remove_headers_on_redirect=self.remove_headers_on_redirect,
297 respect_retry_after_header=self.respect_retry_after_header,
298 backoff_jitter=self.backoff_jitter,
299 )
301 params.update(kw)
302 return type(self)(**params) # type: ignore[arg-type]
304 @classmethod
305 def from_int(
306 cls,
307 retries: Retry | bool | int | None,
308 redirect: bool | int | None = True,
309 default: Retry | bool | int | None = None,
310 ) -> Retry:
311 """Backwards-compatibility for the old retries format."""
312 if retries is None:
313 retries = default if default is not None else cls.DEFAULT
315 if isinstance(retries, Retry):
316 return retries
318 redirect = bool(redirect) and None
319 new_retries = cls(retries, redirect=redirect)
320 log.debug("Converted retries value: %r -> %r", retries, new_retries)
321 return new_retries
323 def get_backoff_time(self) -> float:
324 """Formula for computing the current backoff
326 :rtype: float
327 """
328 # We want to consider only the last consecutive errors sequence (Ignore redirects).
329 consecutive_errors_len = len(
330 list(
331 takewhile(lambda x: x.redirect_location is None, reversed(self.history))
332 )
333 )
334 if consecutive_errors_len <= 1:
335 return 0
337 backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1))
338 if self.backoff_jitter != 0.0:
339 backoff_value += random.random() * self.backoff_jitter
340 return float(max(0, min(self.backoff_max, backoff_value)))
342 def parse_retry_after(self, retry_after: str) -> float:
343 seconds: float
344 # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4
345 if re.match(r"^\s*[0-9]+\s*$", retry_after):
346 seconds = int(retry_after)
347 else:
348 retry_date_tuple = email.utils.parsedate_tz(retry_after)
349 if retry_date_tuple is None:
350 raise InvalidHeader(f"Invalid Retry-After header: {retry_after}")
352 retry_date = email.utils.mktime_tz(retry_date_tuple)
353 seconds = retry_date - time.time()
355 seconds = max(seconds, 0)
357 # Check the seconds do not exceed the specified maximum
358 if seconds > self.retry_after_max:
359 seconds = self.retry_after_max
361 return seconds
363 def get_retry_after(self, response: BaseHTTPResponse) -> float | None:
364 """Get the value of Retry-After in seconds."""
366 retry_after = response.headers.get("Retry-After")
368 if retry_after is None:
369 return None
371 return self.parse_retry_after(retry_after)
373 def sleep_for_retry(self, response: BaseHTTPResponse) -> bool:
374 retry_after = self.get_retry_after(response)
375 if retry_after:
376 time.sleep(retry_after)
377 return True
379 return False
381 def _sleep_backoff(self) -> None:
382 backoff = self.get_backoff_time()
383 if backoff <= 0:
384 return
385 time.sleep(backoff)
387 def sleep(self, response: BaseHTTPResponse | None = None) -> None:
388 """Sleep between retry attempts.
390 This method will respect a server's ``Retry-After`` response header
391 and sleep the duration of the time requested. If that is not present, it
392 will use an exponential backoff. By default, the backoff factor is 0 and
393 this method will return immediately.
394 """
396 if self.respect_retry_after_header and response:
397 slept = self.sleep_for_retry(response)
398 if slept:
399 return
401 self._sleep_backoff()
403 def _is_connection_error(self, err: Exception) -> bool:
404 """Errors when we're fairly sure that the server did not receive the
405 request, so it should be safe to retry.
406 """
407 if isinstance(err, ProxyError):
408 err = err.original_error
409 return isinstance(err, ConnectTimeoutError)
411 def _is_read_error(self, err: Exception) -> bool:
412 """Errors that occur after the request has been started, so we should
413 assume that the server began processing it.
414 """
415 return isinstance(err, (ReadTimeoutError, ProtocolError))
417 def _is_method_retryable(self, method: str) -> bool:
418 """Checks if a given HTTP method should be retried upon, depending if
419 it is included in the allowed_methods
420 """
421 if self.allowed_methods and method.upper() not in self.allowed_methods:
422 return False
423 return True
425 def is_retry(
426 self, method: str, status_code: int, has_retry_after: bool = False
427 ) -> bool:
428 """Is this method/status code retryable? (Based on allowlists and control
429 variables such as the number of total retries to allow, whether to
430 respect the Retry-After header, whether this header is present, and
431 whether the returned status code is on the list of status codes to
432 be retried upon on the presence of the aforementioned header)
433 """
434 if not self._is_method_retryable(method):
435 return False
437 if self.status_forcelist and status_code in self.status_forcelist:
438 return True
440 return bool(
441 self.total
442 and self.respect_retry_after_header
443 and has_retry_after
444 and (status_code in self.RETRY_AFTER_STATUS_CODES)
445 )
447 def is_exhausted(self) -> bool:
448 """Are we out of retries?"""
449 retry_counts = [
450 x
451 for x in (
452 self.total,
453 self.connect,
454 self.read,
455 self.redirect,
456 self.status,
457 self.other,
458 )
459 if x
460 ]
461 if not retry_counts:
462 return False
464 return min(retry_counts) < 0
466 def increment(
467 self,
468 method: str | None = None,
469 url: str | None = None,
470 response: BaseHTTPResponse | None = None,
471 error: Exception | None = None,
472 _pool: ConnectionPool | None = None,
473 _stacktrace: TracebackType | None = None,
474 ) -> Self:
475 """Return a new Retry object with incremented retry counters.
477 :param response: A response object, or None, if the server did not
478 return a response.
479 :type response: :class:`~urllib3.response.BaseHTTPResponse`
480 :param Exception error: An error encountered during the request, or
481 None if the response was received successfully.
483 :return: A new ``Retry`` object.
484 """
485 if self.total is False and error:
486 # Disabled, indicate to re-raise the error.
487 raise reraise(type(error), error, _stacktrace)
489 total = self.total
490 if total is not None:
491 total -= 1
493 connect = self.connect
494 read = self.read
495 redirect = self.redirect
496 status_count = self.status
497 other = self.other
498 cause = "unknown"
499 status = None
500 redirect_location = None
502 if error and self._is_connection_error(error):
503 # Connect retry?
504 if connect is False:
505 raise reraise(type(error), error, _stacktrace)
506 elif connect is not None:
507 connect -= 1
509 elif error and self._is_read_error(error):
510 # Read retry?
511 if read is False or method is None or not self._is_method_retryable(method):
512 raise reraise(type(error), error, _stacktrace)
513 elif read is not None:
514 read -= 1
516 elif error:
517 # Other retry?
518 if other is not None:
519 other -= 1
521 elif response and response.get_redirect_location():
522 # Redirect retry?
523 if redirect is not None:
524 redirect -= 1
525 cause = "too many redirects"
526 response_redirect_location = response.get_redirect_location()
527 if response_redirect_location:
528 redirect_location = response_redirect_location
529 status = response.status
531 else:
532 # Incrementing because of a server error like a 500 in
533 # status_forcelist and the given method is in the allowed_methods
534 cause = ResponseError.GENERIC_ERROR
535 if response and response.status:
536 if status_count is not None:
537 status_count -= 1
538 cause = ResponseError.SPECIFIC_ERROR.format(status_code=response.status)
539 status = response.status
541 history = self.history + (
542 RequestHistory(method, url, error, status, redirect_location),
543 )
545 new_retry = self.new(
546 total=total,
547 connect=connect,
548 read=read,
549 redirect=redirect,
550 status=status_count,
551 other=other,
552 history=history,
553 )
555 if new_retry.is_exhausted():
556 reason = error or ResponseError(cause)
557 raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type]
559 log.debug("Incremented Retry for (url='%s'): %r", url, new_retry)
561 return new_retry
563 def __repr__(self) -> str:
564 return (
565 f"{type(self).__name__}(total={self.total}, connect={self.connect}, "
566 f"read={self.read}, redirect={self.redirect}, status={self.status})"
567 )
570# For backwards compatibility (equivalent to pre-v1.9):
571Retry.DEFAULT = Retry(3)