Index: django/utils/cache.py
===================================================================
--- django/utils/cache.py	(revision 0)
+++ django/utils/cache.py	(revision 0)
@@ -0,0 +1,191 @@
+"""
+This module contains helper functions and decorators
+for controlling caching middleware and external caches.
+It does so by managing the Vary header of responses.
+Included are functions to patch the header of response
+objects directly and decorators that change functions
+to do that header patching on return.
+
+The Vary header of the response defines what headers a
+cache has to take into account when building it's cache
+key. Requests with the same path but different header
+content for headers named in Vary need to get different
+cache keys to prevent delivery of wrong content.
+
+A sample would be some i18n middleware that needs to
+distinguish caches by the Accept-language header to
+prevent delivering wrong language content to users just
+because it happens to stay in the cache.
+"""
+
+import re
+import md5
+import datetime
+
+from django.conf import settings
+from django.core.cache import cache
+
+vary_delim_re = re.compile(r',\s*')
+
+def patch_response_headers(response, cache_timeout=None):
+    """
+    This function patches several response headers to match
+    cache settings. Not all are cache related, but all are
+    related to traffic reducing activity and cache controlling.
+
+    The cache timeout is measured in seconds and usually taken
+    from CACHE_MIDDLEWARE_SECONDS, but can be overridden per
+    invocation.
+    """
+
+    if cache_timeout is None:
+        cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
+
+    now = datetime.datetime.utcnow()
+    expires = now + datetime.timedelta(0, cache_timeout)
+
+    if not response.has_header('ETag'):
+        response['ETag'] = md5.new(response.content).hexdigest()
+    if not response.has_header('Last-Modified'):
+        response['Last-Modified'] = now.strftime('%a, %d %b %Y %H:%M:%S GMT')
+    if not response.has_header('Expires'):
+        response['Expires'] = expires.strftime('%a, %d %b %Y %H:%M:%S GMT')
+    if not response.has_header('Cache-Control'):
+        response['Cache-Control'] = 'max-age=%d' % cache_timeout
+
+def patch_vary_headers(response, newheaders):
+    """
+    This helper patches a header into the Vary header
+    of the response. It takes a response object and
+    a list of headers that should be added to the Vary
+    header. It adds only those headers that are not
+    already in there.
+    """
+    vary = []
+    if response.has_header('Vary'):
+        vary = vary_delim_re.split(response['Vary'])
+    d = dict([(el.lower(), 1) for el in vary])
+    for newheader in newheaders:
+        if not newheader.lower() in d:
+            vary.append(newheader)
+    response['Vary'] = ', '.join(vary)
+
+def vary_on_headers(*headers):
+    """
+    This is a decorator that manipulates a response to
+    take headers into account for caching. It does so by adding
+    those headers to the Vary header of the response.
+
+    Usage is like this::
+
+       @vary_on_headers('Cookie', 'Accept-language')
+       def index(request):
+           ...
+           return HttpResponse(html, context)
+    """
+
+    def decorator(func):
+    
+        def inner_func(*args, **kw):
+            resp = func(*args, **kw)
+            patch_vary_headers(resp, headers)
+            return resp
+
+        return inner_func
+    
+    return decorator
+
+def vary_on_cookie(func):
+    """
+    This is a decorator that will add the Cookie header
+    to the Vary header of a response to designate that a
+    page contents depends on cookies.
+
+    Usage is like this::
+
+        @vary_on_cookie
+        def index(request):
+            ...
+            return HttpResponse(html, context)
+    """
+    
+    def inner_func(*args, **kw):
+        resp = func(*args, **kw)
+        patch_vary_headers(resp, ['Cookie'])
+        return resp
+
+    return inner_func
+
+def _generate_cache_key(request, headerlist, key_prefix):
+    """
+    This helper function pulls out the headers given
+    in the header list and constructs a cache key from
+    those.
+    """
+    ctx = md5.new()
+    for header in headerlist:
+        value = request.META.get(header, None)
+        if value is not None:
+            ctx.update(value)
+    return 'views.decorators.cache.cache_page.%s.%s.%s' % (
+        key_prefix, request.path, ctx.hexdigest())
+
+def get_cache_key(request, key_prefix=None):
+    """
+    This function returns a cache key based on the request path.
+    It can be used in the request phase because it pulls the
+    list of headers to take into account from the global path
+    registry and uses those to build a cache key to check
+    against.
+
+    If there is no headerlist stored, the page needs to be
+    rebuilt and so this function returns None.
+    """
+
+    if key_prefix is None:
+        key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
+
+    cache_key = 'views.decorators.cache.cache_header.%s.%s' % (
+        key_prefix, request.path)
+    headerlist = cache.get(cache_key, None)
+    if headerlist is not None:
+        return _generate_cache_key(request, headerlist, key_prefix)
+    else:
+        return None
+
+def learn_cache_key(request, response, cache_timeout=None, key_prefix=None):
+    """
+    This function learns what headers to take into account
+    for some request path from the response object. It stores
+    those headers in a global path registry so that later access
+    to that path will know what headers to take into account without
+    building the response object itself - the headers are named in
+    the Vary header of the response, but we want to prevent response
+    generation.
+
+    The list of headers to use for cache key generation is stored
+    in the same cache as the pages themselves. If the cache ages
+    some data out of the cache, this just means that we have to
+    build the response once to get at the Vary header and so at
+    the list of headers to use for the cache key.
+    """
+
+    if key_prefix is None:
+        key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
+
+    if cache_timeout is None:
+        cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
+
+    cache_key = 'views.decorators.cache.cache_header.%s.%s' % (
+        key_prefix, request.path)
+
+    if response.has_header('Vary'):
+        headerlist = ['HTTP_'+header.upper().replace('-', '_') for header in vary_delim_re.split(response['Vary'])]
+        cache.set(cache_key, headerlist, cache_timeout)
+        return _generate_cache_key(request, headerlist, key_prefix)
+    else:
+        # if there is no Vary header, we still need a cache key
+        # for the request.path
+        cache.set(cache_key, [], cache_timeout)
+        return _generate_cache_key(request, [], key_prefix)
+
Index: django/utils/decorators.py
===================================================================
--- django/utils/decorators.py	(revision 0)
+++ django/utils/decorators.py	(revision 0)
@@ -0,0 +1,31 @@
+"""
+This module has functions that help with
+creating decorators for views.
+"""
+
+def decorator_from_middleware(middleware_class):
+    """
+    This function turns any middleware that implements either
+    process_request or process_response (or both) into a decorator
+    so that you can use it on a per-view base.
+    """
+
+    def _decorator_from_middleware(view_func, *args, **kwargs):
+    
+        middleware = middleware_class(*args, **kwargs)
+
+        def _wrapped_view(request, *args, **kwargs):
+            if hasattr(middleware, 'process_request'):
+                result = middleware.process_request(request)
+                if result is not None:
+                    return result
+            response = view_func(request, *args, **kwargs)
+            if hasattr(middleware, 'process_response'):
+                result = middleware.process_response(request, response)
+                if result is not None:
+                    return result
+            return response
+
+        return _wrapped_view
+
+    return _decorator_from_middleware
Index: django/middleware/gzip.py
===================================================================
--- django/middleware/gzip.py	(revision 0)
+++ django/middleware/gzip.py	(revision 0)
@@ -0,0 +1,28 @@
+import re
+from django.utils.text import compress_string
+from django.utils.cache import patch_vary_headers
+
+re_accepts_gzip = re.compile(r'\bgzip\b')
+
+class GZipMiddleware:
+    """
+    This middleware just compresses content if the browser
+    allows compression to take place. It sets the Vary
+    header accordingly so that other caches will base
+    their storage on the Accept-Encoding header.
+    """
+    
+    def process_response(self, request, response):
+        patch_vary_headers(response, ['Accept-Encoding'])
+
+        if response.has_header('Content-Encoding'):
+            return response
+
+        ae = request.META.get('HTTP_ACCEPT_ENCODING', '')
+        if not re_accepts_gzip.search(ae):
+            return response
+
+        response.content = compress_string(response.content)
+        response['Content-Encoding'] = 'gzip'
+
+        return response
Index: django/middleware/http.py
===================================================================
--- django/middleware/http.py	(revision 0)
+++ django/middleware/http.py	(revision 0)
@@ -0,0 +1,40 @@
+import datetime
+
+class HTTPMiddleware:
+    """HTTP middleware:
+
+    Handles conditional GET operations, if the response has a ETag og
+    Last-Modified header, and the request has If-None-Match or
+    If-Modified-Since the response is replaced by a HttpNotModified, when
+    it is the case.
+
+    Removes the content from any response to a HEAD request.
+
+    Sets the Date and Content-Length response-headers.
+    """
+    
+    def process_response(self, request, response):
+        now = datetime.datetime.utcnow()
+        response['Date'] = now.strftime('%a, %d %b %Y %H:%M:%S GMT')
+        if not response.has_header('Content-Length'):
+            response['Content-Length'] = str(len(response.content))
+    
+        if response.has_header('ETag'):
+            if_none_match = request.META.get('HTTP_IF_NONE_MATCH', None)
+            if if_none_match == response['ETag']:
+                response.status_code = 304
+                response.content = ''
+                response['Content-Length'] = '0'
+
+        if response.has_header('Last-Modified'):
+            last_mod = response['Last-Modified']
+            if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None)
+            if if_modified_since == response['Last-Modified']:
+                response.status_code = 304
+                response.content = ''
+                response['Content-Length'] = '0'
+
+        if request.META['REQUEST_METHOD'] == 'HEAD':
+            response.content = ''
+
+        return response
Index: django/middleware/cache.py
===================================================================
--- django/middleware/cache.py	(revision 803)
+++ django/middleware/cache.py	(working copy)
@@ -1,88 +1,87 @@
+"""A Vary-capable cache middleware. Adapted from Djangos cache
+middleware."""
+
 from django.conf import settings
 from django.core.cache import cache
