Repository: mozilla/kitsune

Issue:
Add middleware to add cache headers
This will mostly be enabled when `READ_ONLY` is `True` since we'll be using a CDN in front of the site in `READ_ONLY` mode to reduce the load on our hot-spare cluster in the event of a failover.


Discussion:

Resolving patch:
diff --git a/kitsune/settings.py b/kitsune/settings.py
--- a/kitsune/settings.py
+++ b/kitsune/settings.py
@@ -90,6 +90,10 @@ def parse_conn_max_age(value):
     },
 }
 
+CACHE_MIDDLEWARE_SECONDS = config('CACHE_MIDDLEWARE_SECONDS',
+                                  default=(2 * 60 * 60) if READ_ONLY else 0,
+                                  cast=int)
+
 # Setting this to the Waffle version.
 WAFFLE_CACHE_PREFIX = 'w0.7.7a:'
 
@@ -347,8 +351,7 @@ def parse_conn_max_age(value):
 
 SITE_ID = 1
 
-# If you set this to False, Django will make some optimizations so as
-# not to load the internationalization machinery.
+USE_ETAGS = config('USE_ETAGS', default=False, cast=bool)
 USE_I18N = True
 USE_L10N = True
 
@@ -517,7 +520,7 @@ def parse_conn_max_age(value):
     'django.middleware.common.CommonMiddleware',
     'kitsune.sumo.middleware.RemoveSlashMiddleware',
     'kitsune.inproduct.middleware.EuBuildMiddleware',
-    'kitsune.sumo.middleware.NoCacheHttpsMiddleware',
+    'kitsune.sumo.middleware.CacheHeadersMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
     'kitsune.sumo.anonymous.AnonymousIdentityMiddleware',
     'kitsune.sumo.middleware.ReadOnlyMiddleware',
diff --git a/kitsune/sumo/middleware.py b/kitsune/sumo/middleware.py
--- a/kitsune/sumo/middleware.py
+++ b/kitsune/sumo/middleware.py
@@ -10,7 +10,7 @@
 from django.http import HttpResponseForbidden
 from django.shortcuts import render
 from django.utils import translation
-from django.utils.cache import patch_vary_headers
+from django.utils.cache import add_never_cache_headers, patch_response_headers, patch_vary_headers
 from django.utils.encoding import iri_to_uri, smart_str, smart_unicode
 
 import mobility
@@ -104,16 +104,21 @@ def process_response(self, request, response):
         return response
 
 
-class NoCacheHttpsMiddleware(object):
+class CacheHeadersMiddleware(MiddlewareMixin):
     """
-    Sets no-cache headers when HTTPS META variable is set
-    and not equal to 'off'.
+    Sets no-cache headers normally, and cache for some time in READ_ONLY mode.
     """
     def process_response(self, request, response):
-        if request.is_secure():
-            response['Expires'] = 'Thu, 19 Nov 1981 08:52:00 GMT'
-            response['Cache-Control'] = 'no-cache, must-revalidate'
-            response['Pragma'] = 'no-cache'
+        if 'cache-control' in response or response.status_code >= 400:
+            return response
+
+        if (request.method in ('GET', 'HEAD') and
+                settings.CACHE_MIDDLEWARE_SECONDS):
+            # uses CACHE_MIDDLEWARE_SECONDS by default
+            patch_response_headers(response)
+        else:
+            add_never_cache_headers(response)
+
         return response
 
 


Test patch:
diff --git a/kitsune/sumo/tests/test_middleware.py b/kitsune/sumo/tests/test_middleware.py
--- a/kitsune/sumo/tests/test_middleware.py
+++ b/kitsune/sumo/tests/test_middleware.py
@@ -1,13 +1,60 @@
-from django.http import HttpResponsePermanentRedirect
+from django.http import HttpResponsePermanentRedirect, HttpResponse
+from django.test import override_settings
 from django.test.client import RequestFactory
 
 import mobility
 from nose.tools import eq_
 
-from kitsune.sumo.middleware import PlusToSpaceMiddleware, DetectMobileMiddleware
+from kitsune.sumo.middleware import (
+    PlusToSpaceMiddleware,
+    DetectMobileMiddleware,
+    CacheHeadersMiddleware,
+)
 from kitsune.sumo.tests import TestCase, PyQuery as pq
 
 
+class CacheHeadersMiddlewareTestCase(TestCase):
+    def setUp(self):
+        self.rf = RequestFactory()
+        self.mw = CacheHeadersMiddleware()
+
+    @override_settings(CACHE_MIDDLEWARE_SECONDS=60)
+    def test_add_cache_control(self):
+        req = self.rf.get('/')
+        resp = HttpResponse('OK')
+        resp = self.mw.process_response(req, resp)
+        assert resp['cache-control'] == 'max-age=60'
+
+    @override_settings(CACHE_MIDDLEWARE_SECONDS=60)
+    def test_already_has_cache_control(self):
+        req = self.rf.get('/')
+        resp = HttpResponse('OK')
+        resp['cache-control'] = 'no-cache'
+        resp = self.mw.process_response(req, resp)
+        assert resp['cache-control'] == 'no-cache'
+
+    @override_settings(CACHE_MIDDLEWARE_SECONDS=60)
+    def test_non_200_response(self):
+        req = self.rf.get('/')
+        resp = HttpResponse('WHA?', status=404)
+        resp = self.mw.process_response(req, resp)
+        assert 'cache-control' not in resp
+
+    @override_settings(CACHE_MIDDLEWARE_SECONDS=0)
+    def test_middleware_seconds_0(self):
+        req = self.rf.get('/')
+        resp = HttpResponse('OK')
+        resp = self.mw.process_response(req, resp)
+        assert resp['cache-control'] == 'no-cache, no-store, must-revalidate, max-age=0'
+
+    @override_settings(CACHE_MIDDLEWARE_SECONDS=60)
+    def test_post_request(self):
+        req = self.rf.post('/')
+        resp = HttpResponse('OK')
+        resp = self.mw.process_response(req, resp)
+        assert resp['cache-control'] == 'no-cache, no-store, must-revalidate, max-age=0'
+
+
 class TrailingSlashMiddlewareTestCase(TestCase):
     def test_no_trailing_slash(self):
         response = self.client.get(u'/en-US/ohnoez')