Skip to content

Commit

Permalink
Fixed #10762, #17514 -- Prevented the GZip middleware from returning …
Browse files Browse the repository at this point in the history
…a response longer than the original content, allowed compression of non-200 responses, and added tests (there were none). Thanks cannona for the initial patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@17365 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
aaugustin committed Jan 9, 2012
1 parent 62766f4 commit 4288c88
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 10 deletions.
11 changes: 8 additions & 3 deletions django/middleware/gzip.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class GZipMiddleware(object):
on the Accept-Encoding header.
"""
def process_response(self, request, response):
# It's not worth compressing non-OK or really short responses.
if response.status_code != 200 or len(response.content) < 200:
# It's not worth attempting to compress really short responses.
if len(response.content) < 200:
return response

patch_vary_headers(response, ('Accept-Encoding',))
Expand All @@ -32,7 +32,12 @@ def process_response(self, request, response):
if not re_accepts_gzip.search(ae):
return response

response.content = compress_string(response.content)
# Return the compressed content only if it's actually shorter.
compressed_content = compress_string(response.content)
if len(compressed_content) >= len(response.content):
return response

response.content = compressed_content
response['Content-Encoding'] = 'gzip'
response['Content-Length'] = str(len(response.content))
return response
21 changes: 14 additions & 7 deletions docs/ref/middleware.txt
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,29 @@ addresses defined in the :setting:`INTERNAL_IPS` setting. This is used by
Django's :doc:`automatic documentation system </ref/contrib/admin/admindocs>`.
Depends on :class:`~django.contrib.auth.middleware.AuthenticationMiddleware`.

GZIP middleware
GZip middleware
---------------

.. module:: django.middleware.gzip
:synopsis: Middleware to serve gziped content for performance.
:synopsis: Middleware to serve GZipped content for performance.

.. class:: GZipMiddleware

Compresses content for browsers that understand gzip compression (all modern
Compresses content for browsers that understand GZip compression (all modern
browsers).

It is suggested to place this first in the middleware list, so that the
compression of the response content is the last thing that happens. Will not
compress content bodies less than 200 bytes long, when the response code is
something other than 200, JavaScript files (for IE compatibility), or
responses that have the ``Content-Encoding`` header already specified.
compression of the response content is the last thing that happens.

It will not compress content bodies less than 200 bytes long, when the
``Content-Encoding`` header is already set, or when the browser does not send
an ``Accept-Encoding`` header containing ``gzip``.

Content will also not be compressed when the browser is Internet Explorer and
the ``Content-Type`` header contains ``javascript`` or starts with anything
other than ``text/``. This is done to overcome a bug present in early versions
of Internet Explorer which caused decompression not to be performed on certain
content types.

GZip compression can be applied to individual views using the
:func:`~django.views.decorators.http.gzip_page()` decorator.
Expand Down
87 changes: 87 additions & 0 deletions tests/regressiontests/middleware/tests.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-

import gzip
import re
import random
import StringIO

from django.conf import settings
from django.core import mail
Expand All @@ -9,6 +12,7 @@
from django.middleware.clickjacking import XFrameOptionsMiddleware
from django.middleware.common import CommonMiddleware
from django.middleware.http import ConditionalGetMiddleware
from django.middleware.gzip import GZipMiddleware
from django.test import TestCase


Expand Down Expand Up @@ -495,3 +499,86 @@ def get_xframe_options_value(self, request, response):
r = OtherXFrameOptionsMiddleware().process_response(HttpRequest(),
HttpResponse())
self.assertEqual(r['X-Frame-Options'], 'DENY')


class GZipMiddlewareTest(TestCase):
"""
Tests the GZip middleware.
"""
short_string = "This string is too short to be worth compressing."
compressible_string = 'a' * 500
uncompressible_string = ''.join(chr(random.randint(0, 255)) for _ in xrange(500))

def setUp(self):
self.req = HttpRequest()
self.req.META = {
'SERVER_NAME': 'testserver',
'SERVER_PORT': 80,
}
self.req.path = self.req.path_info = "/"
self.req.META['HTTP_ACCEPT_ENCODING'] = 'gzip, deflate'
self.req.META['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 5.1; rv:9.0.1) Gecko/20100101 Firefox/9.0.1'
self.resp = HttpResponse()
self.resp.status_code = 200
self.resp.content = self.compressible_string
self.resp['Content-Type'] = 'text/html; charset=UTF-8'

@staticmethod
def decompress(gzipped_string):
return gzip.GzipFile(mode='rb', fileobj=StringIO.StringIO(gzipped_string)).read()

def test_compress_response(self):
"""
Tests that compression is performed on responses with compressible content.
"""
r = GZipMiddleware().process_response(self.req, self.resp)
self.assertEqual(self.decompress(r.content), self.compressible_string)
self.assertEqual(r.get('Content-Encoding'), 'gzip')
self.assertEqual(r.get('Content-Length'), str(len(r.content)))

def test_compress_non_200_response(self):
"""
Tests that compression is performed on responses with a status other than 200.
See #10762.
"""
self.resp.status_code = 404
r = GZipMiddleware().process_response(self.req, self.resp)
self.assertEqual(self.decompress(r.content), self.compressible_string)
self.assertEqual(r.get('Content-Encoding'), 'gzip')

def test_no_compress_short_response(self):
"""
Tests that compression isn't performed on responses with short content.
"""
self.resp.content = self.short_string
r = GZipMiddleware().process_response(self.req, self.resp)
self.assertEqual(r.content, self.short_string)
self.assertEqual(r.get('Content-Encoding'), None)

def test_no_compress_compressed_response(self):
"""
Tests that compression isn't performed on responses that are already compressed.
"""
self.resp['Content-Encoding'] = 'deflate'
r = GZipMiddleware().process_response(self.req, self.resp)
self.assertEqual(r.content, self.compressible_string)
self.assertEqual(r.get('Content-Encoding'), 'deflate')

def test_no_compress_ie_js_requests(self):
"""
Tests that compression isn't performed on JavaScript requests from Internet Explorer.
"""
self.req.META['HTTP_USER_AGENT'] = 'Mozilla/4.0 (compatible; MSIE 5.00; Windows 98)'
self.resp['Content-Type'] = 'application/javascript; charset=UTF-8'
r = GZipMiddleware().process_response(self.req, self.resp)
self.assertEqual(r.content, self.compressible_string)
self.assertEqual(r.get('Content-Encoding'), None)

def test_no_compress_uncompressible_response(self):
"""
Tests that compression isn't performed on responses with uncompressible content.
"""
self.resp.content = self.uncompressible_string
r = GZipMiddleware().process_response(self.req, self.resp)
self.assertEqual(r.content, self.uncompressible_string)
self.assertEqual(r.get('Content-Encoding'), None)

0 comments on commit 4288c88

Please sign in to comment.