mirror of
				https://github.com/django/django.git
				synced 2025-10-31 01:25:32 +00:00 
			
		
		
		
	Fixed #33735 -- Added async support to StreamingHttpResponse.
Thanks to Florian Vazelle for initial exploratory work, and to Nick Pope and Mariusz Felisiak for review.
This commit is contained in:
		| @@ -19,6 +19,7 @@ from django.http import ( | ||||
|     parse_cookie, | ||||
| ) | ||||
| from django.urls import set_script_prefix | ||||
| from django.utils.asyncio import aclosing | ||||
| from django.utils.functional import cached_property | ||||
|  | ||||
| logger = logging.getLogger("django.request") | ||||
| @@ -263,19 +264,22 @@ class ASGIHandler(base.BaseHandler): | ||||
|         ) | ||||
|         # Streaming responses need to be pinned to their iterator. | ||||
|         if response.streaming: | ||||
|             # Access `__iter__` and not `streaming_content` directly in case | ||||
|             # it has been overridden in a subclass. | ||||
|             for part in response: | ||||
|                 for chunk, _ in self.chunk_bytes(part): | ||||
|                     await send( | ||||
|                         { | ||||
|                             "type": "http.response.body", | ||||
|                             "body": chunk, | ||||
|                             # Ignore "more" as there may be more parts; instead, | ||||
|                             # use an empty final closing message with False. | ||||
|                             "more_body": True, | ||||
|                         } | ||||
|                     ) | ||||
|             # - Consume via `__aiter__` and not `streaming_content` directly, to | ||||
|             #   allow mapping of a sync iterator. | ||||
|             # - Use aclosing() when consuming aiter. | ||||
|             #   See https://github.com/python/cpython/commit/6e8dcda | ||||
|             async with aclosing(response.__aiter__()) as content: | ||||
|                 async for part in content: | ||||
|                     for chunk, _ in self.chunk_bytes(part): | ||||
|                         await send( | ||||
|                             { | ||||
|                                 "type": "http.response.body", | ||||
|                                 "body": chunk, | ||||
|                                 # Ignore "more" as there may be more parts; instead, | ||||
|                                 # use an empty final closing message with False. | ||||
|                                 "more_body": True, | ||||
|                             } | ||||
|                         ) | ||||
|             # Final closing message. | ||||
|             await send({"type": "http.response.body"}) | ||||
|         # Other responses just need chunking. | ||||
|   | ||||
| @@ -6,10 +6,13 @@ import os | ||||
| import re | ||||
| import sys | ||||
| import time | ||||
| import warnings | ||||
| from email.header import Header | ||||
| from http.client import responses | ||||
| from urllib.parse import urlparse | ||||
|  | ||||
| from asgiref.sync import async_to_sync, sync_to_async | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.core import signals, signing | ||||
| from django.core.exceptions import DisallowedRedirect | ||||
| @@ -476,7 +479,18 @@ class StreamingHttpResponse(HttpResponseBase): | ||||
|  | ||||
|     @property | ||||
|     def streaming_content(self): | ||||
|         return map(self.make_bytes, self._iterator) | ||||
|         if self.is_async: | ||||
|             # pull to lexical scope to capture fixed reference in case | ||||
|             # streaming_content is set again later. | ||||
|             _iterator = self._iterator | ||||
|  | ||||
|             async def awrapper(): | ||||
|                 async for part in _iterator: | ||||
|                     yield self.make_bytes(part) | ||||
|  | ||||
|             return awrapper() | ||||
|         else: | ||||
|             return map(self.make_bytes, self._iterator) | ||||
|  | ||||
|     @streaming_content.setter | ||||
|     def streaming_content(self, value): | ||||
| @@ -484,12 +498,48 @@ class StreamingHttpResponse(HttpResponseBase): | ||||
|  | ||||
|     def _set_streaming_content(self, value): | ||||
|         # Ensure we can never iterate on "value" more than once. | ||||
|         self._iterator = iter(value) | ||||
|         try: | ||||
|             self._iterator = iter(value) | ||||
|             self.is_async = False | ||||
|         except TypeError: | ||||
|             self._iterator = value.__aiter__() | ||||
|             self.is_async = True | ||||
|         if hasattr(value, "close"): | ||||
|             self._resource_closers.append(value.close) | ||||
|  | ||||
|     def __iter__(self): | ||||
|         return self.streaming_content | ||||
|         try: | ||||
|             return iter(self.streaming_content) | ||||
|         except TypeError: | ||||
|             warnings.warn( | ||||
|                 "StreamingHttpResponse must consume asynchronous iterators in order to " | ||||
|                 "serve them synchronously. Use a synchronous iterator instead.", | ||||
|                 Warning, | ||||
|             ) | ||||
|  | ||||
|             # async iterator. Consume in async_to_sync and map back. | ||||
|             async def to_list(_iterator): | ||||
|                 as_list = [] | ||||
|                 async for chunk in _iterator: | ||||
|                     as_list.append(chunk) | ||||
|                 return as_list | ||||
|  | ||||
|             return map(self.make_bytes, iter(async_to_sync(to_list)(self._iterator))) | ||||
|  | ||||
|     async def __aiter__(self): | ||||
|         try: | ||||
|             async for part in self.streaming_content: | ||||
|                 yield part | ||||
|         except TypeError: | ||||
|             warnings.warn( | ||||
|                 "StreamingHttpResponse must consume synchronous iterators in order to " | ||||
|                 "serve them asynchronously. Use an asynchronous iterator instead.", | ||||
|                 Warning, | ||||
|             ) | ||||
|             # sync iterator. Consume via sync_to_async and yield via async | ||||
|             # generator. | ||||
|             for part in await sync_to_async(list)(self.streaming_content): | ||||
|                 yield part | ||||
|  | ||||
|     def getvalue(self): | ||||
|         return b"".join(self.streaming_content) | ||||
|   | ||||
| @@ -31,12 +31,26 @@ class GZipMiddleware(MiddlewareMixin): | ||||
|             return response | ||||
|  | ||||
|         if response.streaming: | ||||
|             if response.is_async: | ||||
|                 # pull to lexical scope to capture fixed reference in case | ||||
|                 # streaming_content is set again later. | ||||
|                 orignal_iterator = response.streaming_content | ||||
|  | ||||
|                 async def gzip_wrapper(): | ||||
|                     async for chunk in orignal_iterator: | ||||
|                         yield compress_string( | ||||
|                             chunk, | ||||
|                             max_random_bytes=self.max_random_bytes, | ||||
|                         ) | ||||
|  | ||||
|                 response.streaming_content = gzip_wrapper() | ||||
|             else: | ||||
|                 response.streaming_content = compress_sequence( | ||||
|                     response.streaming_content, | ||||
|                     max_random_bytes=self.max_random_bytes, | ||||
|                 ) | ||||
|             # 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, | ||||
|                 max_random_bytes=self.max_random_bytes, | ||||
|             ) | ||||
|             del response.headers["Content-Length"] | ||||
|         else: | ||||
|             # Return the compressed content only if it's actually shorter. | ||||
|   | ||||
| @@ -37,3 +37,28 @@ def async_unsafe(message): | ||||
|         return decorator(func) | ||||
|     else: | ||||
|         return decorator | ||||
|  | ||||
|  | ||||
| try: | ||||
|     from contextlib import aclosing | ||||
| except ImportError: | ||||
|     # TODO: Remove when dropping support for PY39. | ||||
|     from contextlib import AbstractAsyncContextManager | ||||
|  | ||||
|     # Backport of contextlib.aclosing() from Python 3.10. Copyright (C) Python | ||||
|     # Software Foundation (see LICENSE.python). | ||||
|     class aclosing(AbstractAsyncContextManager): | ||||
|         """ | ||||
|         Async context manager for safely finalizing an asynchronously | ||||
|         cleaned-up resource such as an async generator, calling its | ||||
|         ``aclose()`` method. | ||||
|         """ | ||||
|  | ||||
|         def __init__(self, thing): | ||||
|             self.thing = thing | ||||
|  | ||||
|         async def __aenter__(self): | ||||
|             return self.thing | ||||
|  | ||||
|         async def __aexit__(self, *exc_info): | ||||
|             await self.thing.aclose() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user