|  | 
| 73 | 73 | from ._constants import ( | 
| 74 | 74 |     DEFAULT_LIMITS, | 
| 75 | 75 |     DEFAULT_TIMEOUT, | 
|  | 76 | +    MAX_RETRY_DELAY, | 
| 76 | 77 |     DEFAULT_MAX_RETRIES, | 
|  | 78 | +    INITIAL_RETRY_DELAY, | 
| 77 | 79 |     RAW_RESPONSE_HEADER, | 
| 78 | 80 |     OVERRIDE_CAST_TO_HEADER, | 
| 79 | 81 | ) | 
| @@ -590,47 +592,57 @@ def base_url(self, url: URL | str) -> None: | 
| 590 | 592 |     def platform_headers(self) -> Dict[str, str]: | 
| 591 | 593 |         return platform_headers(self._version) | 
| 592 | 594 | 
 | 
|  | 595 | +    def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None: | 
|  | 596 | +        """Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified. | 
|  | 597 | +
 | 
|  | 598 | +        About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After | 
|  | 599 | +        See also  https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax | 
|  | 600 | +        """ | 
|  | 601 | +        if response_headers is None: | 
|  | 602 | +            return None | 
|  | 603 | + | 
|  | 604 | +        # First, try the non-standard `retry-after-ms` header for milliseconds, | 
|  | 605 | +        # which is more precise than integer-seconds `retry-after` | 
|  | 606 | +        try: | 
|  | 607 | +            retry_ms_header = response_headers.get("retry-after-ms", None) | 
|  | 608 | +            return float(retry_ms_header) / 1000 | 
|  | 609 | +        except (TypeError, ValueError): | 
|  | 610 | +            pass | 
|  | 611 | + | 
|  | 612 | +        # Next, try parsing `retry-after` header as seconds (allowing nonstandard floats). | 
|  | 613 | +        retry_header = response_headers.get("retry-after") | 
|  | 614 | +        try: | 
|  | 615 | +            # note: the spec indicates that this should only ever be an integer | 
|  | 616 | +            # but if someone sends a float there's no reason for us to not respect it | 
|  | 617 | +            return float(retry_header) | 
|  | 618 | +        except (TypeError, ValueError): | 
|  | 619 | +            pass | 
|  | 620 | + | 
|  | 621 | +        # Last, try parsing `retry-after` as a date. | 
|  | 622 | +        retry_date_tuple = email.utils.parsedate_tz(retry_header) | 
|  | 623 | +        if retry_date_tuple is None: | 
|  | 624 | +            return None | 
|  | 625 | + | 
|  | 626 | +        retry_date = email.utils.mktime_tz(retry_date_tuple) | 
|  | 627 | +        return float(retry_date - time.time()) | 
|  | 628 | + | 
| 593 | 629 |     def _calculate_retry_timeout( | 
| 594 | 630 |         self, | 
| 595 | 631 |         remaining_retries: int, | 
| 596 | 632 |         options: FinalRequestOptions, | 
| 597 | 633 |         response_headers: Optional[httpx.Headers] = None, | 
| 598 | 634 |     ) -> float: | 
| 599 | 635 |         max_retries = options.get_max_retries(self.max_retries) | 
| 600 |  | -        try: | 
| 601 |  | -            # About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After | 
| 602 |  | -            # | 
| 603 |  | -            # <http-date>". See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax for | 
| 604 |  | -            # details. | 
| 605 |  | -            if response_headers is not None: | 
| 606 |  | -                retry_header = response_headers.get("retry-after") | 
| 607 |  | -                try: | 
| 608 |  | -                    # note: the spec indicates that this should only ever be an integer | 
| 609 |  | -                    # but if someone sends a float there's no reason for us to not respect it | 
| 610 |  | -                    retry_after = float(retry_header) | 
| 611 |  | -                except Exception: | 
| 612 |  | -                    retry_date_tuple = email.utils.parsedate_tz(retry_header) | 
| 613 |  | -                    if retry_date_tuple is None: | 
| 614 |  | -                        retry_after = -1 | 
| 615 |  | -                    else: | 
| 616 |  | -                        retry_date = email.utils.mktime_tz(retry_date_tuple) | 
| 617 |  | -                        retry_after = int(retry_date - time.time()) | 
| 618 |  | -            else: | 
| 619 |  | -                retry_after = -1 | 
| 620 |  | - | 
| 621 |  | -        except Exception: | 
| 622 |  | -            retry_after = -1 | 
| 623 | 636 | 
 | 
| 624 | 637 |         # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. | 
| 625 |  | -        if 0 < retry_after <= 60: | 
|  | 638 | +        retry_after = self._parse_retry_after_header(response_headers) | 
|  | 639 | +        if retry_after is not None and 0 < retry_after <= 60: | 
| 626 | 640 |             return retry_after | 
| 627 | 641 | 
 | 
| 628 |  | -        initial_retry_delay = 0.5 | 
| 629 |  | -        max_retry_delay = 8.0 | 
| 630 | 642 |         nb_retries = max_retries - remaining_retries | 
| 631 | 643 | 
 | 
| 632 | 644 |         # Apply exponential backoff, but not more than the max. | 
| 633 |  | -        sleep_seconds = min(initial_retry_delay * pow(2.0, nb_retries), max_retry_delay) | 
|  | 645 | +        sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY) | 
| 634 | 646 | 
 | 
| 635 | 647 |         # Apply some jitter, plus-or-minus half a second. | 
| 636 | 648 |         jitter = 1 - 0.25 * random() | 
|  | 
0 commit comments