+from django.utils.cache import get_cache_key, learn_cache_key, patch_vary_headers, patch_response_headers
 from django.utils.httpwrappers import HttpResponseNotModified
-from django.utils.text import compress_string
-import datetime, md5
 
+import datetime
+import copy
+import md5
+
 class CacheMiddleware:
     """
     Cache middleware. If this is enabled, each Django-powered page will be
-    cached for CACHE_MIDDLEWARE_SECONDS seconds. Cache is based on URLs. Pages
-    with GET or POST parameters are not cached.
+    cached for CACHE_MIDDLEWARE_SECONDS seconds. Cache is based on URLs.
 
-    If the cache is shared across multiple sites using the same Django
-    installation, set the CACHE_MIDDLEWARE_KEY_PREFIX to the name of the site,
-    or some other string that is unique to this Django instance, to prevent key
-    collisions.
+    Only parameter-less GET or HEAD-requests are cached.
 
-    This middleware will also make the following optimizations:
+    Only responses with status-code 200 will be cached.
 
-    * If the CACHE_MIDDLEWARE_GZIP setting is True, the content will be
-      gzipped.
+    This middleware expects that a HEAD request is answered with a
+    response exactly like the corresponding GET request.
 
-    * ETags will be added, using a simple MD5 hash of the page's content.
+    When a hit occurs, a shallow copy of the original response object is
+    returned from process_request.
+
+    Pages will be cached based on the contents of the request headers
+    listed in the response Vary-header [ FIXME -- need example, or better
+    description? ]. This means that pages cannot change their Vary-header,
+    without strange results.
+
+    Also, this middleware sets ETag, Last-Modified, Expires and
+    Cache-Control headers on the response object.
     """
