master
py 402 lines 14.3 KB
Raw
1 # SPDX-License-Identifier: MIT
2 from __future__ import absolute_import
3 import time
4 import logging
5 from collections import namedtuple
6 from itertools import takewhile
7 import email
8 import re
9
10 from ..exceptions import (
11 ConnectTimeoutError,
12 MaxRetryError,
13 ProtocolError,
14 ReadTimeoutError,
15 ResponseError,
16 InvalidHeader,
17 )
18 from ..packages import six
19
20
21 log = logging.getLogger(__name__)
22
23 # Data structure for representing the metadata of requests that result in a retry.
24 RequestHistory = namedtuple('RequestHistory', ["method", "url", "error",
25 "status", "redirect_location"])
26
27
28 class Retry(object):
29 """ Retry configuration.
30
31 Each retry attempt will create a new Retry object with updated values, so
32 they can be safely reused.
33
34 Retries can be defined as a default for a pool::
35
36 retries = Retry(connect=5, read=2, redirect=5)
37 http = PoolManager(retries=retries)
38 response = http.request('GET', 'http://example.com/')
39
40 Or per-request (which overrides the default for the pool)::
41
42 response = http.request('GET', 'http://example.com/', retries=Retry(10))
43
44 Retries can be disabled by passing ``False``::
45
46 response = http.request('GET', 'http://example.com/', retries=False)
47
48 Errors will be wrapped in :class:`~urllib3.exceptions.MaxRetryError` unless
49 retries are disabled, in which case the causing exception will be raised.
50
51 :param int total:
52 Total number of retries to allow. Takes precedence over other counts.
53
54 Set to ``None`` to remove this constraint and fall back on other
55 counts. It's a good idea to set this to some sensibly-high value to
56 account for unexpected edge cases and avoid infinite retry loops.
57
58 Set to ``0`` to fail on the first retry.
59
60 Set to ``False`` to disable and imply ``raise_on_redirect=False``.
61
62 :param int connect:
63 How many connection-related errors to retry on.
64
65 These are errors raised before the request is sent to the remote server,
66 which we assume has not triggered the server to process the request.
67
68 Set to ``0`` to fail on the first retry of this type.
69
70 :param int read:
71 How many times to retry on read errors.
72
73 These errors are raised after the request was sent to the server, so the
74 request may have side-effects.
75
76 Set to ``0`` to fail on the first retry of this type.
77
78 :param int redirect:
79 How many redirects to perform. Limit this to avoid infinite redirect
80 loops.
81
82 A redirect is a HTTP response with a status code 301, 302, 303, 307 or
83 308.
84
85 Set to ``0`` to fail on the first retry of this type.
86
87 Set to ``False`` to disable and imply ``raise_on_redirect=False``.
88
89 :param int status:
90 How many times to retry on bad status codes.
91
92 These are retries made on responses, where status code matches
93 ``status_forcelist``.
94
95 Set to ``0`` to fail on the first retry of this type.
96
97 :param iterable method_whitelist:
98 Set of uppercased HTTP method verbs that we should retry on.
99
100 By default, we only retry on methods which are considered to be
101 idempotent (multiple requests with the same parameters end with the
102 same state). See :attr:`Retry.DEFAULT_METHOD_WHITELIST`.
103
104 Set to a ``False`` value to retry on any verb.
105
106 :param iterable status_forcelist:
107 A set of integer HTTP status codes that we should force a retry on.
108 A retry is initiated if the request method is in ``method_whitelist``
109 and the response status code is in ``status_forcelist``.
110
111 By default, this is disabled with ``None``.
112
113 :param float backoff_factor:
114 A backoff factor to apply between attempts after the second try
115 (most errors are resolved immediately by a second try without a
116 delay). urllib3 will sleep for::
117
118 {backoff factor} * (2 ^ ({number of total retries} - 1))
119
120 seconds. If the backoff_factor is 0.1, then :func:`.sleep` will sleep
121 for [0.0s, 0.2s, 0.4s, ...] between retries. It will never be longer
122 than :attr:`Retry.BACKOFF_MAX`.
123
124 By default, backoff is disabled (set to 0).
125
126 :param bool raise_on_redirect: Whether, if the number of redirects is
127 exhausted, to raise a MaxRetryError, or to return a response with a
128 response code in the 3xx range.
129
130 :param bool raise_on_status: Similar meaning to ``raise_on_redirect``:
131 whether we should raise an exception, or return a response,
132 if status falls in ``status_forcelist`` range and retries have
133 been exhausted.
134
135 :param tuple history: The history of the request encountered during
136 each call to :meth:`~Retry.increment`. The list is in the order
137 the requests occurred. Each list item is of class :class:`RequestHistory`.
138
139 :param bool respect_retry_after_header:
140 Whether to respect Retry-After header on status codes defined as
141 :attr:`Retry.RETRY_AFTER_STATUS_CODES` or not.
142
143 """
144
145 DEFAULT_METHOD_WHITELIST = frozenset([
146 'HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'])
147
148 RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503])
149
150 #: Maximum backoff time.
151 BACKOFF_MAX = 120
152
153 def __init__(self, total=10, connect=None, read=None, redirect=None, status=None,
154 method_whitelist=DEFAULT_METHOD_WHITELIST, status_forcelist=None,
155 backoff_factor=0, raise_on_redirect=True, raise_on_status=True,
156 history=None, respect_retry_after_header=True):
157
158 self.total = total
159 self.connect = connect
160 self.read = read
161 self.status = status
162
163 if redirect is False or total is False:
164 redirect = 0
165 raise_on_redirect = False
166
167 self.redirect = redirect
168 self.status_forcelist = status_forcelist or set()
169 self.method_whitelist = method_whitelist
170 self.backoff_factor = backoff_factor
171 self.raise_on_redirect = raise_on_redirect
172 self.raise_on_status = raise_on_status
173 self.history = history or tuple()
174 self.respect_retry_after_header = respect_retry_after_header
175
176 def new(self, **kw):
177 params = dict(
178 total=self.total,
179 connect=self.connect, read=self.read, redirect=self.redirect, status=self.status,
180 method_whitelist=self.method_whitelist,
181 status_forcelist=self.status_forcelist,
182 backoff_factor=self.backoff_factor,
183 raise_on_redirect=self.raise_on_redirect,
184 raise_on_status=self.raise_on_status,
185 history=self.history,
186 )
187 params.update(kw)
188 return type(self)(**params)
189
190 @classmethod
191 def from_int(cls, retries, redirect=True, default=None):
192 """ Backwards-compatibility for the old retries format."""
193 if retries is None:
194 retries = default if default is not None else cls.DEFAULT
195
196 if isinstance(retries, Retry):
197 return retries
198
199 redirect = bool(redirect) and None
200 new_retries = cls(retries, redirect=redirect)
201 log.debug("Converted retries value: %r -> %r", retries, new_retries)
202 return new_retries
203
204 def get_backoff_time(self):
205 """ Formula for computing the current backoff
206
207 :rtype: float
208 """
209 # We want to consider only the last consecutive errors sequence (Ignore redirects).
210 consecutive_errors_len = len(list(takewhile(lambda x: x.redirect_location is None,
211 reversed(self.history))))
212 if consecutive_errors_len <= 1:
213 return 0
214
215 backoff_value = self.backoff_factor * (2 ** (consecutive_errors_len - 1))
216 return min(self.BACKOFF_MAX, backoff_value)
217
218 def parse_retry_after(self, retry_after):
219 # Whitespace: https://tools.ietf.org/html/rfc7230#section-3.2.4
220 if re.match(r"^\s*[0-9]+\s*$", retry_after):
221 seconds = int(retry_after)
222 else:
223 retry_date_tuple = email.utils.parsedate(retry_after)
224 if retry_date_tuple is None:
225 raise InvalidHeader("Invalid Retry-After header: %s" % retry_after)
226 retry_date = time.mktime(retry_date_tuple)
227 seconds = retry_date - time.time()
228
229 if seconds < 0:
230 seconds = 0
231
232 return seconds
233
234 def get_retry_after(self, response):
235 """ Get the value of Retry-After in seconds. """
236
237 retry_after = response.getheader("Retry-After")
238
239 if retry_after is None:
240 return None
241
242 return self.parse_retry_after(retry_after)
243
244 def sleep_for_retry(self, response=None):
245 retry_after = self.get_retry_after(response)
246 if retry_after:
247 time.sleep(retry_after)
248 return True
249
250 return False
251
252 def _sleep_backoff(self):
253 backoff = self.get_backoff_time()
254 if backoff <= 0:
255 return
256 time.sleep(backoff)
257
258 def sleep(self, response=None):
259 """ Sleep between retry attempts.
260
261 This method will respect a server's ``Retry-After`` response header
262 and sleep the duration of the time requested. If that is not present, it
263 will use an exponential backoff. By default, the backoff factor is 0 and
264 this method will return immediately.
265 """
266
267 if response:
268 slept = self.sleep_for_retry(response)
269 if slept:
270 return
271
272 self._sleep_backoff()
273
274 def _is_connection_error(self, err):
275 """ Errors when we're fairly sure that the server did not receive the
276 request, so it should be safe to retry.
277 """
278 return isinstance(err, ConnectTimeoutError)
279
280 def _is_read_error(self, err):
281 """ Errors that occur after the request has been started, so we should
282 assume that the server began processing it.
283 """
284 return isinstance(err, (ReadTimeoutError, ProtocolError))
285
286 def _is_method_retryable(self, method):
287 """ Checks if a given HTTP method should be retried upon, depending if
288 it is included on the method whitelist.
289 """
290 if self.method_whitelist and method.upper() not in self.method_whitelist:
291 return False
292
293 return True
294
295 def is_retry(self, method, status_code, has_retry_after=False):
296 """ Is this method/status code retryable? (Based on whitelists and control
297 variables such as the number of total retries to allow, whether to
298 respect the Retry-After header, whether this header is present, and
299 whether the returned status code is on the list of status codes to
300 be retried upon on the presence of the aforementioned header)
301 """
302 if not self._is_method_retryable(method):
303 return False
304
305 if self.status_forcelist and status_code in self.status_forcelist:
306 return True
307
308 return (self.total and self.respect_retry_after_header and
309 has_retry_after and (status_code in self.RETRY_AFTER_STATUS_CODES))
310
311 def is_exhausted(self):
312 """ Are we out of retries? """
313 retry_counts = (self.total, self.connect, self.read, self.redirect, self.status)
314 retry_counts = list(filter(None, retry_counts))
315 if not retry_counts:
316 return False
317
318 return min(retry_counts) < 0
319
320 def increment(self, method=None, url=None, response=None, error=None,
321 _pool=None, _stacktrace=None):
322 """ Return a new Retry object with incremented retry counters.
323
324 :param response: A response object, or None, if the server did not
325 return a response.
326 :type response: :class:`~urllib3.response.HTTPResponse`
327 :param Exception error: An error encountered during the request, or
328 None if the response was received successfully.
329
330 :return: A new ``Retry`` object.
331 """
332 if self.total is False and error:
333 # Disabled, indicate to re-raise the error.
334 raise six.reraise(type(error), error, _stacktrace)
335
336 total = self.total
337 if total is not None:
338 total -= 1
339
340 connect = self.connect
341 read = self.read
342 redirect = self.redirect
343 status_count = self.status
344 cause = 'unknown'
345 status = None
346 redirect_location = None
347
348 if error and self._is_connection_error(error):
349 # Connect retry?
350 if connect is False:
351 raise six.reraise(type(error), error, _stacktrace)
352 elif connect is not None:
353 connect -= 1
354
355 elif error and self._is_read_error(error):
356 # Read retry?
357 if read is False or not self._is_method_retryable(method):
358 raise six.reraise(type(error), error, _stacktrace)
359 elif read is not None:
360 read -= 1
361
362 elif response and response.get_redirect_location():
363 # Redirect retry?
364 if redirect is not None:
365 redirect -= 1
366 cause = 'too many redirects'
367 redirect_location = response.get_redirect_location()
368 status = response.status
369
370 else:
371 # Incrementing because of a server error like a 500 in
372 # status_forcelist and a the given method is in the whitelist
373 cause = ResponseError.GENERIC_ERROR
374 if response and response.status:
375 if status_count is not None:
376 status_count -= 1
377 cause = ResponseError.SPECIFIC_ERROR.format(
378 status_code=response.status)
379 status = response.status
380
381 history = self.history + (RequestHistory(method, url, error, status, redirect_location),)
382
383 new_retry = self.new(
384 total=total,
385 connect=connect, read=read, redirect=redirect, status=status_count,
386 history=history)
387
388 if new_retry.is_exhausted():
389 raise MaxRetryError(_pool, url, error or ResponseError(cause))
390
391 log.debug("Incremented Retry for (url='%s'): %r", url, new_retry)
392
393 return new_retry
394
395 def __repr__(self):
396 return ('{cls.__name__}(total={self.total}, connect={self.connect}, '
397 'read={self.read}, redirect={self.redirect}, status={self.status})').format(
398 cls=type(self), self=self)
399
400
401 # For backwards compatibility (equivalent to pre-v1.9):
402 Retry.DEFAULT = Retry(3)