diff --git a/django/utils/text.py b/django/utils/text.py index a77f27eed7..ec7af5389c 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -293,7 +293,7 @@ def phone2numeric(phone): # Used with permission. def compress_string(s): zbuf = BytesIO() - with GzipFile(mode='wb', compresslevel=6, fileobj=zbuf) as zfile: + with GzipFile(mode='wb', compresslevel=6, fileobj=zbuf, mtime=0) as zfile: zfile.write(s) return zbuf.getvalue() @@ -322,7 +322,7 @@ class StreamingBuffer(object): # Like compress_string, but for iterators of strings. def compress_sequence(sequence): buf = StreamingBuffer() - with GzipFile(mode='wb', compresslevel=6, fileobj=buf) as zfile: + with GzipFile(mode='wb', compresslevel=6, fileobj=buf, mtime=0) as zfile: # Output headers... yield buf.read() for item in sequence: diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index db8ed6ea90..12109b3137 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -770,6 +770,12 @@ class GZipMiddlewareTest(SimpleTestCase): with gzip.GzipFile(mode='rb', fileobj=BytesIO(gzipped_string)) as f: return f.read() + @staticmethod + def get_mtime(gzipped_string): + with gzip.GzipFile(mode='rb', fileobj=BytesIO(gzipped_string)) as f: + f.read() # must read the data before accessing the header + return f.mtime + def test_compress_response(self): """ Compression is performed on responses with compressible content. @@ -850,6 +856,20 @@ class GZipMiddlewareTest(SimpleTestCase): self.assertEqual(r.content, self.incompressible_string) self.assertIsNone(r.get('Content-Encoding')) + def test_compress_deterministic(self): + """ + Compression results are the same for the same content and don't + include a modification time (since that would make the results + of compression non-deterministic and prevent + ConditionalGetMiddleware from recognizing conditional matches + on gzipped content). + """ + r1 = GZipMiddleware().process_response(self.req, self.resp) + r2 = GZipMiddleware().process_response(self.req, self.resp) + self.assertEqual(r1.content, r2.content) + self.assertEqual(self.get_mtime(r1.content), 0) + self.assertEqual(self.get_mtime(r2.content), 0) + @ignore_warnings(category=RemovedInDjango21Warning) @override_settings(USE_ETAGS=True)