diff --git a/django/core/handlers/asgi.py b/django/core/handlers/asgi.py index b4056ca042b8..af8582d539e7 100644 --- a/django/core/handlers/asgi.py +++ b/django/core/handlers/asgi.py @@ -3,7 +3,7 @@ import sys import tempfile import traceback -from contextlib import aclosing +from contextlib import aclosing, closing from asgiref.sync import ThreadSensitiveContext, sync_to_async @@ -174,65 +174,41 @@ async def handle(self, scope, receive, send): body_file = await self.read_body(receive) except RequestAborted: return - # Request is complete and can be served. - set_script_prefix(get_script_prefix(scope)) - await signals.request_started.asend(sender=self.__class__, scope=scope) - # Get the request and check for basic issues. - request, error_response = self.create_request(scope, body_file) - if request is None: - body_file.close() - await self.send_response(error_response, send) - await sync_to_async(error_response.close)() - return - async def process_request(request, send): - response = await self.run_get_response(request) - try: - await self.send_response(response, send) - except asyncio.CancelledError: - # Client disconnected during send_response (ignore exception). + with closing(body_file): + # Request is complete and can be served. + set_script_prefix(get_script_prefix(scope)) + await signals.request_started.asend(sender=self.__class__, scope=scope) + # Get the request and check for basic issues. + request, error_response = self.create_request(scope, body_file) + if request is None: + body_file.close() + await self.send_response(error_response, send) + await sync_to_async(error_response.close)() + return + + class RequestProcessed(Exception): pass - return response - - # Try to catch a disconnect while getting response. - tasks = [ - # Check the status of these tasks and (optionally) terminate them - # in this order. The listen_for_disconnect() task goes first - # because it should not raise unexpected errors that would prevent - # us from cancelling process_request(). - asyncio.create_task(self.listen_for_disconnect(receive)), - asyncio.create_task(process_request(request, send)), - ] - await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) - # Now wait on both tasks (they may have both finished by now). - for task in tasks: - if task.done(): - try: - task.result() - except RequestAborted: - # Ignore client disconnects. - pass - except AssertionError: - body_file.close() - raise - else: - # Allow views to handle cancellation. - task.cancel() + response = None + try: try: - await task - except asyncio.CancelledError: - # Task re-raised the CancelledError as expected. + async with asyncio.TaskGroup() as tg: + tg.create_task(self.listen_for_disconnect(receive)) + response = await self.run_get_response(request) + await self.send_response(response, send) + raise RequestProcessed + except* (RequestProcessed, RequestAborted): pass + except BaseExceptionGroup as exception_group: + if len(exception_group.exceptions) == 1: + raise exception_group.exceptions[0] + raise - try: - response = tasks[1].result() - except asyncio.CancelledError: - await signals.request_finished.asend(sender=self.__class__) - else: - await sync_to_async(response.close)() - - body_file.close() + if response is None: + await signals.request_finished.asend(sender=self.__class__) + else: + await sync_to_async(response.close)() async def listen_for_disconnect(self, receive): """Listen for disconnect from the client."""