Repository: getsentry/sentry

Issue:
Better avatar uploads caching
Right now, we cache resizes on the server, but these responses are unable to be cached by any browser or proxy.

To fix this we need to:
- [ ] Send a proper `Cache-Control` header that tells the browser it's allowed to cache it. Something like `Cache-Control: max-age=315360000`
- [ ] Urls that don't change, so they _can_ be cached. Right now, uploaded avatars reuse the same `UserAvatar.ident`, which means we get the same url even if we change the file. So we can't cache.
- [ ] Remove `Set-Cookie` header on response (this may be tricky but @mattrobenolt can help). `Set-Cookie` header kills all caching.
- [ ] Remove `Vary` header. We don't need to Vary on anything, and right now it's `Vary: Accept-Encoding, Cookie`, which basically makes caching useless. (this may be tricky as well, but @mattrobenolt can help)

@macqueen 



Discussion:

Resolving patch:
diff --git a/src/sentry/api/endpoints/useravatar.py b/src/sentry/api/endpoints/useravatar.py
--- a/src/sentry/api/endpoints/useravatar.py
+++ b/src/sentry/api/endpoints/useravatar.py
@@ -1,5 +1,7 @@
 from __future__ import absolute_import
 
+from uuid import uuid4
+
 from PIL import Image
 
 from django.conf import settings
@@ -57,6 +59,7 @@ def put(self, request, user):
             avatar.clear_cached_photos()
         if photo:
             avatar.file = photo
+            avatar.ident = uuid4().hex
 
         avatar_type = request.DATA.get('avatar_type')
 
diff --git a/src/sentry/middleware/locale.py b/src/sentry/middleware/locale.py
--- a/src/sentry/middleware/locale.py
+++ b/src/sentry/middleware/locale.py
@@ -22,8 +22,9 @@ def process_request(self, request):
         # This avoids touching user session, which means we avoid
         # setting `Vary: Cookie` as a response header which will
         # break HTTP caching entirely.
-        self.__is_static = request.path_info[:9] == '/_static/'
-        if self.__is_static:
+        self.__skip_caching = (request.path_info[:9] == '/_static/' or
+                               request.path_info[:8] == '/avatar/')
+        if self.__skip_caching:
             return
 
         safe_execute(self.load_user_conf, request,
@@ -50,10 +51,10 @@ def process_response(self, request, response):
         # adds an extra `Vary: Accept-Language`. Static files don't need this and is
         # less effective for caching.
         try:
-            if self.__is_static:
+            if self.__skip_caching:
                 return response
         except AttributeError:
-            # catch ourselves in case __is_static never got set.
+            # catch ourselves in case __skip_caching never got set.
             # It's possible that process_request never ran.
             pass
         return super(SentryLocaleMiddleware, self).process_response(request, response)
diff --git a/src/sentry/web/frontend/user_avatar.py b/src/sentry/web/frontend/user_avatar.py
--- a/src/sentry/web/frontend/user_avatar.py
+++ b/src/sentry/web/frontend/user_avatar.py
@@ -4,6 +4,7 @@
 from django.views.generic import View
 
 from sentry.models import UserAvatar
+from sentry.web.frontend.generic import FOREVER_CACHE
 
 
 class UserAvatarPhotoView(View):
@@ -31,4 +32,7 @@ def get(self, request, *args, **kwargs):
                 return HttpResponseBadRequest()
             else:
                 photo_file = avatar.get_cached_photo(size)
-        return HttpResponse(photo_file, content_type='image/png')
+
+        res = HttpResponse(photo_file, content_type='image/png')
+        res['Cache-Control'] = FOREVER_CACHE
+        return res


Test patch:
diff --git a/tests/sentry/web/frontend/test_user_avatar.py b/tests/sentry/web/frontend/test_user_avatar.py
new file mode 100644
--- /dev/null
+++ b/tests/sentry/web/frontend/test_user_avatar.py
@@ -0,0 +1,22 @@
+from __future__ import absolute_import
+
+from django.core.urlresolvers import reverse
+
+from sentry.models import File, UserAvatar
+from sentry.testutils import TestCase
+from sentry.utils.compat import StringIO
+from sentry.web.frontend.generic import FOREVER_CACHE
+
+
+class UserAvatarTest(TestCase):
+    def test_headers(self):
+        user = self.create_user(email='a@example.com')
+        photo = File.objects.create(name='test.png', type='avatar.file')
+        photo.putfile(StringIO('test'))
+        avatar = UserAvatar.objects.create(user=user, file=photo)
+        url = reverse('sentry-user-avatar-url', kwargs={'avatar_id': avatar.ident})
+        response = self.client.get(url)
+        assert response.status_code == 200
+        assert response['Cache-Control'] == FOREVER_CACHE
+        assert response.get('Vary') is None
+        assert response.get('Set-Cookie') is None