Skip to content

Commit

Permalink
Fixed #580 -- Added mega support for generating Vary headers, includi…
Browse files Browse the repository at this point in the history
…ng some view decorators, and changed the CacheMiddleware to account for the Vary header. Also added GZipMiddleware and ConditionalGetMiddleware, which are no longer handled by CacheMiddleware itself. Also updated the cache.txt and middleware.txt docs. Thanks to Hugo and Sune for the excellent patches

git-svn-id: http://code.djangoproject.com/svn/django/trunk@810 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
adrianholovaty committed Oct 9, 2005
1 parent a5a89b5 commit d65526d
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 153 deletions.
116 changes: 49 additions & 67 deletions django/middleware/cache.py
@@ -1,88 +1,70 @@
import copy
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers
from django.utils.httpwrappers import HttpResponseNotModified from django.utils.httpwrappers import HttpResponseNotModified
from django.utils.text import compress_string
import datetime, md5


class CacheMiddleware: class CacheMiddleware:
""" """
Cache middleware. If this is enabled, each Django-powered page will be Cache middleware. If this is enabled, each Django-powered page will be
cached for CACHE_MIDDLEWARE_SECONDS seconds. Cache is based on URLs. Pages cached for CACHE_MIDDLEWARE_SECONDS seconds. Cache is based on URLs.
with GET or POST parameters are not cached.
If the cache is shared across multiple sites using the same Django Only parameter-less GET or HEAD-requests with status code 200 are cached.
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.
This middleware will also make the following optimizations: This middleware expects that a HEAD request is answered with a response
exactly like the corresponding GET request.
* If the CACHE_MIDDLEWARE_GZIP setting is True, the content will be When a hit occurs, a shallow copy of the original response object is
gzipped. returned from process_request.
* ETags will be added, using a simple MD5 hash of the page's content. Pages will be cached based on the contents of the request headers
listed in the response's "Vary" header. This means that pages shouldn't
change their "Vary" header.
This middleware also 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): def process_request(self, request):
""" "Checks whether the page is already cached and returns the cached version if available."
Checks whether the page is already cached. If it is, returns the cached if not request.META['REQUEST_METHOD'] in ('GET', 'HEAD') or request.GET:
version. Also handles ETag stuff. request._cache_update_cache = False
"""
if request.GET or request.POST:
request._cache_middleware_set_cache = False
return None # Don't bother checking the cache. return None # Don't bother checking the cache.


accept_encoding = '' cache_key = get_cache_key(request, self.key_prefix)
if settings.CACHE_MIDDLEWARE_GZIP: if cache_key is None:
try: request._cache_update_cache = True
accept_encoding = request.META['HTTP_ACCEPT_ENCODING'] return None # No cache information available, need to rebuild.
except KeyError:
pass
accepts_gzip = 'gzip' in accept_encoding
request._cache_middleware_accepts_gzip = accepts_gzip

# 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) response = cache.get(cache_key, None)
if response is None: if response is None:
request._cache_middleware_set_cache = True request._cache_update_cache = True
return None return None # No cache information available, need to rebuild.
else:
request._cache_middleware_set_cache = False request._cache_update_cache = False
# Logic is from http://simon.incutio.com/archive/2003/04/23/conditionalGet return copy.copy(response)
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


