-
Notifications
You must be signed in to change notification settings - Fork 110
ISSUE-627: (feature/retry-after-sync) Add handling of Retry-After header for 429 and 503 status codes for sync requests #628
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
temsocial
wants to merge
12
commits into
python-caldav:master
from
temsocial:feature/retry-after-sync
Closed
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
e7cad4c
chore: add ratelimit error class
temsocial e34ebb5
feat(retry-after support): add retry-after for sync requests
temsocial a9ada16
chor: add retry_after_seconds in RateLimit error class
temsocial 37fa3ae
chore: devide source retry-after and parsed int value
temsocial a3d231a
chore: fix ruff errs
asoloukhin-cmeet 80ce80b
chore: fix DAVError
temsocial 290d696
chore: add tobixen ideas
temsocial 457a739
chore: fix max2min sleep
temsocial e65ed19
chore: add if self.rate_limit_max_sleep
temsocial 5919c77
chore: fix ruff styles
temsocial 119add5
chore: retry in case only 429 http_code without headers
temsocial 22b79ce
chore: fix comments
temsocial File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,10 @@ | |
|
|
||
| import logging | ||
| import sys | ||
| import time | ||
| import warnings | ||
| from datetime import datetime, timezone | ||
| from email.utils import parsedate_to_datetime | ||
| from types import TracebackType | ||
| from typing import TYPE_CHECKING, Any, Optional | ||
| from urllib.parse import unquote | ||
|
|
@@ -206,6 +209,9 @@ def __init__( | |
| features: FeatureSet | dict | str = None, | ||
| enable_rfc6764: bool = True, | ||
| require_tls: bool = True, | ||
| rate_limit_handle: bool = False, | ||
| rate_limit_default_sleep: int = None, | ||
| rate_limit_max_sleep: int = None, | ||
| ) -> None: | ||
| """ | ||
| Sets up a HTTPConnection object towards the server in the url. | ||
|
|
@@ -243,6 +249,13 @@ def __init__( | |
| redirect to unencrypted HTTP. Set to False ONLY if you need to | ||
| support non-TLS servers and trust your DNS infrastructure. | ||
| This parameter has no effect if enable_rfc6764=False. | ||
| rate_limit_handle: boolean, a parameter that determines whether the rate limit response | ||
| should be handled. Default: False. | ||
| rate_limit_default_sleep: integer, the default number of seconds to sleep if the server | ||
| response cannot be parsed, or if no retry-after is specified | ||
| and the HTTP response status code is 429. Default: None. | ||
| rate_limit_max_sleep: integer, the maximum number of seconds the script will sleep | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here, |
||
| when encountering a rate limit. Default: None. | ||
|
|
||
| The niquests library will honor a .netrc-file, if such a file exists | ||
| username and password may be omitted. | ||
|
|
@@ -341,6 +354,10 @@ def __init__( | |
|
|
||
| self._principal = None | ||
|
|
||
| self.rate_limit_handle = rate_limit_handle | ||
| self.rate_limit_default_sleep = rate_limit_default_sleep | ||
| self.rate_limit_max_sleep = rate_limit_max_sleep | ||
|
|
||
| def __enter__(self) -> Self: | ||
| ## Used for tests, to set up a temporarily test server | ||
| if hasattr(self, "setup"): | ||
|
|
@@ -931,7 +948,21 @@ def request( | |
| Returns: | ||
| DAVResponse | ||
| """ | ||
| return self._sync_request(url, method, body, headers) | ||
| try: | ||
| return self._sync_request(url, method, body, headers) | ||
| except error.RateLimitError as e: | ||
| if self.rate_limit_handle: | ||
| retry_after_seconds = self.rate_limit_default_sleep | ||
| if e.retry_after_seconds is not None: | ||
| retry_after_seconds = e.retry_after_seconds | ||
| if self.rate_limit_max_sleep: | ||
| retry_after_seconds = min(retry_after_seconds or 0, self.rate_limit_max_sleep) | ||
| if retry_after_seconds <= 0: | ||
| raise e | ||
| time.sleep(retry_after_seconds) | ||
| return self._sync_request(url, method, body, headers) | ||
|
|
||
| raise e | ||
|
|
||
| def _sync_request( | ||
| self, | ||
|
|
@@ -974,8 +1005,32 @@ def _sync_request( | |
| cert=self.ssl_cert, | ||
| ) | ||
|
|
||
| # Handle 401 responses for auth negotiation | ||
| r_headers = CaseInsensitiveDict(r.headers) | ||
|
|
||
| # Handle 429, 503 responses for retry negotiation | ||
| if r.status_code in (429, 503): | ||
| retry_after_header: Optional[str] = r_headers.get("Retry-After") | ||
| retry_after_value: Optional[str] = None | ||
| retry_seconds: Optional[float] = None | ||
| if retry_after_header: | ||
| retry_after_value = retry_after_header | ||
| try: | ||
| retry_seconds = int(retry_after_header) | ||
| except ValueError: | ||
| try: | ||
| retry_date = parsedate_to_datetime(retry_after) | ||
| now = datetime.now(timezone.utc) | ||
| retry_seconds = max(0, (retry_date - now).total_seconds()) | ||
| except: | ||
| pass | ||
| if r.status_code == 429 or retry_after_header is not None: | ||
| raise error.RateLimitError( | ||
| f"Rate limited or service unavailable. Retry after: {retry_after}", | ||
| retry_after=retry_after, | ||
| retry_after_seconds=retry_seconds, | ||
| ) | ||
|
|
||
| # Handle 401 responses for auth negotiation | ||
| if ( | ||
| r.status_code == 401 | ||
| and "WWW-Authenticate" in r_headers | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So if I understand it right,
Nonehere will cause it to raise theRateLimitErroron 429s withoutretry-after, this should be explicit in the inline documentation here.