+
+    def __init__(self, cache_timeout=None, key_prefix=None):
+        self.cache_timeout = cache_timeout
+        if cache_timeout is None:
+            self.cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
+        self.key_prefix = key_prefix
+        if key_prefix is None:
+            self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
+
     def process_request(self, request):
-        """
-        Checks whether the page is already cached. If it is, returns the cached
-        version. Also handles ETag stuff.
-        """
-        if request.GET or request.POST:
-            request._cache_middleware_set_cache = False
+        """Checks whether the page is already cached. If it is, returns
+        the cached version."""
+
+        method = request.META['REQUEST_METHOD']
+        if not method in ('GET', 'HEAD') or request.GET:
+            request._cache_update_cache = False
             return None # Don't bother checking the cache.
 
-        accept_encoding = ''
-        if settings.CACHE_MIDDLEWARE_GZIP:
-            try:
-                accept_encoding = request.META['HTTP_ACCEPT_ENCODING']
-            except KeyError:
-                pass
-        accepts_gzip = 'gzip' in accept_encoding
-        request._cache_middleware_accepts_gzip = accepts_gzip
+        cache_key = get_cache_key(request, self.key_prefix)
+        if cache_key is None:
+            request._cache_update_cache = True
+            return None # No cache information available, need to rebuild
 
