Repository: Kinto/kinto

Issue:
There's still a problem with browser caching
It seems adding the `Vary` header to responses didn't entirely solve the issue we had with cached authenticated responses in the browser.

[This fiddle](https://jsfiddle.net/n1k0/5axpdbbj/3/) reproduces the issue against the kinto dev server, where you can see by running the script at least twice that the second time the responses are cached and served to distinct authenticated requests.

The issue is reproducible in both Firefox and Chrome.

First user response for `GET /buckets`:

![](http://i.imgur.com/SfgWnWX.png)

Second user response  for `GET /buckets`:

![](http://i.imgur.com/c7YPpLP.png)

Notice the two distinctly used Authorization headers sent, and the the two equivalent responses obtained. In the second capture, the second user received a cached response containing data from the first user. That's rather annoying.



Discussion:

Resolving patch:
diff --git a/kinto/core/__init__.py b/kinto/core/__init__.py
--- a/kinto/core/__init__.py
+++ b/kinto/core/__init__.py
@@ -45,7 +45,6 @@
         'kinto.core.initialization.setup_permission',
         'kinto.core.initialization.setup_cache',
         'kinto.core.initialization.setup_requests_scheme',
-        'kinto.core.initialization.setup_vary_headers',
         'kinto.core.initialization.setup_version_redirection',
         'kinto.core.initialization.setup_deprecation',
         'kinto.core.initialization.setup_authentication',
@@ -96,7 +95,7 @@ class Service(CorniceService):
     patching the default cornice service (which would impact other uses of it)
     """
     default_cors_headers = ('Backoff', 'Retry-After', 'Alert',
-                            'Content-Length', 'Vary')
+                            'Content-Length')
 
     def error_handler(self, error):
         return errors.json_error_handler(error)
diff --git a/kinto/core/initialization.py b/kinto/core/initialization.py
--- a/kinto/core/initialization.py
+++ b/kinto/core/initialization.py
@@ -140,20 +140,6 @@ def on_new_request(event):
         config.add_subscriber(on_new_request, NewRequest)
 
 
-def setup_vary_headers(config):
-    """Add Vary headers to each response."""
-    settings = config.get_settings()
-
-    vary = aslist(settings.get('vary', 'Authorization'))
-
-    def on_new_request(event):
-        def vary_callback(request, response):
-            response.vary = vary
-        event.request.add_response_callback(vary_callback)
-
-    config.add_subscriber(on_new_request, NewRequest)
-
-
 def setup_deprecation(config):
     config.add_tween("kinto.core.initialization._end_of_life_tween_factory")
 
diff --git a/kinto/core/resource/__init__.py b/kinto/core/resource/__init__.py
--- a/kinto/core/resource/__init__.py
+++ b/kinto/core/resource/__init__.py
@@ -717,6 +717,7 @@ def _add_cache_header(self, response):
             # resolution in seconds, do not use Pyramid `cache_expires()` in
             # order to omit it.
             response.cache_control.no_cache = True
+            response.cache_control.no_store = True
 
     def _raise_400_if_invalid_id(self, record_id):
         """Raise 400 if specified record id does not match the format excepted


Test patch:
diff --git a/kinto/tests/core/resource/test_views.py b/kinto/tests/core/resource/test_views.py
--- a/kinto/tests/core/resource/test_views.py
+++ b/kinto/tests/core/resource/test_views.py
@@ -667,6 +667,7 @@ def test_cache_control_headers_are_set_if_anonymous(self):
     def test_cache_control_headers_are_not_set_if_authenticated(self):
         resp = self.app.get(self.collection_url, headers=self.headers)
         self.assertIn('no-cache', resp.headers['Cache-Control'])
+        self.assertIn('no-store', resp.headers['Cache-Control'])
         self.assertNotIn('Expires', resp.headers)
 
     def test_cache_control_headers_set_no_cache_if_zero(self):
diff --git a/kinto/tests/core/resource/test_views_cors.py b/kinto/tests/core/resource/test_views_cors.py
--- a/kinto/tests/core/resource/test_views_cors.py
+++ b/kinto/tests/core/resource/test_views_cors.py
@@ -147,11 +147,11 @@ def test_collection_get_exposes_every_possible_header(self):
         self.assert_expose_headers('GET', self.collection_url, [
             'Alert', 'Backoff', 'ETag', 'Last-Modified', 'Next-Page',
             'Retry-After', 'Total-Records', 'Content-Length',
-            'Cache-Control', 'Expires', 'Pragma', 'Vary'])
+            'Cache-Control', 'Expires', 'Pragma'])
 
     def test_hello_endpoint_exposes_only_minimal_set_of_headers(self):
         self.assert_expose_headers('GET', '/', [
-            'Alert', 'Backoff', 'Retry-After', 'Content-Length', 'Vary'])
+            'Alert', 'Backoff', 'Retry-After', 'Content-Length'])
 
     def test_record_get_exposes_only_used_headers(self):
         body = {'data': MINIMALIST_RECORD}
