-
Notifications
You must be signed in to change notification settings - Fork 144
cache temporary redirects with explicit caching directives #405
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
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
|
|
||
| from requests.structures import CaseInsensitiveDict | ||
|
|
||
| from cachecontrol.heuristics import HEURISTICALLY_CACHEABLE_STATUSES | ||
| from cachecontrol.cache import DictCache, SeparateBodyBaseCache | ||
| from cachecontrol.serialize import Serializer | ||
|
|
||
|
|
@@ -33,6 +34,17 @@ | |
|
|
||
| URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?") | ||
|
|
||
| NEVER_CACHE_STATUSES = ( | ||
| # Per https://www.rfc-editor.org/rfc/rfc9111.html#section-3 the status | ||
| # code must be final | ||
| 100, | ||
| 101, | ||
| # From httplib2: Don't cache 206's since we aren't going to | ||
| # handle byte range requests | ||
| 206, | ||
| ) | ||
|
|
||
|
|
||
| PERMANENT_REDIRECT_STATUSES = (301, 308) | ||
|
|
||
|
|
||
|
|
@@ -60,7 +72,20 @@ def __init__( | |
| self.cache = DictCache() if cache is None else cache | ||
| self.cache_etags = cache_etags | ||
| self.serializer = serializer or Serializer() | ||
| self.cacheable_status_codes = status_codes or (200, 203, 300, 301, 308) | ||
| # Per https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.7.1 all | ||
| # all final response codes are potentially cacheable, subject to the | ||
| # other conditions. CacheController conservatively only caches common | ||
| # ones codes. For example, even with a max-age set, a 500 Internal | ||
|
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. Q: what does "common ones" mean here?
Contributor
Author
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. I've rephrased the comment to -- I hope! -- be clearer. The existing name My motivation for this change was that I'm working with a package index built around redirecting from a web a web server to cloud provider blob storage (ex: S3). I think this a reasonable and common design and I'm trying to make the smallest relaxation to the existing conservative rules that supports that. I think it's less likely that anyone has designed a system around the cacheable of something like "414 URI Too Long". (But if they did this would be the place in the code to look.) |
||
| # Server Error will not be cached | ||
| self.cacheable_status_codes = status_codes or ( | ||
| 200, | ||
| 203, | ||
| 300, | ||
| 301, | ||
| 302, | ||
| 307, | ||
| 308, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def _urlnorm(cls, uri: str) -> str: | ||
|
|
@@ -128,12 +153,12 @@ def parse_cache_control(self, headers: Mapping[str, str]) -> dict[str, int | Non | |
| except IndexError: | ||
| if required: | ||
| logger.debug( | ||
| "Missing value for cache-control " "directive: %s", | ||
| "Missing value for cache-control directive: %s", | ||
| directive, | ||
| ) | ||
| except ValueError: | ||
| logger.debug( | ||
| "Invalid value for cache-control directive " "%s, must be %s", | ||
| "Invalid value for cache-control directive %s, must be %s", | ||
| directive, | ||
| typ.__name__, | ||
| ) | ||
|
|
@@ -343,12 +368,11 @@ def cache_response( | |
| else: | ||
| response = response_or_ref | ||
|
|
||
| # From httplib2: Don't cache 206's since we aren't going to | ||
| # handle byte range requests | ||
| cacheable_status_codes = status_codes or self.cacheable_status_codes | ||
| if response.status not in cacheable_status_codes: | ||
| if response.status in NEVER_CACHE_STATUSES: | ||
| logger.debug( | ||
| "Status code %s not in %s", response.status, cacheable_status_codes | ||
| "Status code %s in never cache set %s", | ||
| response.status, | ||
| NEVER_CACHE_STATUSES, | ||
| ) | ||
| return | ||
|
|
||
|
|
@@ -378,6 +402,30 @@ def cache_response( | |
| cc_req = self.parse_cache_control(request.headers) | ||
| cc = self.parse_cache_control(response_headers) | ||
|
|
||
| # "at least one of the following" from | ||
| # https://www.rfc-editor.org/rfc/rfc9111.html#section-3-2.7.1 | ||
| has_explicit_freshness = ( | ||
| "public" in cc or "expires" in response_headers or "max-age" in cc | ||
| # NOTE: s-maxage is also listed in the RFC section, but | ||
| # cache_response() doesn't currently express the concept of shared | ||
| # vs private caching | ||
| ) | ||
| cacheable_status_codes = status_codes or self.cacheable_status_codes | ||
| if response.status not in cacheable_status_codes: | ||
| logger.debug( | ||
| "Status code %s not in %s", response.status, cacheable_status_codes | ||
| ) | ||
| return | ||
| if ( | ||
| response.status not in HEURISTICALLY_CACHEABLE_STATUSES | ||
| and not has_explicit_freshness | ||
| ): | ||
| logger.debug( | ||
| "Status code %s is not heuristically cacheable and no explicit caching headers are set", | ||
| response.status, | ||
| ) | ||
| return | ||
|
|
||
| assert request.url is not None | ||
| cache_url = self.cache_url(request.url) | ||
| logger.debug('Updating cache with response from "%s"', cache_url) | ||
|
|
||
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.
Nit: let's make this and the other constants introduced by this PR private, since we're not committing to them as new public APIs 🙂
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.
(We do already have some public constants, which is unfortunate. So I'm not 100% opposed to making these public if you have an argument for it, but by default I'd prefer not to make the public API surface any bigger.)
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.
_prefixedNEVER_CACHE_STATUSESHEURISTICALLY_CACHEABLE_STATUSESthis is replacingLastModified.cacheable_by_default_statuseswhich didn't have a_so I think that very slight leads towards leavingHEURISTICALLY_CACHEABLE_STATUSESwithout one. Happy to prefix that one as well if you prefer.