Mercurial > p > roundup > code
comparison roundup/cgi/client.py @ 8185:e84d4585b16d
fix(web): issue2551356. Add etag header for not-modified (304) request.
When a 304 is returned to a conditional request for a static file,
print an ETag for the response.
ETag was always sent with a 200 response.
This also adds initial support for if-none-match conditional requests
for static files.
Changes:
Refactors the if-modified-since code out to a method.
It moves a file stat call from serve_static_file to _serve_file
so that an etag can be generated by both serve_static_file and
serve_file which call _serve_file.
Tests added. This does not test the codepath where serve_file pulls
content from the database rather than from a local file on disk.
Test mocking _serve_file changed to account for 5th argument to serve_file
BREAKING CHANGE:
function signature for client.py-Client::_serve_file() now has 5 not 4
parameters (added etag param). Since this is a "hidden" method I am
not too worried about it.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Tue, 10 Dec 2024 16:06:13 -0500 |
| parents | bd628e64725f |
| children | b938fd5223ae |
comparison
equal
deleted
inserted
replaced
| 8184:53dba022d4cd | 8185:e84d4585b16d |
|---|---|
| 1947 if not filename: | 1947 if not filename: |
| 1948 content = klass.get(nodeid, 'content') | 1948 content = klass.get(nodeid, 'content') |
| 1949 | 1949 |
| 1950 lmt = klass.get(nodeid, 'activity').timestamp() | 1950 lmt = klass.get(nodeid, 'activity').timestamp() |
| 1951 | 1951 |
| 1952 self._serve_file(lmt, mime_type, content, filename) | 1952 self._serve_file(lmt, None, mime_type, content, filename) |
| 1953 | 1953 |
| 1954 def serve_static_file(self, file): | 1954 def serve_static_file(self, file): |
| 1955 """ Serve up the file named from the templates dir | 1955 """ Serve up the file named from the templates dir |
| 1956 """ | 1956 """ |
| 1957 # figure the filename - try STATIC_FILES, then TEMPLATES dir | 1957 # figure the filename - try STATIC_FILES, then TEMPLATES dir |
| 1987 break | 1987 break |
| 1988 | 1988 |
| 1989 if filename is None: # we didn't find a filename | 1989 if filename is None: # we didn't find a filename |
| 1990 raise NotFound(file) | 1990 raise NotFound(file) |
| 1991 | 1991 |
| 1992 # last-modified time | |
| 1993 lmt = os.stat(filename)[stat.ST_MTIME] | |
| 1994 | |
| 1995 # detemine meta-type | 1992 # detemine meta-type |
| 1996 file = str(file) | 1993 file = str(file) |
| 1997 mime_type = mimetypes.guess_type(file)[0] | 1994 mime_type = mimetypes.guess_type(file)[0] |
| 1998 if not mime_type: | 1995 if not mime_type: |
| 1999 mime_type = 'text/css' if file.endswith('.css') else 'text/plain' | 1996 mime_type = 'text/css' if file.endswith('.css') else 'text/plain' |
| 2007 self.Cache_Control[fn] | 2004 self.Cache_Control[fn] |
| 2008 elif mime_type in self.Cache_Control: | 2005 elif mime_type in self.Cache_Control: |
| 2009 self.additional_headers['Cache-Control'] = \ | 2006 self.additional_headers['Cache-Control'] = \ |
| 2010 self.Cache_Control[mime_type] | 2007 self.Cache_Control[mime_type] |
| 2011 | 2008 |
| 2012 self._serve_file(lmt, mime_type, '', filename) | 2009 self._serve_file(None, None, mime_type, '', filename) |
| 2013 | 2010 |
| 2014 def _serve_file(self, lmt, mime_type, content=None, filename=None): | 2011 def _serve_file(self, lmt, etag, mime_type, content=None, filename=None): |
| 2015 """ guts of serve_file() and serve_static_file() | 2012 """guts of serve_file() and serve_static_file() |
| 2013 | |
| 2014 if lmt or etag are None, derive them from file filename. | |
| 2015 | |
| 2016 Handles if-modified-since and if-none-match etag | |
| 2017 conditional gets. | |
| 2018 | |
| 2019 It produces an raw etag header without encoding suffix. | |
| 2020 But it adds Accept-Encoding to the vary header. | |
| 2021 | |
| 2016 """ | 2022 """ |
| 2017 | 2023 if filename: |
| 2018 # spit out headers | 2024 stat_info = os.stat(filename) |
| 2019 self.additional_headers['Last-Modified'] = email.utils.formatdate(lmt, | 2025 |
| 2020 usegmt=True) | 2026 if lmt is None: |
| 2021 | 2027 # last-modified time |
| 2022 ims = None | 2028 lmt = stat_info[stat.ST_MTIME] |
| 2023 # see if there's an if-modified-since... | 2029 if etag is None: |
| 2024 # used if this is run behind a non-caching http proxy | 2030 # FIXME: maybe etag should depend on encoding. |
| 2025 if hasattr(self.request, 'headers'): | 2031 # it is an apache compatible etag without encoding. |
| 2026 ims = self.request.headers.get('if-modified-since') | 2032 etag = '"%x-%x-%x"' % (stat_info[stat.ST_INO], |
| 2027 elif 'HTTP_IF_MODIFIED_SINCE' in self.env: | 2033 stat_info[stat.ST_SIZE], |
| 2028 # cgi will put the header in the env var | 2034 stat_info[stat.ST_MTIME]) |
| 2029 ims = self.env['HTTP_IF_MODIFIED_SINCE'] | 2035 |
| 2030 if ims: | 2036 # spit out headers for conditional request |
| 2031 ims = email.utils.parsedate(ims)[:6] | 2037 self.setHeader("ETag", etag) |
| 2032 lmtt = time.gmtime(lmt)[:6] | 2038 self.additional_headers['Last-Modified'] = \ |
| 2033 if lmtt <= ims: | 2039 email.utils.formatdate(lmt, usegmt=True) |
| 2040 | |
| 2041 inm = None | |
| 2042 # ETag is a more strict check than modified date. Use etag | |
| 2043 # check if available. Skip testing modified data. | |
| 2044 if hasattr(self.request, 'headers'): | |
| 2045 inm = self.request.headers.get('if-none-match') | |
| 2046 elif 'HTTP_IF_NONE_MATCH' in self.env: | |
| 2047 # maybe the cgi will put the header in the env var | |
| 2048 inm = self.env['HTTP_ETAG'] | |
| 2049 if inm and etag == inm: | |
| 2050 # because we can compress, always set Accept-Encoding | |
| 2051 # value. Otherwise caches can serve up the wrong info | |
| 2052 # if their cached copy has no compression. | |
| 2053 self.setVary("Accept-Encoding") | |
| 2054 ''' | |
| 2055 to solve issue2551356 I may need to determine | |
| 2056 the content encoding. | |
| 2057 if (self.determine_content_encoding()): | |
| 2058 ''' | |
| 2059 raise NotModified | |
| 2060 | |
| 2061 if self.if_not_modified_since(lmt): | |
| 2034 # because we can compress, always set Accept-Encoding | 2062 # because we can compress, always set Accept-Encoding |
| 2035 # value. Otherwise caches can serve up the wrong info | 2063 # value. Otherwise caches can serve up the wrong info |
| 2036 # if their cached copy has no compression. | 2064 # if their cached copy has no compression. |
| 2037 self.setVary("Accept-Encoding") | 2065 self.setVary("Accept-Encoding") |
| 2038 ''' | 2066 ''' |
| 2048 if filename: | 2076 if filename: |
| 2049 self.write_file(filename) | 2077 self.write_file(filename) |
| 2050 else: | 2078 else: |
| 2051 self.additional_headers['Content-Length'] = str(len(content)) | 2079 self.additional_headers['Content-Length'] = str(len(content)) |
| 2052 self.write(content) | 2080 self.write(content) |
| 2081 | |
| 2082 def if_not_modified_since(self, lmt): | |
| 2083 ims = None | |
| 2084 # see if there's an if-modified-since... | |
| 2085 if hasattr(self.request, 'headers'): | |
| 2086 ims = self.request.headers.get('if-modified-since') | |
| 2087 elif 'HTTP_IF_MODIFIED_SINCE' in self.env: | |
| 2088 # cgi will put the header in the env var | |
| 2089 ims = self.env['HTTP_IF_MODIFIED_SINCE'] | |
| 2090 | |
| 2091 if ims: | |
| 2092 datestamp = email.utils.parsedate(ims) | |
| 2093 if datestamp is not None: | |
| 2094 ims = datestamp[:6] | |
| 2095 else: | |
| 2096 # set to beginning of time so whole file will be sent | |
| 2097 ims = (0, 0, 0, 0, 0, 0) | |
| 2098 lmtt = time.gmtime(lmt)[:6] | |
| 2099 return lmtt <= ims | |
| 2100 | |
| 2101 return False | |
| 2053 | 2102 |
| 2054 def send_error_to_admin(self, subject, html, txt): | 2103 def send_error_to_admin(self, subject, html, txt): |
| 2055 """Send traceback information to admin via email. | 2104 """Send traceback information to admin via email. |
| 2056 We send both, the formatted html (with more information) and | 2105 We send both, the formatted html (with more information) and |
| 2057 the text version of the traceback. We use | 2106 the text version of the traceback. We use |
