mirror of
				https://github.com/django/django.git
				synced 2025-10-30 17:16:10 +00:00 
			
		
		
		
	Fixed #7581 -- Added streaming responses.
Thanks mrmachine and everyone else involved on this long-standing ticket.
This commit is contained in:
		| @@ -528,18 +528,23 @@ def parse_cookie(cookie): | ||||
| class BadHeaderError(ValueError): | ||||
|     pass | ||||
|  | ||||
| class HttpResponse(object): | ||||
|     """A basic HTTP response, with content and dictionary-accessed headers.""" | ||||
| class HttpResponseBase(object): | ||||
|     """ | ||||
|     An HTTP response base class with dictionary-accessed headers. | ||||
|  | ||||
|     This class doesn't handle content. It should not be used directly. | ||||
|     Use the HttpResponse and StreamingHttpResponse subclasses instead. | ||||
|     """ | ||||
|  | ||||
|     status_code = 200 | ||||
|  | ||||
|     def __init__(self, content='', content_type=None, status=None, | ||||
|             mimetype=None): | ||||
|     def __init__(self, content_type=None, status=None, mimetype=None): | ||||
|         # _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. | ||||
|         self._headers = {} | ||||
|         self._charset = settings.DEFAULT_CHARSET | ||||
|         self._closable_objects = [] | ||||
|         if mimetype: | ||||
|             warnings.warn("Using mimetype keyword argument is deprecated, use" | ||||
|                           " content_type instead", PendingDeprecationWarning) | ||||
| @@ -547,26 +552,24 @@ class HttpResponse(object): | ||||
|         if not content_type: | ||||
|             content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, | ||||
|                     self._charset) | ||||
|         # content is a bytestring. See the content property methods. | ||||
|         self.content = content | ||||
|         self.cookies = SimpleCookie() | ||||
|         if status: | ||||
|             self.status_code = status | ||||
|  | ||||
|         self['Content-Type'] = content_type | ||||
|  | ||||
|     def serialize(self): | ||||
|         """Full HTTP message, including headers, as a bytestring.""" | ||||
|     def serialize_headers(self): | ||||
|         """HTTP headers as a bytestring.""" | ||||
|         headers = [ | ||||
|             ('%s: %s' % (key, value)).encode('us-ascii') | ||||
|             for key, value in self._headers.values() | ||||
|         ] | ||||
|         return b'\r\n'.join(headers) + b'\r\n\r\n' + self.content | ||||
|         return b'\r\n'.join(headers) | ||||
|  | ||||
|     if six.PY3: | ||||
|         __bytes__ = serialize | ||||
|         __bytes__ = serialize_headers | ||||
|     else: | ||||
|         __str__ = serialize | ||||
|         __str__ = serialize_headers | ||||
|  | ||||
|     def _convert_to_charset(self, value, charset, mime_encode=False): | ||||
|         """Converts headers key/value to ascii/latin1 native strings. | ||||
| @@ -690,24 +693,75 @@ class HttpResponse(object): | ||||
|         self.set_cookie(key, max_age=0, path=path, domain=domain, | ||||
|                         expires='Thu, 01-Jan-1970 00:00:00 GMT') | ||||
|  | ||||
|     # Common methods used by subclasses | ||||
|  | ||||
|     def make_bytes(self, value): | ||||
|         """Turn a value into a bytestring encoded in the output charset.""" | ||||
|         # For backwards compatibility, this method supports values that are | ||||
|         # unlikely to occur in real applications. It has grown complex and | ||||
|         # should be refactored. It also overlaps __next__. See #18796. | ||||
|         if self.has_header('Content-Encoding'): | ||||
|             if isinstance(value, int): | ||||
|                 value = six.text_type(value) | ||||
|             if isinstance(value, six.text_type): | ||||
|                 value = value.encode('ascii') | ||||
|             # force conversion to bytes in case chunk is a subclass | ||||
|             return bytes(value) | ||||
|         else: | ||||
|             return force_bytes(value, self._charset) | ||||
|  | ||||
|     # These methods partially implement the file-like object interface. | ||||
|     # See http://docs.python.org/lib/bltin-file-objects.html | ||||
|  | ||||
|     # The WSGI server must call this method upon completion of the request. | ||||
|     # See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html | ||||
|     def close(self): | ||||
|         for closable in self._closable_objects: | ||||
|             closable.close() | ||||
|  | ||||
|     def write(self, content): | ||||
|         raise Exception("This %s instance is not writable" % self.__class__.__name__) | ||||
|  | ||||
|     def flush(self): | ||||
|         pass | ||||
|  | ||||
|     def tell(self): | ||||
|         raise Exception("This %s instance cannot tell its position" % self.__class__.__name__) | ||||
|  | ||||
| class HttpResponse(HttpResponseBase): | ||||
|     """ | ||||
|     An HTTP response class with a string as content. | ||||
|  | ||||
|     This content that can be read, appended to or replaced. | ||||
|     """ | ||||
|  | ||||
|     streaming = False | ||||
|  | ||||
|     def __init__(self, content='', *args, **kwargs): | ||||
|         super(HttpResponse, self).__init__(*args, **kwargs) | ||||
|         # Content is a bytestring. See the `content` property methods. | ||||
|         self.content = content | ||||
|  | ||||
|     def serialize(self): | ||||
|         """Full HTTP message, including headers, as a bytestring.""" | ||||
|         return self.serialize_headers() + b'\r\n\r\n' + self.content | ||||
|  | ||||
|     if six.PY3: | ||||
|         __bytes__ = serialize | ||||
|     else: | ||||
|         __str__ = serialize | ||||
|  | ||||
|     @property | ||||
|     def content(self): | ||||
|         if self.has_header('Content-Encoding'): | ||||
|             def make_bytes(value): | ||||
|                 if isinstance(value, int): | ||||
|                     value = six.text_type(value) | ||||
|                 if isinstance(value, six.text_type): | ||||
|                     value = value.encode('ascii') | ||||
|                 # force conversion to bytes in case chunk is a subclass | ||||
|                 return bytes(value) | ||||
|             return b''.join(make_bytes(e) for e in self._container) | ||||
|         return b''.join(force_bytes(e, self._charset) for e in self._container) | ||||
|         return b''.join(self.make_bytes(e) for e in self._container) | ||||
|  | ||||
|     @content.setter | ||||
|     def content(self, value): | ||||
|         if hasattr(value, '__iter__') and not isinstance(value, (bytes, six.string_types)): | ||||
|             self._container = value | ||||
|             self._base_content_is_iter = True | ||||
|             if hasattr(value, 'close'): | ||||
|                 self._closable_objects.append(value) | ||||
|         else: | ||||
|             self._container = [value] | ||||
|             self._base_content_is_iter = False | ||||
| @@ -727,25 +781,85 @@ class HttpResponse(object): | ||||
|  | ||||
|     next = __next__             # Python 2 compatibility | ||||
|  | ||||
|     def close(self): | ||||
|         if hasattr(self._container, 'close'): | ||||
|             self._container.close() | ||||
|  | ||||
|     # The remaining methods partially implement the file-like object interface. | ||||
|     # See http://docs.python.org/lib/bltin-file-objects.html | ||||
|     def write(self, content): | ||||
|         if self._base_content_is_iter: | ||||
|             raise Exception("This %s instance is not writable" % self.__class__) | ||||
|             raise Exception("This %s instance is not writable" % self.__class__.__name__) | ||||
|         self._container.append(content) | ||||
|  | ||||
|     def flush(self): | ||||
|         pass | ||||
|  | ||||
|     def tell(self): | ||||
|         if self._base_content_is_iter: | ||||
|             raise Exception("This %s instance cannot tell its position" % self.__class__) | ||||
|             raise Exception("This %s instance cannot tell its position" % self.__class__.__name__) | ||||
|         return sum([len(chunk) for chunk in self]) | ||||
|  | ||||
| class StreamingHttpResponse(HttpResponseBase): | ||||
|     """ | ||||
|     A streaming HTTP response class with an iterator as content. | ||||
|  | ||||
|     This should only be iterated once, when the response is streamed to the | ||||
|     client. However, it can be appended to or replaced with a new iterator | ||||
|     that wraps the original content (or yields entirely new content). | ||||
|     """ | ||||
|  | ||||
|     streaming = True | ||||
|  | ||||
|     def __init__(self, streaming_content=(), *args, **kwargs): | ||||
|         super(StreamingHttpResponse, self).__init__(*args, **kwargs) | ||||
|         # `streaming_content` should be an iterable of bytestrings. | ||||
|         # See the `streaming_content` property methods. | ||||
|         self.streaming_content = streaming_content | ||||
|  | ||||
|     @property | ||||
|     def content(self): | ||||
|         raise AttributeError("This %s instance has no `content` attribute. " | ||||
|             "Use `streaming_content` instead." % self.__class__.__name__) | ||||
|  | ||||
|     @property | ||||
|     def streaming_content(self): | ||||
|         return self._iterator | ||||
|  | ||||
|     @streaming_content.setter | ||||
|     def streaming_content(self, value): | ||||
|         # Ensure we can never iterate on "value" more than once. | ||||
|         self._iterator = iter(value) | ||||
|         if hasattr(value, 'close'): | ||||
|             self._closable_objects.append(value) | ||||
|  | ||||
|     def __iter__(self): | ||||
|         return self | ||||
|  | ||||
|     def __next__(self): | ||||
|         return self.make_bytes(next(self._iterator)) | ||||
|  | ||||
|     next = __next__             # Python 2 compatibility | ||||
|  | ||||
| class CompatibleStreamingHttpResponse(StreamingHttpResponse): | ||||
|     """ | ||||
|     This class maintains compatibility with middleware that doesn't know how | ||||
|     to handle the content of a streaming response by exposing a `content` | ||||
|     attribute that will consume and cache the content iterator when accessed. | ||||
|  | ||||
|     These responses will stream only if no middleware attempts to access the | ||||
|     `content` attribute. Otherwise, they will behave like a regular response, | ||||
|     and raise a `PendingDeprecationWarning`. | ||||
|     """ | ||||
|     @property | ||||
|     def content(self): | ||||
|         warnings.warn( | ||||
|             'Accessing the `content` attribute on a streaming response is ' | ||||
|             'deprecated. Use the `streaming_content` attribute instead.', | ||||
|             PendingDeprecationWarning) | ||||
|         content = b''.join(self) | ||||
|         self.streaming_content = [content] | ||||
|         return content | ||||
|  | ||||
|     @content.setter | ||||
|     def content(self, content): | ||||
|         warnings.warn( | ||||
|             'Accessing the `content` attribute on a streaming response is ' | ||||
|             'deprecated. Use the `streaming_content` attribute instead.', | ||||
|             PendingDeprecationWarning) | ||||
|         self.streaming_content = [content] | ||||
|  | ||||
| class HttpResponseRedirectBase(HttpResponse): | ||||
|     allowed_schemes = ['http', 'https', 'ftp'] | ||||
|  | ||||
|   | ||||
| @@ -26,10 +26,16 @@ def conditional_content_removal(request, response): | ||||
|     responses. Ensures compliance with RFC 2616, section 4.3. | ||||
|     """ | ||||
|     if 100 <= response.status_code < 200 or response.status_code in (204, 304): | ||||
|        response.content = '' | ||||
|        response['Content-Length'] = 0 | ||||
|         if response.streaming: | ||||
|             response.streaming_content = [] | ||||
|         else: | ||||
|             response.content = '' | ||||
|         response['Content-Length'] = '0' | ||||
|     if request.method == 'HEAD': | ||||
|         response.content = '' | ||||
|         if response.streaming: | ||||
|             response.streaming_content = [] | ||||
|         else: | ||||
|             response.content = '' | ||||
|     return response | ||||
|  | ||||
| def fix_IE_for_attach(request, response): | ||||
|   | ||||
| @@ -113,14 +113,18 @@ class CommonMiddleware(object): | ||||
|         if settings.USE_ETAGS: | ||||
|             if response.has_header('ETag'): | ||||
|                 etag = response['ETag'] | ||||
|             elif response.streaming: | ||||
|                 etag = None | ||||
|             else: | ||||
|                 etag = '"%s"' % hashlib.md5(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 | ||||
|             if etag is not None: | ||||
|                 if (200 <= 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 | ||||
|  | ||||
|         return response | ||||
|  | ||||
|   | ||||
| @@ -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 @@ class GZipMiddleware(object): | ||||
|     """ | ||||
|     def process_response(self, request, response): | ||||
|         # It's not worth attempting to compress really short responses. | ||||
|         if len(response.content) < 200: | ||||
|         if not response.streaming and len(response.content) < 200: | ||||
|             return response | ||||
|  | ||||
|         patch_vary_headers(response, ('Accept-Encoding',)) | ||||
| @@ -32,15 +32,21 @@ class GZipMiddleware(object): | ||||
|         if not re_accepts_gzip.search(ae): | ||||
|             return response | ||||
|  | ||||
|         # 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 | ||||
|         if response.streaming: | ||||
|             # Delete the `Content-Length` header for streaming content, because | ||||
|             # we won't know the compressed size until we stream it. | ||||
|             response.streaming_content = compress_sequence(response.streaming_content) | ||||
|             del response['Content-Length'] | ||||
|         else: | ||||
|             # 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-Length'] = str(len(response.content)) | ||||
|  | ||||
|         if response.has_header('ETag'): | ||||
|             response['ETag'] = re.sub('"$', ';gzip"', response['ETag']) | ||||
|  | ||||
|         response.content = compressed_content | ||||
|         response['Content-Encoding'] = 'gzip' | ||||
|         response['Content-Length'] = str(len(response.content)) | ||||
|  | ||||
|         return response | ||||
|   | ||||
| @@ -10,7 +10,7 @@ class ConditionalGetMiddleware(object): | ||||
|     """ | ||||
|     def process_response(self, request, response): | ||||
|         response['Date'] = http_date() | ||||
|         if not response.has_header('Content-Length'): | ||||
|         if not response.streaming and not response.has_header('Content-Length'): | ||||
|             response['Content-Length'] = str(len(response.content)) | ||||
|  | ||||
|         if response.has_header('ETag'): | ||||
|   | ||||
| @@ -596,7 +596,9 @@ class TransactionTestCase(SimpleTestCase): | ||||
|             msg_prefix + "Couldn't retrieve content: Response code was %d" | ||||
|             " (expected %d)" % (response.status_code, status_code)) | ||||
|         text = force_text(text, encoding=response._charset) | ||||
|         content = response.content.decode(response._charset) | ||||
|         content = b''.join(response).decode(response._charset) | ||||
|         # Avoid ResourceWarning about unclosed files. | ||||
|         response.close() | ||||
|         if html: | ||||
|             content = assert_and_parse_html(self, content, None, | ||||
|                 "Response's content is not valid HTML:") | ||||
|   | ||||
| @@ -95,7 +95,8 @@ def get_max_age(response): | ||||
|             pass | ||||
|  | ||||
| def _set_response_etag(response): | ||||
|     response['ETag'] = '"%s"' % hashlib.md5(response.content).hexdigest() | ||||
|     if not response.streaming: | ||||
|         response['ETag'] = '"%s"' % hashlib.md5(response.content).hexdigest() | ||||
|     return response | ||||
|  | ||||
| def patch_response_headers(response, cache_timeout=None): | ||||
|   | ||||
| @@ -288,6 +288,37 @@ def compress_string(s): | ||||
|     zfile.close() | ||||
|     return zbuf.getvalue() | ||||
|  | ||||
| class StreamingBuffer(object): | ||||
|     def __init__(self): | ||||
|         self.vals = [] | ||||
|  | ||||
|     def write(self, val): | ||||
|         self.vals.append(val) | ||||
|  | ||||
|     def read(self): | ||||
|         ret = b''.join(self.vals) | ||||
|         self.vals = [] | ||||
|         return ret | ||||
|  | ||||
|     def flush(self): | ||||
|         return | ||||
|  | ||||
|     def close(self): | ||||
|         return | ||||
|  | ||||
| # Like compress_string, but for iterators of strings. | ||||
| def compress_sequence(sequence): | ||||
|     buf = StreamingBuffer() | ||||
|     zfile = GzipFile(mode='wb', compresslevel=6, fileobj=buf) | ||||
|     # Output headers... | ||||
|     yield buf.read() | ||||
|     for item in sequence: | ||||
|         zfile.write(item) | ||||
|         zfile.flush() | ||||
|         yield buf.read() | ||||
|     zfile.close() | ||||
|     yield buf.read() | ||||
|  | ||||
| ustring_re = re.compile("([\u0080-\uffff])") | ||||
|  | ||||
| def javascript_quote(s, quote_double_quotes=False): | ||||
|   | ||||
| @@ -99,7 +99,7 @@ class View(object): | ||||
|         """ | ||||
|         response = http.HttpResponse() | ||||
|         response['Allow'] = ', '.join(self._allowed_methods()) | ||||
|         response['Content-Length'] = 0 | ||||
|         response['Content-Length'] = '0' | ||||
|         return response | ||||
|  | ||||
|     def _allowed_methods(self): | ||||
|   | ||||
| @@ -14,7 +14,8 @@ try: | ||||
| except ImportError:     # Python 2 | ||||
|     from urllib import unquote | ||||
|  | ||||
| from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified | ||||
| from django.http import (CompatibleStreamingHttpResponse, Http404, | ||||
|     HttpResponse, HttpResponseRedirect, HttpResponseNotModified) | ||||
| from django.template import loader, Template, Context, TemplateDoesNotExist | ||||
| from django.utils.http import http_date, parse_http_date | ||||
| from django.utils.translation import ugettext as _, ugettext_noop | ||||
| @@ -62,8 +63,7 @@ def serve(request, path, document_root=None, show_indexes=False): | ||||
|     if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), | ||||
|                               statobj.st_mtime, statobj.st_size): | ||||
|         return HttpResponseNotModified() | ||||
|     with open(fullpath, 'rb') as f: | ||||
|         response = HttpResponse(f.read(), content_type=mimetype) | ||||
|     response = CompatibleStreamingHttpResponse(open(fullpath, 'rb'), content_type=mimetype) | ||||
|     response["Last-Modified"] = http_date(statobj.st_mtime) | ||||
|     if stat.S_ISREG(statobj.st_mode): | ||||
|         response["Content-Length"] = statobj.st_size | ||||
|   | ||||
| @@ -566,13 +566,21 @@ file-like object:: | ||||
| Passing iterators | ||||
| ~~~~~~~~~~~~~~~~~ | ||||
|  | ||||
| Finally, you can pass ``HttpResponse`` an iterator rather than passing it | ||||
| hard-coded strings. If you use this technique, follow these guidelines: | ||||
| Finally, you can pass ``HttpResponse`` an iterator rather than strings. If you | ||||
| use this technique, the iterator should return strings. | ||||
|  | ||||
| * The iterator should return strings. | ||||
| * If an :class:`HttpResponse` has been initialized with an iterator as its | ||||
|   content, you can't use the :class:`HttpResponse` instance as a file-like | ||||
|   object. Doing so will raise ``Exception``. | ||||
| .. versionchanged:: 1.5 | ||||
|  | ||||
|     Passing an iterator as content to :class:`HttpResponse` creates a | ||||
|     streaming response if (and only if) no middleware accesses the | ||||
|     :attr:`HttpResponse.content` attribute before the response is returned. | ||||
|  | ||||
|     If you want to guarantee that your response will stream to the client, you | ||||
|     should use the new :class:`StreamingHttpResponse` class instead. | ||||
|  | ||||
| If an :class:`HttpResponse` instance has been initialized with an iterator as | ||||
| its content, you can't use it as a file-like object. Doing so will raise an | ||||
| exception. | ||||
|  | ||||
| Setting headers | ||||
| ~~~~~~~~~~~~~~~ | ||||
| @@ -614,6 +622,13 @@ Attributes | ||||
|  | ||||
|     The `HTTP Status code`_ for the response. | ||||
|  | ||||
| .. attribute:: HttpResponse.streaming | ||||
|  | ||||
|     This is always ``False``. | ||||
|  | ||||
|     This attribute exists so middleware can treat streaming responses | ||||
|     differently from regular responses. | ||||
|  | ||||
| Methods | ||||
| ------- | ||||
|  | ||||
| @@ -781,3 +796,63 @@ types of HTTP responses. Like ``HttpResponse``, these subclasses live in | ||||
|     method, Django will treat it as emulating a | ||||
|     :class:`~django.template.response.SimpleTemplateResponse`, and the | ||||
|     ``render`` method must itself return a valid response object. | ||||
|  | ||||
| StreamingHttpResponse objects | ||||
| ============================= | ||||
|  | ||||
| .. versionadded:: 1.5 | ||||
|  | ||||
| .. class:: StreamingHttpResponse | ||||
|  | ||||
| The :class:`StreamingHttpResponse` class is used to stream a response from | ||||
| Django to the browser. You might want to do this if generating the response | ||||
| takes too long or uses too much memory. For instance, it's useful for | ||||
| generating large CSV files. | ||||
|  | ||||
| .. admonition:: Performance considerations | ||||
|  | ||||
|     Django is designed for short-lived requests. Streaming responses will tie | ||||
|     a worker process and keep a database connection idle in transaction for | ||||
|     the entire duration of the response. This may result in poor performance. | ||||
|  | ||||
|     Generally speaking, you should perform expensive tasks outside of the | ||||
|     request-response cycle, rather than resorting to a streamed response. | ||||
|  | ||||
| The :class:`StreamingHttpResponse` is not a subclass of :class:`HttpResponse`, | ||||
| because it features a slightly different API. However, it is almost identical, | ||||
| with the following notable differences: | ||||
|  | ||||
| * It should be given an iterator that yields strings as content. | ||||
|  | ||||
| * You cannot access its content, except by iterating the response object | ||||
|   itself. This should only occur when the response is returned to the client. | ||||
|  | ||||
| * It has no ``content`` attribute. Instead, it has a | ||||
|   :attr:`~StreamingHttpResponse.streaming_content` attribute. | ||||
|  | ||||
| * You cannot use the file-like object ``tell()`` or ``write()`` methods. | ||||
|   Doing so will raise an exception. | ||||
|  | ||||
| * Any iterators that have a ``close()`` method and are assigned as content will | ||||
|   be closed automatically after the response has been iterated. | ||||
|  | ||||
| :class:`StreamingHttpResponse` should only be used in situations where it is | ||||
| absolutely required that the whole content isn't iterated before transferring | ||||
| the data to the client. Because the content can't be accessed, many | ||||
| middlewares can't function normally. For example the ``ETag`` and ``Content- | ||||
| Length`` headers can't be generated for streaming responses. | ||||
|  | ||||
| Attributes | ||||
| ---------- | ||||
|  | ||||
| .. attribute:: StreamingHttpResponse.streaming_content | ||||
|  | ||||
|     An iterator of strings representing the content. | ||||
|  | ||||
| .. attribute:: HttpResponse.status_code | ||||
|  | ||||
|     The `HTTP Status code`_ for the response. | ||||
|  | ||||
| .. attribute:: HttpResponse.streaming | ||||
|  | ||||
|     This is always ``True``. | ||||
|   | ||||
| @@ -84,6 +84,24 @@ For one-to-one relationships, both sides can be cached. For many-to-one | ||||
| relationships, only the single side of the relationship can be cached. This | ||||
| is particularly helpful in combination with ``prefetch_related``. | ||||
|  | ||||
| Explicit support for streaming responses | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
|  | ||||
| Before Django 1.5, it was possible to create a streaming response by passing | ||||
| an iterator to :class:`~django.http.HttpResponse`. But this was unreliable: | ||||
| any middleware that accessed the :attr:`~django.http.HttpResponse.content` | ||||
| attribute would consume the iterator prematurely. | ||||
|  | ||||
| You can now explicitly generate a streaming response with the new | ||||
| :class:`~django.http.StreamingHttpResponse` class. This class exposes a | ||||
| :class:`~django.http.StreamingHttpResponse.streaming_content` attribute which | ||||
| is an iterator. | ||||
|  | ||||
| Since :class:`~django.http.StreamingHttpResponse` does not have a ``content`` | ||||
| attribute, middleware that need access to the response content must test for | ||||
| streaming responses and behave accordingly. See :ref:`response-middleware` for | ||||
| more information. | ||||
|  | ||||
| ``{% verbatim %}`` template tag | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
|  | ||||
|   | ||||
| @@ -164,6 +164,23 @@ an earlier middleware method returned an :class:`~django.http.HttpResponse` | ||||
| classes are applied in reverse order, from the bottom up. This means classes | ||||
| defined at the end of :setting:`MIDDLEWARE_CLASSES` will be run first. | ||||
|  | ||||
| .. versionchanged:: 1.5 | ||||
|     ``response`` may also be an :class:`~django.http.StreamingHttpResponse` | ||||
|     object. | ||||
|  | ||||
| Unlike :class:`~django.http.HttpResponse`, | ||||
| :class:`~django.http.StreamingHttpResponse` does not have a ``content`` | ||||
| attribute. As a result, middleware can no longer assume that all responses | ||||
| will have a ``content`` attribute. If they need access to the content, they | ||||
| must test for streaming responses and adjust their behavior accordingly:: | ||||
|  | ||||
|     if response.streaming: | ||||
|         response.streaming_content = wrap_streaming_content(response.streaming_content) | ||||
|     else: | ||||
|         response.content = wrap_content(response.content) | ||||
|  | ||||
| ``streaming_content`` should be assumed to be too large to hold in memory. | ||||
| Middleware may wrap it in a new generator, but must not consume it. | ||||
|  | ||||
| .. _exception-middleware: | ||||
|  | ||||
|   | ||||
							
								
								
									
										26
									
								
								tests/regressiontests/cache/tests.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								tests/regressiontests/cache/tests.py
									
									
									
									
										vendored
									
									
								
							| @@ -19,7 +19,8 @@ from django.core.cache import get_cache | ||||
| from django.core.cache.backends.base import (CacheKeyWarning, | ||||
|     InvalidCacheBackendError) | ||||
| from django.db import router | ||||
| from django.http import HttpResponse, HttpRequest, QueryDict | ||||
| from django.http import (HttpResponse, HttpRequest, StreamingHttpResponse, | ||||
|     QueryDict) | ||||
| from django.middleware.cache import (FetchFromCacheMiddleware, | ||||
|     UpdateCacheMiddleware, CacheMiddleware) | ||||
| from django.template import Template | ||||
| @@ -1416,6 +1417,29 @@ class CacheI18nTest(TestCase): | ||||
|         # reset the language | ||||
|         translation.deactivate() | ||||
|  | ||||
|     @override_settings( | ||||
|             CACHE_MIDDLEWARE_KEY_PREFIX="test", | ||||
|             CACHE_MIDDLEWARE_SECONDS=60, | ||||
|             USE_ETAGS=True, | ||||
|     ) | ||||
|     def test_middleware_with_streaming_response(self): | ||||
|         # cache with non empty request.GET | ||||
|         request = self._get_request_cache(query_string='foo=baz&other=true') | ||||
|  | ||||
|         # first access, cache must return None | ||||
|         get_cache_data = FetchFromCacheMiddleware().process_request(request) | ||||
|         self.assertEqual(get_cache_data, None) | ||||
|  | ||||
|         # pass streaming response through UpdateCacheMiddleware. | ||||
|         content = 'Check for cache with QUERY_STRING and streaming content' | ||||
|         response = StreamingHttpResponse(content) | ||||
|         UpdateCacheMiddleware().process_response(request, response) | ||||
|  | ||||
|         # second access, cache must still return None, because we can't cache | ||||
|         # streaming response. | ||||
|         get_cache_data = FetchFromCacheMiddleware().process_request(request) | ||||
|         self.assertEqual(get_cache_data, None) | ||||
|  | ||||
|  | ||||
| @override_settings( | ||||
|         CACHES={ | ||||
|   | ||||
							
								
								
									
										1
									
								
								tests/regressiontests/httpwrappers/abc.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/regressiontests/httpwrappers/abc.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| random content | ||||
| @@ -2,12 +2,13 @@ | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| import copy | ||||
| import os | ||||
| import pickle | ||||
|  | ||||
| from django.core.exceptions import SuspiciousOperation | ||||
| from django.http import (QueryDict, HttpResponse, HttpResponseRedirect, | ||||
|                          HttpResponsePermanentRedirect, HttpResponseNotAllowed, | ||||
|                          HttpResponseNotModified, | ||||
|                          HttpResponseNotModified, StreamingHttpResponse, | ||||
|                          SimpleCookie, BadHeaderError, | ||||
|                          parse_cookie) | ||||
| from django.test import TestCase | ||||
| @@ -351,7 +352,6 @@ class HttpResponseTests(unittest.TestCase): | ||||
|             self.assertRaises(SuspiciousOperation, | ||||
|                               HttpResponsePermanentRedirect, url) | ||||
|  | ||||
|  | ||||
| class HttpResponseSubclassesTests(TestCase): | ||||
|     def test_redirect(self): | ||||
|         response = HttpResponseRedirect('/redirected/') | ||||
| @@ -379,6 +379,113 @@ class HttpResponseSubclassesTests(TestCase): | ||||
|             content_type='text/html') | ||||
|         self.assertContains(response, 'Only the GET method is allowed', status_code=405) | ||||
|  | ||||
| class StreamingHttpResponseTests(TestCase): | ||||
|     def test_streaming_response(self): | ||||
|         r = StreamingHttpResponse(iter(['hello', 'world'])) | ||||
|  | ||||
|         # iterating over the response itself yields bytestring chunks. | ||||
|         chunks = list(r) | ||||
|         self.assertEqual(chunks, [b'hello', b'world']) | ||||
|         for chunk in chunks: | ||||
|             self.assertIsInstance(chunk, six.binary_type) | ||||
|  | ||||
|         # and the response can only be iterated once. | ||||
|         self.assertEqual(list(r), []) | ||||
|  | ||||
|         # even when a sequence that can be iterated many times, like a list, | ||||
|         # is given as content. | ||||
|         r = StreamingHttpResponse(['abc', 'def']) | ||||
|         self.assertEqual(list(r), [b'abc', b'def']) | ||||
|         self.assertEqual(list(r), []) | ||||
|  | ||||
|         # streaming responses don't have a `content` attribute. | ||||
|         self.assertFalse(hasattr(r, 'content')) | ||||
|  | ||||
|         # and you can't accidentally assign to a `content` attribute. | ||||
|         with self.assertRaises(AttributeError): | ||||
|             r.content = 'xyz' | ||||
|  | ||||
|         # but they do have a `streaming_content` attribute. | ||||
|         self.assertTrue(hasattr(r, 'streaming_content')) | ||||
|  | ||||
|         # that exists so we can check if a response is streaming, and wrap or | ||||
|         # replace the content iterator. | ||||
|         r.streaming_content = iter(['abc', 'def']) | ||||
|         r.streaming_content = (chunk.upper() for chunk in r.streaming_content) | ||||
|         self.assertEqual(list(r), [b'ABC', b'DEF']) | ||||
|  | ||||
|         # coercing a streaming response to bytes doesn't return a complete HTTP | ||||
|         # message like a regular response does. it only gives us the headers. | ||||
|         r = StreamingHttpResponse(iter(['hello', 'world'])) | ||||
|         self.assertEqual( | ||||
|             six.binary_type(r), b'Content-Type: text/html; charset=utf-8') | ||||
|  | ||||
|         # and this won't consume its content. | ||||
|         self.assertEqual(list(r), [b'hello', b'world']) | ||||
|  | ||||
|         # additional content cannot be written to the response. | ||||
|         r = StreamingHttpResponse(iter(['hello', 'world'])) | ||||
|         with self.assertRaises(Exception): | ||||
|             r.write('!') | ||||
|  | ||||
|         # and we can't tell the current position. | ||||
|         with self.assertRaises(Exception): | ||||
|             r.tell() | ||||
|  | ||||
| class FileCloseTests(TestCase): | ||||
|     def test_response(self): | ||||
|         filename = os.path.join(os.path.dirname(__file__), 'abc.txt') | ||||
|  | ||||
|         # file isn't closed until we close the response. | ||||
|         file1 = open(filename) | ||||
|         r = HttpResponse(file1) | ||||
|         self.assertFalse(file1.closed) | ||||
|         r.close() | ||||
|         self.assertTrue(file1.closed) | ||||
|  | ||||
|         # don't automatically close file when we finish iterating the response. | ||||
|         file1 = open(filename) | ||||
|         r = HttpResponse(file1) | ||||
|         self.assertFalse(file1.closed) | ||||
|         list(r) | ||||
|         self.assertFalse(file1.closed) | ||||
|         r.close() | ||||
|         self.assertTrue(file1.closed) | ||||
|  | ||||
|         # when multiple file are assigned as content, make sure they are all | ||||
|         # closed with the response. | ||||
|         file1 = open(filename) | ||||
|         file2 = open(filename) | ||||
|         r = HttpResponse(file1) | ||||
|         r.content = file2 | ||||
|         self.assertFalse(file1.closed) | ||||
|         self.assertFalse(file2.closed) | ||||
|         r.close() | ||||
|         self.assertTrue(file1.closed) | ||||
|         self.assertTrue(file2.closed) | ||||
|  | ||||
|     def test_streaming_response(self): | ||||
|         filename = os.path.join(os.path.dirname(__file__), 'abc.txt') | ||||
|  | ||||
|         # file isn't closed until we close the response. | ||||
|         file1 = open(filename) | ||||
|         r = StreamingHttpResponse(file1) | ||||
|         self.assertFalse(file1.closed) | ||||
|         r.close() | ||||
|         self.assertTrue(file1.closed) | ||||
|  | ||||
|         # when multiple file are assigned as content, make sure they are all | ||||
|         # closed with the response. | ||||
|         file1 = open(filename) | ||||
|         file2 = open(filename) | ||||
|         r = StreamingHttpResponse(file1) | ||||
|         r.streaming_content = file2 | ||||
|         self.assertFalse(file1.closed) | ||||
|         self.assertFalse(file2.closed) | ||||
|         r.close() | ||||
|         self.assertTrue(file1.closed) | ||||
|         self.assertTrue(file2.closed) | ||||
|  | ||||
| class CookieTests(unittest.TestCase): | ||||
|     def test_encode(self): | ||||
|         """ | ||||
|   | ||||
| @@ -8,7 +8,7 @@ from io import BytesIO | ||||
| from django.conf import settings | ||||
| from django.core import mail | ||||
| from django.http import HttpRequest | ||||
| from django.http import HttpResponse | ||||
| from django.http import HttpResponse, StreamingHttpResponse | ||||
| from django.middleware.clickjacking import XFrameOptionsMiddleware | ||||
| from django.middleware.common import CommonMiddleware | ||||
| from django.middleware.http import ConditionalGetMiddleware | ||||
| @@ -322,6 +322,12 @@ class ConditionalGetMiddlewareTest(TestCase): | ||||
|         self.assertTrue('Content-Length' in self.resp) | ||||
|         self.assertEqual(int(self.resp['Content-Length']), content_length) | ||||
|  | ||||
|     def test_content_length_header_not_added(self): | ||||
|         resp = StreamingHttpResponse('content') | ||||
|         self.assertFalse('Content-Length' in resp) | ||||
|         resp = ConditionalGetMiddleware().process_response(self.req, resp) | ||||
|         self.assertFalse('Content-Length' in resp) | ||||
|  | ||||
|     def test_content_length_header_not_changed(self): | ||||
|         bad_content_length = len(self.resp.content) + 10 | ||||
|         self.resp['Content-Length'] = bad_content_length | ||||
| @@ -351,6 +357,29 @@ class ConditionalGetMiddlewareTest(TestCase): | ||||
|         self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp) | ||||
|         self.assertEqual(self.resp.status_code, 200) | ||||
|  | ||||
|     @override_settings(USE_ETAGS=True) | ||||
|     def test_etag(self): | ||||
|         req = HttpRequest() | ||||
|         res = HttpResponse('content') | ||||
|         self.assertTrue( | ||||
|             CommonMiddleware().process_response(req, res).has_header('ETag')) | ||||
|  | ||||
|     @override_settings(USE_ETAGS=True) | ||||
|     def test_etag_streaming_response(self): | ||||
|         req = HttpRequest() | ||||
|         res = StreamingHttpResponse(['content']) | ||||
|         res['ETag'] = 'tomatoes' | ||||
|         self.assertEqual( | ||||
|             CommonMiddleware().process_response(req, res).get('ETag'), | ||||
|             'tomatoes') | ||||
|  | ||||
|     @override_settings(USE_ETAGS=True) | ||||
|     def test_no_etag_streaming_response(self): | ||||
|         req = HttpRequest() | ||||
|         res = StreamingHttpResponse(['content']) | ||||
|         self.assertFalse( | ||||
|             CommonMiddleware().process_response(req, res).has_header('ETag')) | ||||
|  | ||||
|     # Tests for the Last-Modified header | ||||
|  | ||||
|     def test_if_modified_since_and_no_last_modified(self): | ||||
| @@ -511,6 +540,7 @@ class GZipMiddlewareTest(TestCase): | ||||
|     short_string = b"This string is too short to be worth compressing." | ||||
|     compressible_string = b'a' * 500 | ||||
|     uncompressible_string = b''.join(six.int2byte(random.randint(0, 255)) for _ in xrange(500)) | ||||
|     sequence = [b'a' * 500, b'b' * 200, b'a' * 300] | ||||
|  | ||||
|     def setUp(self): | ||||
|         self.req = HttpRequest() | ||||
| @@ -525,6 +555,8 @@ class GZipMiddlewareTest(TestCase): | ||||
|         self.resp.status_code = 200 | ||||
|         self.resp.content = self.compressible_string | ||||
|         self.resp['Content-Type'] = 'text/html; charset=UTF-8' | ||||
|         self.stream_resp = StreamingHttpResponse(self.sequence) | ||||
|         self.stream_resp['Content-Type'] = 'text/html; charset=UTF-8' | ||||
|  | ||||
|     @staticmethod | ||||
|     def decompress(gzipped_string): | ||||
| @@ -539,6 +571,15 @@ class GZipMiddlewareTest(TestCase): | ||||
|         self.assertEqual(r.get('Content-Encoding'), 'gzip') | ||||
|         self.assertEqual(r.get('Content-Length'), str(len(r.content))) | ||||
|  | ||||
|     def test_compress_streaming_response(self): | ||||
|         """ | ||||
|         Tests that compression is performed on responses with streaming content. | ||||
|         """ | ||||
|         r = GZipMiddleware().process_response(self.req, self.stream_resp) | ||||
|         self.assertEqual(self.decompress(b''.join(r)), b''.join(self.sequence)) | ||||
|         self.assertEqual(r.get('Content-Encoding'), 'gzip') | ||||
|         self.assertFalse(r.has_header('Content-Length')) | ||||
|  | ||||
|     def test_compress_non_200_response(self): | ||||
|         """ | ||||
|         Tests that compression is performed on responses with a status other than 200. | ||||
|   | ||||
| @@ -31,28 +31,35 @@ class StaticTests(TestCase): | ||||
|         media_files = ['file.txt', 'file.txt.gz'] | ||||
|         for filename in media_files: | ||||
|             response = self.client.get('/views/%s/%s' % (self.prefix, filename)) | ||||
|             response_content = b''.join(response) | ||||
|             response.close() | ||||
|             file_path = path.join(media_dir, filename) | ||||
|             with open(file_path, 'rb') as fp: | ||||
|                 self.assertEqual(fp.read(), response.content) | ||||
|             self.assertEqual(len(response.content), int(response['Content-Length'])) | ||||
|                 self.assertEqual(fp.read(), response_content) | ||||
|             self.assertEqual(len(response_content), int(response['Content-Length'])) | ||||
|             self.assertEqual(mimetypes.guess_type(file_path)[1], response.get('Content-Encoding', None)) | ||||
|  | ||||
|     def test_unknown_mime_type(self): | ||||
|         response = self.client.get('/views/%s/file.unknown' % self.prefix) | ||||
|         response.close() | ||||
|         self.assertEqual('application/octet-stream', response['Content-Type']) | ||||
|  | ||||
|     def test_copes_with_empty_path_component(self): | ||||
|         file_name = 'file.txt' | ||||
|         response = self.client.get('/views/%s//%s' % (self.prefix, file_name)) | ||||
|         response_content = b''.join(response) | ||||
|         response.close() | ||||
|         with open(path.join(media_dir, file_name), 'rb') as fp: | ||||
|             self.assertEqual(fp.read(), response.content) | ||||
|             self.assertEqual(fp.read(), response_content) | ||||
|  | ||||
|     def test_is_modified_since(self): | ||||
|         file_name = 'file.txt' | ||||
|         response = self.client.get('/views/%s/%s' % (self.prefix, file_name), | ||||
|             HTTP_IF_MODIFIED_SINCE='Thu, 1 Jan 1970 00:00:00 GMT') | ||||
|         response_content = b''.join(response) | ||||
|         response.close() | ||||
|         with open(path.join(media_dir, file_name), 'rb') as fp: | ||||
|             self.assertEqual(fp.read(), response.content) | ||||
|             self.assertEqual(fp.read(), response_content) | ||||
|  | ||||
|     def test_not_modified_since(self): | ||||
|         file_name = 'file.txt' | ||||
| @@ -74,9 +81,11 @@ class StaticTests(TestCase): | ||||
|         invalid_date = 'Mon, 28 May 999999999999 28:25:26 GMT' | ||||
|         response = self.client.get('/views/%s/%s' % (self.prefix, file_name), | ||||
|                                    HTTP_IF_MODIFIED_SINCE=invalid_date) | ||||
|         response_content = b''.join(response) | ||||
|         response.close() | ||||
|         with open(path.join(media_dir, file_name), 'rb') as fp: | ||||
|             self.assertEqual(fp.read(), response.content) | ||||
|         self.assertEqual(len(response.content), | ||||
|             self.assertEqual(fp.read(), response_content) | ||||
|         self.assertEqual(len(response_content), | ||||
|                           int(response['Content-Length'])) | ||||
|  | ||||
|     def test_invalid_if_modified_since2(self): | ||||
| @@ -89,9 +98,11 @@ class StaticTests(TestCase): | ||||
|         invalid_date = ': 1291108438, Wed, 20 Oct 2010 14:05:00 GMT' | ||||
|         response = self.client.get('/views/%s/%s' % (self.prefix, file_name), | ||||
|                                    HTTP_IF_MODIFIED_SINCE=invalid_date) | ||||
|         response_content = b''.join(response) | ||||
|         response.close() | ||||
|         with open(path.join(media_dir, file_name), 'rb') as fp: | ||||
|             self.assertEqual(fp.read(), response.content) | ||||
|         self.assertEqual(len(response.content), | ||||
|             self.assertEqual(fp.read(), response_content) | ||||
|         self.assertEqual(len(response_content), | ||||
|                           int(response['Content-Length'])) | ||||
|  | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user