Index: django/http/__init__.py
===================================================================
--- django/http/__init__.py	(revision 17342)
+++ django/http/__init__.py	(working copy)
@@ -485,7 +485,7 @@
     status_code = 200
 
     def __init__(self, content='', mimetype=None, status=None,
-            content_type=None):
+            content_type=None, stream_content=False):
         # _headers is a mapping of the lower-case name to the original case of
         # the header (required for working with legacy systems) and the header
         # value.  Both the name of the header and its value are ASCII strings.
@@ -496,12 +496,8 @@
         if not content_type:
             content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE,
                     self._charset)
-        if not isinstance(content, basestring) and hasattr(content, '__iter__'):
-            self._container = content
-            self._is_string = False
-        else:
-            self._container = [content]
-            self._is_string = True
+        self._stream_content = stream_content
+        self.content = content
         self.cookies = SimpleCookie()
         if status:
             self.status_code = status
@@ -595,17 +591,35 @@
         self.set_cookie(key, max_age=0, path=path, domain=domain,
                         expires='Thu, 01-Jan-1970 00:00:00 GMT')
 
+    def _consume_content(self):
+        if not self._is_string:
+            self._container = [''.join(self._container)]
+            self._is_string = True
+
     def _get_content(self):
+        self._consume_content()
         if self.has_header('Content-Encoding'):
             return ''.join(self._container)
         return smart_str(''.join(self._container), self._charset)
 
     def _set_content(self, value):
-        self._container = [value]
-        self._is_string = True
+        if not isinstance(value, basestring) and hasattr(value, '__iter__'):
+            self._container = value
+            self._is_string = False
+            if not self._stream_content:
+                self._consume_content()
+        else:
+            self._container = [value]
+            self._is_string = True
 
     content = property(_get_content, _set_content)
 
+    def _get_content_generator(self):
+        if not self._is_string:
+            return (smart_str(item, self._charset) for item in self._container)
+
+    content_generator = property(_get_content_generator)
+
     def __iter__(self):
         self._iterator = iter(self._container)
         return self
Index: django/utils/text.py
===================================================================
--- django/utils/text.py	(revision 17342)
+++ django/utils/text.py	(working copy)
@@ -187,6 +187,24 @@
     zfile.close()
     return zbuf.getvalue()
 
+# WARNING - be aware that compress_sequence does not achieve the same
+# level of compression as compress_string
+def compress_sequence(sequence):
+    import cStringIO, gzip
+    zbuf = cStringIO.StringIO()
+    zfile = gzip.GzipFile(mode='wb', compresslevel=6, fileobj=zbuf)
+    yield zbuf.getvalue()
+    for item in sequence:
+        position = zbuf.tell()
+        zfile.write(item)
+        zfile.flush()
+        zbuf.seek(position)
+        yield zbuf.read()
+    position = zbuf.tell()
+    zfile.close()
+    zbuf.seek(position)
+    yield zbuf.read()
+
 ustring_re = re.compile(u"([\u0080-\uffff])")
 
 def javascript_quote(s, quote_double_quotes=False):
Index: django/middleware/common.py
===================================================================
--- django/middleware/common.py	(revision 17342)
+++ django/middleware/common.py	(working copy)
@@ -112,15 +112,19 @@
         if settings.USE_ETAGS:
             if response.has_header('ETag'):
                 etag = response['ETag']
-            else:
+            elif not response.content_generator:
                 etag = '"%s"' % md5_constructor(response.content).hexdigest()
-            if response.status_code >= 200 and response.status_code < 300 and request.META.get('HTTP_IF_NONE_MATCH') == etag:
-                cookies = response.cookies
-                response = http.HttpResponseNotModified()
-                response.cookies = cookies
-            else:
-                response['ETag'] = etag
 
+            try:
+                if response.status_code >= 200 and response.status_code < 300 and request.META.get('HTTP_IF_NONE_MATCH') == etag:
+                    cookies = response.cookies
+                    response = http.HttpResponseNotModified()
+                    response.cookies = cookies
+                else:
+                    response['ETag'] = etag
+            except NameError:
+                pass
+
         return response
 
 def _is_ignorable_404(uri):
Index: django/middleware/gzip.py
===================================================================
--- django/middleware/gzip.py	(revision 17342)
+++ django/middleware/gzip.py	(working copy)
@@ -1,6 +1,6 @@
 import re
 
-from django.utils.text import compress_string
+from django.utils.text import compress_sequence, compress_string
 from django.utils.cache import patch_vary_headers
 
 re_accepts_gzip = re.compile(r'\bgzip\b')
@@ -13,7 +13,7 @@
     """
     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:
+        if response.status_code != 200 or (not response.content_generator and len(response.content) < 200):
             return response
 
         patch_vary_headers(response, ('Accept-Encoding',))
@@ -32,7 +32,11 @@
         if not re_accepts_gzip.search(ae):
             return response
 
-        response.content = compress_string(response.content)
+        if response.content_generator:
+            response.content = compress_sequence(response.content_generator)
+            del response['Content-Length']
+        else:
+            response.content = compress_string(response.content)
+            response['Content-Length'] = str(len(response.content))
         response['Content-Encoding'] = 'gzip'
-        response['Content-Length'] = str(len(response.content))
         return response
Index: django/middleware/http.py
===================================================================
--- django/middleware/http.py	(revision 17342)
+++ django/middleware/http.py	(working copy)
@@ -11,7 +11,7 @@
     """
     def process_response(self, request, response):
         response['Date'] = http_date()
-        if not response.has_header('Content-Length'):
+        if not response.has_header('Content-Length') and not response.content_generator:
             response['Content-Length'] = str(len(response.content))
 
         if response.has_header('ETag'):