-        # This uses the same cache_key as views.decorators.cache.cache_page,
-        # so the cache can be shared.
-        cache_key = 'views.decorators.cache.cache_page.%s.%s.%s' % \
-            (settings.CACHE_MIDDLEWARE_KEY_PREFIX, request.path, accepts_gzip)
-        request._cache_middleware_key = cache_key
-
         response = cache.get(cache_key, None)
         if response is None:
-            request._cache_middleware_set_cache = True
-            return None
-        else:
-            request._cache_middleware_set_cache = False
-            # Logic is from http://simon.incutio.com/archive/2003/04/23/conditionalGet
-            try:
-                if_none_match = request.META['HTTP_IF_NONE_MATCH']
-            except KeyError:
-                if_none_match = None
-            try:
-                if_modified_since = request.META['HTTP_IF_MODIFIED_SINCE']
-            except KeyError:
-                if_modified_since = None
-            if if_none_match is None and if_modified_since is None:
-                pass
-            elif if_none_match is not None and response['ETag'] != if_none_match:
-                pass
-            elif if_modified_since is not None and response['Last-Modified'] != if_modified_since:
-                pass
-            else:
-                return HttpResponseNotModified()
-        return response
+            request._cache_update_cache = True
+            return None # No cache information available, need to rebuild
 
+        request._cache_update_cache = False
+        return copy.copy(response)
+
     def process_response(self, request, response):
-        """
-        Sets the cache, if needed.
-        """
-        if request._cache_middleware_set_cache:
-            content = response.get_content_as_string(settings.DEFAULT_CHARSET)
-            if request._cache_middleware_accepts_gzip:
-                content = compress_string(content)
-                response.content = content
-                response['Content-Encoding'] = 'gzip'
-            response['ETag'] = md5.new(content).hexdigest()
-            response['Content-Length'] = '%d' % len(content)
-            response['Last-Modified'] = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
-            cache.set(request._cache_middleware_key, response, settings.CACHE_MIDDLEWARE_SECONDS)
+        """Sets the cache, if needed."""
+
+        if not request._cache_update_cache:
+            # we don't need to update the cache, just return
+            return response
+        if not request.META['REQUEST_METHOD'] == 'GET':
+            # This is a stronger requirement than above. It is needed
+            # because of interactions between this middleware and the
+            # HTTPMiddleware, which throws the body of a HEAD-request
+            # away before this middleware gets a chance to cache it.
+            return response
+        if not response.status_code == 200:
+            return response
+
+        patch_response_headers(response, self.cache_timeout)
+
+        cache_key = learn_cache_key(request, response, self.cache_timeout, self.key_prefix)
+        cache.set(cache_key, response, self.cache_timeout)
+
         return response
