Skip to content

Commit 08be136

Browse files
committed
Include HTTP 308 as a permanent redirect.
The new status code was introduced with RFC 7538 in April 2015. This makes it so that "308 Permanent Redirect" status codes are treated the same as "301 Moved Permanently" statuses.
1 parent 12f8a4b commit 08be136

File tree

2 files changed

+17
-14
lines changed

2 files changed

+17
-14
lines changed

cachecontrol/controller.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?")
1919

20+
PERMANENT_REDIRECT_STATUSES = (301, 308)
21+
2022

2123
def parse_uri(uri):
2224
"""Parses a URI using the regex given in Appendix B of RFC 3986.
@@ -37,7 +39,7 @@ def __init__(
3739
self.cache = DictCache() if cache is None else cache
3840
self.cache_etags = cache_etags
3941
self.serializer = serializer or Serializer()
40-
self.cacheable_status_codes = status_codes or (200, 203, 300, 301)
42+
self.cacheable_status_codes = status_codes or (200, 203, 300, 301, 308)
4143

4244
@classmethod
4345
def _urlnorm(cls, uri):
@@ -147,17 +149,18 @@ def cached_request(self, request):
147149
logger.warning("Cache entry deserialization failed, entry ignored")
148150
return False
149151

150-
# If we have a cached 301, return it immediately. We don't
151-
# need to test our response for other headers b/c it is
152+
# If we have a cached permanent redirect, return it immediately. We
153+
# don't need to test our response for other headers b/c it is
152154
# intrinsically "cacheable" as it is Permanent.
155+
#
153156
# See:
154157
# https://tools.ietf.org/html/rfc7231#section-6.4.2
155158
#
156159
# Client can try to refresh the value by repeating the request
157160
# with cache busting headers as usual (ie no-cache).
158-
if resp.status == 301:
161+
if int(resp.status) in PERMANENT_REDIRECT_STATUSES:
159162
msg = (
160-
'Returning cached "301 Moved Permanently" response '
163+
'Returning cached permanent redirect response '
161164
"(ignoring date and etag information)"
162165
)
163166
logger.debug(msg)
@@ -309,9 +312,9 @@ def cache_response(self, request, response, body=None, status_codes=None):
309312
cache_url, self.serializer.dumps(request, response, body=body)
310313
)
311314

312-
# Add to the cache any 301s. We do this before looking that
313-
# the Date headers.
314-
elif response.status == 301:
315+
# Add to the cache any permanent redirects. We do this before looking
316+
# that the Date headers.
317+
elif int(response.status) in PERMANENT_REDIRECT_STATUSES:
315318
logger.debug("Caching permanent redirect")
316319
self.cache.set(cache_url, self.serializer.dumps(request, response))
317320

tests/test_cache_control.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def req(self, headers):
152152
return self.c.cached_request(mock_request)
153153

154154
def test_cache_request_no_headers(self):
155-
cached_resp = Mock(headers={"ETag": "jfd9094r808", "Content-Length": 100})
155+
cached_resp = Mock(headers={"ETag": "jfd9094r808", "Content-Length": 100}, status=200)
156156
self.c.cache = DictCache({self.url: cached_resp})
157157
resp = self.req({})
158158
assert not resp
@@ -179,7 +179,7 @@ def test_cache_request_not_in_cache(self):
179179

180180
def test_cache_request_fresh_max_age(self):
181181
now = time.strftime(TIME_FMT, time.gmtime())
182-
resp = Mock(headers={"cache-control": "max-age=3600", "date": now})
182+
resp = Mock(headers={"cache-control": "max-age=3600", "date": now}, status=200)
183183

184184
cache = DictCache({self.url: resp})
185185
self.c.cache = cache
@@ -189,7 +189,7 @@ def test_cache_request_fresh_max_age(self):
189189
def test_cache_request_unfresh_max_age(self):
190190
earlier = time.time() - 3700 # epoch - 1h01m40s
191191
now = time.strftime(TIME_FMT, time.gmtime(earlier))
192-
resp = Mock(headers={"cache-control": "max-age=3600", "date": now})
192+
resp = Mock(headers={"cache-control": "max-age=3600", "date": now}, status=200)
193193
self.c.cache = DictCache({self.url: resp})
194194
r = self.req({})
195195
assert not r
@@ -198,7 +198,7 @@ def test_cache_request_fresh_expires(self):
198198
later = time.time() + 86400 # GMT + 1 day
199199
expires = time.strftime(TIME_FMT, time.gmtime(later))
200200
now = time.strftime(TIME_FMT, time.gmtime())
201-
resp = Mock(headers={"expires": expires, "date": now})
201+
resp = Mock(headers={"expires": expires, "date": now}, status=200)
202202
cache = DictCache({self.url: resp})
203203
self.c.cache = cache
204204
r = self.req({})
@@ -208,7 +208,7 @@ def test_cache_request_unfresh_expires(self):
208208
sooner = time.time() - 86400 # GMT - 1 day
209209
expires = time.strftime(TIME_FMT, time.gmtime(sooner))
210210
now = time.strftime(TIME_FMT, time.gmtime())
211-
resp = Mock(headers={"expires": expires, "date": now})
211+
resp = Mock(headers={"expires": expires, "date": now}, status=200)
212212
cache = DictCache({self.url: resp})
213213
self.c.cache = cache
214214
r = self.req({})
@@ -217,7 +217,7 @@ def test_cache_request_unfresh_expires(self):
217217
def test_cached_request_with_bad_max_age_headers_not_returned(self):
218218
now = time.strftime(TIME_FMT, time.gmtime())
219219
# Not a valid header; this would be from a misconfigured server
220-
resp = Mock(headers={"cache-control": "max-age=xxx", "date": now})
220+
resp = Mock(headers={"cache-control": "max-age=xxx", "date": now}, status=200)
221221

222222
self.c.cache = DictCache({self.url: resp})
223223

0 commit comments

Comments
 (0)