Skip to content

Commit e79b4d2

Browse files
committed
fix: make If-None-Match work for static file (@@file) case
Found by Redbot testing.
1 parent d0e5f21 commit e79b4d2

File tree

3 files changed

+117
-1
lines changed

3 files changed

+117
-1
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ Fixed:
164164
- Send Vary: Accept-Encoding on any file that could be compressed
165165
even if the file is not encoded/compressed. Found by Redbot
166166
testing. (John Rouillard)
167+
- make If-None-Match work for static file (@@file) case. Found by
168+
Redbot testing (John Rouillard)
167169

168170
Features:
169171

roundup/cgi/client.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2716,10 +2716,32 @@ def write_file(self, filename):
27162716
# Hence the intermediate proxy should/must match
27172717
# Accept-Encoding and ETag to determine whether to return
27182718
# a 304 or report cache miss and fetch from origin server.
2719+
#
2720+
# RFC 9110 8.8.3.3 shows a different strong entity tag
2721+
# generated for gzip and non gzip replies.
27192722
etag = '"%x-%x-%x"' % (stat_info[stat.ST_INO],
27202723
length,
27212724
stat_info[stat.ST_MTIME])
27222725
self.setHeader("ETag", etag)
2726+
2727+
inm = self.request.headers.get('If-None-Match')
2728+
if (inm):
2729+
inm_etags = inm.split(',')
2730+
inm_etags = [tag.strip() for tag in inm_etags]
2731+
if etag in inm_etags:
2732+
self.setHeader('ETag', etag)
2733+
self.setVary('Accept-Encoding')
2734+
raise NotModified
2735+
2736+
# need to check for etag-compression_code:
2737+
# a41932-8b5-664ce93d-zstd or a41932-8b5-664ce93d-gzip
2738+
tag_prefix = etag[:-1] + '-'
2739+
for inm_etag in inm_etags:
2740+
if inm_etag.startswith(tag_prefix):
2741+
self.setHeader('ETag', inm_etag)
2742+
self.setVary('Accept-Encoding')
2743+
raise NotModified
2744+
27232745
# RFC 2616 14.5: Accept-Ranges
27242746
#
27252747
# Let the client know that we will accept range requests.

test/test_liveserver.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -707,9 +707,101 @@ def test_rest_endpoint_user_roles(self):
707707

708708
self.assertEqual(3, len(json.loads(f.content)['data']['collection']))
709709

710+
def test_inm(self):
711+
'''retrieve the user_utils.js file without an if-none-match etag
712+
header, a bad if-none-match header and valid single and
713+
multiple values.
714+
'''
715+
f = requests.get(self.url_base() + '/@@file/user_utils.js',
716+
headers = { 'Accept-Encoding': 'gzip, foo',
717+
'Accept': '*/*'})
718+
print(f.status_code)
719+
print(f.headers)
720+
721+
self.assertEqual(f.status_code, 200)
722+
expected = { 'Content-Type': self.js_mime_type,
723+
'Content-Encoding': 'gzip',
724+
'Vary': 'Accept-Encoding',
725+
}
726+
727+
# use dict comprehension to remove fields like date,
728+
# etag etc. from f.headers.
729+
self.assertDictEqual({ key: value for (key, value) in
730+
f.headers.items() if key in expected },
731+
expected)
732+
733+
# use etag in previous response
734+
etag = f.headers['etag']
735+
f = requests.get(self.url_base() + '/@@file/user_utils.js',
736+
headers = { 'Accept-Encoding': 'gzip, foo',
737+
'If-None-Match': etag,
738+
'Accept': '*/*'})
739+
print(f.status_code)
740+
print(f.headers)
741+
742+
self.assertEqual(f.status_code, 304)
743+
expected = { 'Vary': 'Accept-Encoding',
744+
'Content-Length': '0',
745+
'ETag': etag,
746+
'Vary': 'Accept-Encoding'
747+
}
748+
749+
# use dict comprehension to remove fields like date, server,
750+
# etc. from f.headers.
751+
self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
752+
753+
# test again with etag supplied w/o content-encoding
754+
# and multiple etags
755+
self.assertTrue(etag.endswith('-gzip"'))
756+
757+
# keep etag intact. Used below.
758+
base_etag = etag[:-6] + '"'
759+
760+
all_etags = (
761+
'"a41932-8b5-664ce93d", %s", "a41932-8b5-664ce93d-br"' %
762+
base_etag
763+
)
764+
765+
f = requests.get(self.url_base() + '/@@file/user_utils.js',
766+
headers = { 'Accept-Encoding': 'gzip, foo',
767+
'If-None-Match': base_etag,
768+
'Accept': '*/*'})
769+
print(f.status_code)
770+
print(f.headers)
771+
772+
self.assertEqual(f.status_code, 304)
773+
expected = { 'Vary': 'Accept-Encoding',
774+
'Content-Length': '0',
775+
'ETag': base_etag,
776+
'Vary': 'Accept-Encoding'
777+
}
778+
779+
# use dict comprehension to remove fields like date, server,
780+
# etc. from f.headers.
781+
self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
782+
783+
784+
# test with bad etag
785+
f = requests.get(self.url_base() + '/@@file/user_utils.js',
786+
headers = { 'Accept-Encoding': 'gzip, foo',
787+
'If-None-Match': '"a41932-8b5-664ce93d"',
788+
'Accept': '*/*'})
789+
print(f.status_code)
790+
print(f.headers)
791+
792+
self.assertEqual(f.status_code, 200)
793+
expected = { 'Content-Type': self.js_mime_type,
794+
'ETag': etag,
795+
'Content-Encoding': 'gzip',
796+
'Vary': 'Accept-Encoding',
797+
}
798+
799+
# use dict comprehension to remove fields like date, server,
800+
# etc. from f.headers.
801+
self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
710802

711803
def test_ims(self):
712-
''' retreive the user_utils.js file with old and new
804+
''' retrieve the user_utils.js file with old and new
713805
if-modified-since timestamps.
714806
'''
715807
from datetime import datetime

0 commit comments

Comments
 (0)