Index: django/middleware/sessions.py
===================================================================
--- django/middleware/sessions.py	(revision 803)
+++ django/middleware/sessions.py	(working copy)
@@ -1,5 +1,6 @@
 from django.conf.settings import SESSION_COOKIE_NAME, SESSION_COOKIE_AGE, SESSION_COOKIE_DOMAIN
 from django.models.core import sessions
+from django.utils.cache import patch_vary_headers
 import datetime
 
 TEST_COOKIE_NAME = 'testcookie'
@@ -61,6 +62,7 @@
     def process_response(self, request, response):
         # If request.session was modified, or if response.session was set, save
         # those changes and set a session cookie.
+        patch_vary_headers(response, ['Cookie'])
         try:
             modified = request.session.modified
         except AttributeError:
Index: django/views/decorators/gzip.py
===================================================================
--- django/views/decorators/gzip.py	(revision 0)
+++ django/views/decorators/gzip.py	(revision 0)
@@ -0,0 +1,11 @@
+"""
+Decorator for views that compresses pages if the client supports it.
+Additionally it patches the headers to match the now compressed
+content and adds the Accept-Encoding header to the Vary header to
+give caches a hint on what to take into account for cache keys.
+"""
+from django.utils.decorators import decorator_from_middleware
+from django.middleware.gzip import GZipMiddleware
+
+gzip_page = decorator_from_middleware(GZipMiddleware)
+
Index: django/views/decorators/http.py
===================================================================
--- django/views/decorators/http.py	(revision 0)
+++ django/views/decorators/http.py	(revision 0)
@@ -0,0 +1,10 @@
+"""
+Decorator for views that supports conditional get on ETag and
+Last-Modified headers.
+"""
+
+from django.utils.decorators import decorator_from_middleware
+from django.middleware.http import HTTPMiddleware
+
+conditional_page = decorator_from_middleware(HTTPMiddleware)
+
Index: django/views/decorators/cache.py
===================================================================
--- django/views/decorators/cache.py	(revision 803)
+++ django/views/decorators/cache.py	(working copy)
@@ -1,57 +1,17 @@
-from django.core.cache import cache
-from django.utils.httpwrappers import HttpResponseNotModified
-from django.utils.text import compress_string
-from django.conf.settings import DEFAULT_CHARSET
-import datetime, md5
+"""
+Decorator for views that tries getting the page from the cache and
+populates the cache if the page isn't in the cache yet.
 
-def cache_page(view_func, cache_timeout, key_prefix=''):
-    """
-    Decorator for views that tries getting the page from the cache and
-    populates the cache if the page isn't in the cache yet. Also takes care
-    of ETags and gzips the page if the client supports it.
+The cache is keyed by the URL and some data from the headers. Additionally
+there is the key prefix that is used to distinguish different cache areas
+in a multi-site setup. You could use the sites.get_current().domain, for
+example, as that is unique across a django project.
 