@@ -163,26 +163,22 @@ def test_record_get_exposes_only_used_headers(self):
         self.assert_expose_headers('GET', record_url, [
             'Alert', 'Backoff', 'ETag', 'Retry-After',
             'Last-Modified', 'Content-Length',
-            'Cache-Control', 'Expires', 'Pragma', 'Vary'])
+            'Cache-Control', 'Expires', 'Pragma'])
 
     def test_record_post_exposes_only_minimal_set_of_headers(self):
         body = {'data': MINIMALIST_RECORD}
-        self.assert_expose_headers('POST_JSON', '/mushrooms',
-                                   ['Alert', 'Backoff', 'Retry-After',
-                                    'Content-Length', 'Vary'],
-                                   body=body)
+        self.assert_expose_headers('POST_JSON', '/mushrooms', [
+            'Alert', 'Backoff', 'Retry-After', 'Content-Length'], body=body)
 
     def test_present_on_bad_id_400_errors(self):
         body = {'data': {'name': 'Amanite'}}
         self.assert_expose_headers('PUT_JSON', '/mushrooms/wrong=ids', [
-            'Alert', 'Backoff', 'Retry-After', 'Content-Length', 'Vary'],
+            'Alert', 'Backoff', 'Retry-After', 'Content-Length'],
             body=body, status=400)
 
     def test_present_on_unknown_url(self):
-        self.assert_expose_headers('PUT_JSON', '/unknown',
-                                   ['Alert', 'Backoff', 'Retry-After',
-                                    'Content-Length', 'Vary'],
-                                   status=404)
+        self.assert_expose_headers('PUT_JSON', '/unknown', [
+            'Alert', 'Backoff', 'Retry-After', 'Content-Length'], status=404)
 
 
 class CORSMaxAgeTest(BaseWebTest, unittest.TestCase):
diff --git a/kinto/tests/core/test_views_hello.py b/kinto/tests/core/test_views_hello.py
--- a/kinto/tests/core/test_views_hello.py
+++ b/kinto/tests/core/test_views_hello.py
@@ -52,11 +52,6 @@ def test_if_user_authenticated_userid_is_provided(self):
         self.assertTrue(userid.startswith('basicauth:'),
                         '"%s" does not start with "basicauth:"' % userid)
 
-    def test_vary_header_is_present(self):
-        response = self.app.get('/', headers=self.headers)
-        self.assertIn('Vary', response.headers)
-        self.assertIn('Authorization', response.headers['Vary'])
-
     def test_return_http_api_version_when_set(self):
         with mock.patch.dict(
                 self.app.app.registry.settings,
diff --git a/kinto/tests/test_views_collections_cache.py b/kinto/tests/test_views_collections_cache.py
--- a/kinto/tests/test_views_collections_cache.py
+++ b/kinto/tests/test_views_collections_cache.py
@@ -109,7 +109,7 @@ def test_expires_and_cache_control_are_not_set_if_authenticated(self):
         r = self.app.get(self.records_url, headers=self.headers)
         self.assertNotIn('Expires', r.headers)
         self.assertIn('Cache-Control', r.headers)
-        self.assertEqual(r.headers['Cache-Control'], 'no-cache')
+        self.assertNotIn('max-age', r.headers['Cache-Control'])
 
     def test_expires_and_cache_control_are_set_on_records(self):
         r = self.app.get(self.records_url)