changeset 8020:60c98a8a23bd

fix: make If-None-Match work for static file (@@file) case Found by Redbot testing.
author John Rouillard <rouilj@ieee.org>
date Sun, 02 Jun 2024 20:35:47 -0400
parents 16cc72cd9c17
children 98429efb80cb
files CHANGES.txt roundup/cgi/client.py test/test_liveserver.py
diffstat 3 files changed, 117 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.txt	Sun Jun 02 18:22:53 2024 -0400
+++ b/CHANGES.txt	Sun Jun 02 20:35:47 2024 -0400
@@ -164,6 +164,8 @@
 - Send Vary: Accept-Encoding on any file that could be compressed
   even if the file is not encoded/compressed. Found by Redbot
   testing. (John Rouillard)
+- make If-None-Match work for static file (@@file) case. Found by
+  Redbot testing (John Rouillard)
 
 Features:
 
--- a/roundup/cgi/client.py	Sun Jun 02 18:22:53 2024 -0400
+++ b/roundup/cgi/client.py	Sun Jun 02 20:35:47 2024 -0400
@@ -2716,10 +2716,32 @@
             # Hence the intermediate proxy should/must match
             # Accept-Encoding and ETag to determine whether to return
             # a 304 or report cache miss and fetch from origin server.
+            #
+            # RFC 9110 8.8.3.3 shows a different strong entity tag
+            # generated for gzip and non gzip replies.
             etag = '"%x-%x-%x"' % (stat_info[stat.ST_INO],
                                    length,
                                    stat_info[stat.ST_MTIME])
             self.setHeader("ETag", etag)
+
+            inm = self.request.headers.get('If-None-Match')
+            if (inm):
+                inm_etags = inm.split(',')
+                inm_etags = [tag.strip() for tag in inm_etags]
+                if etag in inm_etags:
+                    self.setHeader('ETag', etag)
+                    self.setVary('Accept-Encoding')
+                    raise NotModified
+
+                # need to check for etag-compression_code:
+                # a41932-8b5-664ce93d-zstd or a41932-8b5-664ce93d-gzip
+                tag_prefix = etag[:-1] + '-'
+                for inm_etag in inm_etags:
+                    if inm_etag.startswith(tag_prefix):
+                        self.setHeader('ETag', inm_etag)
+                        self.setVary('Accept-Encoding')
+                        raise NotModified
+
             # RFC 2616 14.5: Accept-Ranges
             #
             # Let the client know that we will accept range requests.
--- a/test/test_liveserver.py	Sun Jun 02 18:22:53 2024 -0400
+++ b/test/test_liveserver.py	Sun Jun 02 20:35:47 2024 -0400
@@ -707,9 +707,101 @@
 
         self.assertEqual(3, len(json.loads(f.content)['data']['collection']))
 
+    def test_inm(self):
+        '''retrieve the user_utils.js file without an if-none-match etag
+            header, a bad if-none-match header and valid single and
+            multiple values.
+        '''
+        f = requests.get(self.url_base() + '/@@file/user_utils.js',
+                         headers = { 'Accept-Encoding': 'gzip, foo',
+                                     'Accept': '*/*'})
+        print(f.status_code)
+        print(f.headers)
+
+        self.assertEqual(f.status_code, 200)
+        expected = { 'Content-Type': self.js_mime_type,
+                     'Content-Encoding': 'gzip',
+                     'Vary': 'Accept-Encoding',
+        }
+
+        # use dict comprehension to remove fields like date,
+        # etag etc. from f.headers.
+        self.assertDictEqual({ key: value for (key, value) in
+                               f.headers.items() if key in expected },
+                             expected)
+
+        # use etag in previous response
+        etag = f.headers['etag']
+        f = requests.get(self.url_base() + '/@@file/user_utils.js',
+                         headers = { 'Accept-Encoding': 'gzip, foo',
+                                     'If-None-Match': etag,
+                                     'Accept': '*/*'})
+        print(f.status_code)
+        print(f.headers)
+
+        self.assertEqual(f.status_code, 304)
+        expected = { 'Vary': 'Accept-Encoding',
+                     'Content-Length': '0',
+                     'ETag': etag,
+                     'Vary': 'Accept-Encoding'
+        }
+
+        # use dict comprehension to remove fields like date, server,
+        # etc. from f.headers.
+        self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
+
+        # test again with etag supplied w/o content-encoding
+        # and multiple etags
+        self.assertTrue(etag.endswith('-gzip"'))
+
+        # keep etag intact. Used below.
+        base_etag = etag[:-6] + '"'
+
+        all_etags = (
+            '"a41932-8b5-664ce93d", %s", "a41932-8b5-664ce93d-br"' %
+            base_etag
+        )
+
+        f = requests.get(self.url_base() + '/@@file/user_utils.js',
+                         headers = { 'Accept-Encoding': 'gzip, foo',
+                                     'If-None-Match': base_etag,
+                                     'Accept': '*/*'})
+        print(f.status_code)
+        print(f.headers)
+
+        self.assertEqual(f.status_code, 304)
+        expected = { 'Vary': 'Accept-Encoding',
+                     'Content-Length': '0',
+                     'ETag': base_etag,
+                     'Vary': 'Accept-Encoding'
+        }
+
+        # use dict comprehension to remove fields like date, server,
+        # etc. from f.headers.
+        self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
+
+
+        # test with bad etag
+        f = requests.get(self.url_base() + '/@@file/user_utils.js',
+                         headers = { 'Accept-Encoding': 'gzip, foo',
+                                     'If-None-Match': '"a41932-8b5-664ce93d"',
+                                     'Accept': '*/*'})
+        print(f.status_code)
+        print(f.headers)
+
+        self.assertEqual(f.status_code, 200)
+        expected = { 'Content-Type': self.js_mime_type,
+                     'ETag': etag,
+                     'Content-Encoding': 'gzip',
+                     'Vary': 'Accept-Encoding',
+        }
+
+        # use dict comprehension to remove fields like date, server,
+        # etc. from f.headers.
+        self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
 
     def test_ims(self):
-        ''' retreive the user_utils.js file with old and new
+        ''' retrieve the user_utils.js file with old and new
             if-modified-since timestamps.
         '''
         from datetime import datetime

Roundup Issue Tracker: http://roundup-tracker.org/