-    The cache is keyed off of the page's URL plus the optional key_prefix
-    variable. Use key_prefix if your Django setup has multiple sites that
-    use cache; otherwise the cache for one site would affect the other. A good
-    example of key_prefix is to use sites.get_current().domain, because that's
-    unique across all Django instances on a particular server.
-    """
-    def _check_cache(request, *args, **kwargs):
-        try:
-            accept_encoding = request.META['HTTP_ACCEPT_ENCODING']
-        except KeyError:
-            accept_encoding = ''
-        accepts_gzip = 'gzip' in accept_encoding
-        cache_key = 'views.decorators.cache.cache_page.%s.%s.%s' % (key_prefix, request.path, accepts_gzip)
-        response = cache.get(cache_key, None)
-        if response is None:
-            response = view_func(request, *args, **kwargs)
-            content = response.get_content_as_string(DEFAULT_CHARSET)
-            if accepts_gzip:
-                content = compress_string(content)
-                response.content = content
-                response['Content-Encoding'] = 'gzip'
-            response['ETag'] = md5.new(content).hexdigest()
-            response['Content-Length'] = '%d' % len(content)
-            response['Last-Modified'] = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
-            cache.set(cache_key, response, cache_timeout)
-        else:
-            # Logic is from http://simon.incutio.com/archive/2003/04/23/conditionalGet
-            try:
-                if_none_match = request.META['HTTP_IF_NONE_MATCH']
-            except KeyError:
-                if_none_match = None
-            try:
-                if_modified_since = request.META['HTTP_IF_MODIFIED_SINCE']
-            except KeyError:
-                if_modified_since = None
-            if if_none_match is None and if_modified_since is None:
-                pass
-            elif if_none_match is not None and response['ETag'] != if_none_match:
-                pass
-            elif if_modified_since is not None and response['Last-Modified'] != if_modified_since:
-                pass
-            else:
-                return HttpResponseNotModified()
-        return response
-    return _check_cache
+Additionally all headers from the responses Vary header will be taken into
+account on caching (just like the middleware does).
+"""
+
+from django.utils.decorators import decorator_from_middleware
+from django.middleware.cache import CacheMiddleware
+
+cache_page = decorator_from_middleware(CacheMiddleware)
Index: docs/cache.txt
===================================================================
--- docs/cache.txt	(revision 803)
+++ docs/cache.txt	(working copy)
@@ -104,15 +104,57 @@
 
 Pages with GET or POST parameters won't be cached.
 
-The cache middleware also makes a few more optimizations:
-
-* Sets and deals with ``ETag`` headers.
-* Sets the ``Content-Length`` header.
+Additionally some cache helper headers are set up by the CacheMiddlware:
 * Sets the ``Last-Modified`` header to the current date/time when a fresh
   (uncached) version of the page is requested.
+* Sets the ``Expires`` header to the current date/time plus the defined
+  cache seconds
+* Sets the ``Cache-Control`` header to give a max age for the page
+  from the configured cache seconds
 
-It doesn't matter where in the middleware stack you put the cache middleware.
+There are other optimizations that could help with high traffic. Those are
+implemented with different middlewares, though:
 
+* Support for conditional GET is done by the HTTPMiddleware in
+  django.middleware.http. This makes use of ``ETag`` and
+  ``Last-Modified`` headers.
+* Support for gzip encoding responses is done by the GzipMiddleware in
+  django.middleware.gzip
+
+It's important where in the middleware stack you put the cache middleware, as it
+needs to know what headers to vary the cache storage on. Those headers are given
+as a list in the ``Vary`` header of the response and so the cache middleware should
+come after all middlewares that might change this header (like the SessionMiddleware -
+which adds the Cookie header - or the GzipMiddleware - which adds the ``Accept-Encoding``
+header).
+
+Since the cache key for a request is based on the request path and those headers from
+the request that are listed in the Vary response header, you need a way to tell for
+your function how this Vary header should be set. There are several helper functions
+to do this, the important ones are just plain decorators::
+
+    from django.utils.cache import vary_on_cookie, vary_on_headers, patch_vary_headers
+
+    @vary_on_cookie
+    def index(request):
+        ...
+        return render_to_response('template', context)
+
+    @vary_on_headers('Cookie', 'Accept-Language')
+    def overview(request):
+        ...
+        return render_to_response('template', context)
+
+    def something(request):
+        ...
+        response = render_to_response('template', context)
+        patch_vary_headers(response, ['Cookie'])
+        return response
+
+Of course middleware that knows what changes need to be done to the Vary response header
+will do that themselves. That way you don't need to use vary_on_cookie if you activate the
+SessionMiddlware, as that will already patch the Vary response header itself.
+
 The per-page cache
 ==================
 
@@ -135,7 +177,9 @@
         ...
 
 This will cache the result of that view for 15 minutes. (The cache timeout is
-in seconds.)
+in seconds.). The cache is used in the same way as the middlware so that cache
+entries can be shared. There are ``gzip_page`` and ``conditional_page`` decorators
+in correspondence to the other middlewares.
 
 The low-level cache API
 =======================