def process_response(self, request, response): def process_response(self, request, response):
""" "Sets the cache, if needed."
Sets the cache, if needed. if not request._cache_update_cache:
""" # We don't need to update the cache, just return.
if request._cache_middleware_set_cache: return response
content = response.get_content_as_string(settings.DEFAULT_CHARSET) if not request.META['REQUEST_METHOD'] == 'GET':
if request._cache_middleware_accepts_gzip: # This is a stronger requirement than above. It is needed
content = compress_string(content) # because of interactions between this middleware and the
response.content = content # HTTPMiddleware, which throws the body of a HEAD-request
response['Content-Encoding'] = 'gzip' # away before this middleware gets a chance to cache it.
response['ETag'] = md5.new(content).hexdigest() return response
response['Content-Length'] = '%d' % len(content) if not response.status_code == 200:
response['Last-Modified'] = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT') return response
cache.set(request._cache_middleware_key, response, settings.CACHE_MIDDLEWARE_SECONDS) 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 return response
24 changes: 24 additions & 0 deletions django/middleware/gzip.py
@@ -0,0 +1,24 @@
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 compresses content if the browser allows gzip compression.
It sets the Vary header accordingly, so that 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
37 changes: 37 additions & 0 deletions django/middleware/http.py
@@ -0,0 +1,37 @@
import datetime

class ConditionalGetMiddleware:
"""
Handles conditional GET operations. If the response has a ETag or
Last-Modified header, and the request has If-None-Match or
If-Modified-Since, the response is replaced by an HttpNotModified.
Removes the content from any response to a HEAD request.
Also 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
2 changes: 2 additions & 0 deletions django/middleware/sessions.py
@@ -1,5 +1,6 @@
from django.conf.settings import SESSION_COOKIE_NAME, SESSION_COOKIE_AGE, SESSION_COOKIE_DOMAIN from django.conf.settings import SESSION_COOKIE_NAME, SESSION_COOKIE_AGE, SESSION_COOKIE_DOMAIN
from django.models.core import sessions from django.models.core import sessions
from django.utils.cache import patch_vary_headers
import datetime import datetime


TEST_COOKIE_NAME = 'testcookie' TEST_COOKIE_NAME = 'testcookie'
Expand Down Expand Up @@ -61,6 +62,7 @@ def process_request(self, request):
def process_response(self, request, response): def process_response(self, request, response):
# If request.session was modified, or if response.session was set, save # If request.session was modified, or if response.session was set, save
# those changes and set a session cookie. # those changes and set a session cookie.
patch_vary_headers(response, ('Cookie',))
try: try:
modified = request.session.modified modified = request.session.modified
except AttributeError: except AttributeError:
Expand Down
70 changes: 15 additions & 55 deletions django/views/decorators/cache.py
@@ -1,57 +1,17 @@
from django.core.cache import cache """
from django.utils.httpwrappers import HttpResponseNotModified Decorator for views that tries getting the page from the cache and
from django.utils.text import compress_string populates the cache if the page isn't in the cache yet.
from django.conf.settings import DEFAULT_CHARSET
import datetime, md5
def cache_page(view_func, cache_timeout, key_prefix=''): 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
Decorator for views that tries getting the page from the cache and in a multi-site setup. You could use the sites.get_current().domain, for
populates the cache if the page isn't in the cache yet. Also takes care example, as that is unique across a Django project.
of ETags and gzips the page if the client supports it.
The cache is keyed off of the page's URL plus the optional key_prefix Additionally, all headers from the response's Vary header will be taken into
variable. Use key_prefix if your Django setup has multiple sites that account on caching -- just like the middleware does.
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. from django.utils.decorators import decorator_from_middleware
""" from django.middleware.cache import CacheMiddleware
def _check_cache(request, *args, **kwargs):
try: cache_page = decorator_from_middleware(CacheMiddleware)
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
6 changes: 6 additions & 0 deletions django/views/decorators/gzip.py
@@ -0,0 +1,6 @@
"Decorator for views that gzips pages if the client supports it."

from django.utils.decorators import decorator_from_middleware
from django.middleware.gzip import GZipMiddleware

gzip_page = decorator_from_middleware(GZipMiddleware)
9 changes: 9 additions & 0 deletions django/views/decorators/http.py
@@ -0,0 +1,9 @@
"""
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 ConditionalGetMiddleware

conditional_page = decorator_from_middleware(ConditionalGetMiddleware)

0 comments on commit d65526d

Please sign in to comment.