diff --git a/django/middleware/gzip.py b/django/middleware/gzip.py index 642e3188d3..d6e2134f17 100644 --- a/django/middleware/gzip.py +++ b/django/middleware/gzip.py @@ -41,8 +41,12 @@ class GZipMiddleware(MiddlewareMixin): response.content = compressed_content response['Content-Length'] = str(len(response.content)) - if response.has_header('ETag'): - response['ETag'] = re.sub('"$', ';gzip"', response['ETag']) + # If there is a strong ETag, make it weak to fulfill the requirements + # of RFC 7232 section-2.1 while also allowing conditional request + # matches on ETags. + etag = response.get('ETag') + if etag and etag.startswith('"'): + response['ETag'] = 'W/' + etag response['Content-Encoding'] = 'gzip' return response diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index 432f5437ff..d007d252f5 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -155,6 +155,9 @@ It will NOT compress content if any of the following are true: * The request (the browser) hasn't sent an ``Accept-Encoding`` header containing ``gzip``. +If the response has an ``ETag`` header, the ETag is made weak to comply with +:rfc:`7232#section-2.1`. + You can apply GZip compression to individual views using the :func:`~django.views.decorators.gzip.gzip_page()` decorator. diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index a9cb1f31ea..db8ed6ea90 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -855,27 +855,44 @@ class GZipMiddlewareTest(SimpleTestCase): @override_settings(USE_ETAGS=True) class ETagGZipMiddlewareTest(SimpleTestCase): """ - Tests if the ETagMiddleware behaves correctly with GZipMiddleware. + ETags are handled properly by GZipMiddleware. """ rf = RequestFactory() compressible_string = b'a' * 500 - def test_compress_response(self): + def test_strong_etag_modified(self): """ - ETag is changed after gzip compression is performed. + GZipMiddleware makes a strong ETag weak. + """ + request = self.rf.get('/', HTTP_ACCEPT_ENCODING='gzip, deflate') + response = HttpResponse(self.compressible_string) + response['ETag'] = '"eggs"' + gzip_response = GZipMiddleware().process_response(request, response) + self.assertEqual(gzip_response['ETag'], 'W/"eggs"') + + def test_weak_etag_not_modified(self): + """ + GZipMiddleware doesn't modify a weak ETag. + """ + request = self.rf.get('/', HTTP_ACCEPT_ENCODING='gzip, deflate') + response = HttpResponse(self.compressible_string) + response['ETag'] = 'W/"eggs"' + gzip_response = GZipMiddleware().process_response(request, response) + self.assertEqual(gzip_response['ETag'], 'W/"eggs"') + + def test_etag_match(self): + """ + GZipMiddleware allows 304 Not Modified responses. """ request = self.rf.get('/', HTTP_ACCEPT_ENCODING='gzip, deflate') response = GZipMiddleware().process_response( request, - CommonMiddleware().process_response(request, HttpResponse(self.compressible_string)) + ConditionalGetMiddleware().process_response(request, HttpResponse(self.compressible_string)) ) - gzip_etag = response.get('ETag') - - request = self.rf.get('/', HTTP_ACCEPT_ENCODING='') - response = GZipMiddleware().process_response( - request, - CommonMiddleware().process_response(request, HttpResponse(self.compressible_string)) + gzip_etag = response['ETag'] + next_request = self.rf.get('/', HTTP_ACCEPT_ENCODING='gzip, deflate', HTTP_IF_NONE_MATCH=gzip_etag) + next_response = ConditionalGetMiddleware().process_response( + next_request, + HttpResponse(self.compressible_string) ) - nogzip_etag = response.get('ETag') - - self.assertNotEqual(gzip_etag, nogzip_etag) + self.assertEqual(next_response.status_code, 304)