From 796cf3d325b4a1b9d9b5361c2c8c28a5edcfe89b Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 7 Nov 2025 11:13:36 -0500 Subject: [PATCH 1/2] Refs #36315 -- Used contextlib.closing() in ASGIHandler.handle(). --- django/core/handlers/asgi.py | 108 +++++++++++++++++------------------ 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/django/core/handlers/asgi.py b/django/core/handlers/asgi.py index b4056ca042b8..dae545f3b329 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,65 @@ 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). - 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() + 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 + + async def process_request(request, send): + response = await self.run_get_response(request) try: - await task + await self.send_response(response, send) except asyncio.CancelledError: - # Task re-raised the CancelledError as expected. + # Client disconnected during send_response (ignore exception). pass - try: - response = tasks[1].result() - except asyncio.CancelledError: - await signals.request_finished.asend(sender=self.__class__) - else: - await sync_to_async(response.close)() + 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() + try: + await task + except asyncio.CancelledError: + # Task re-raised the CancelledError as expected. + pass - body_file.close() + try: + response = tasks[1].result() + except asyncio.CancelledError: + 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.""" From 2501958b5127020411df6271445ccfd0906df70e Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 7 Nov 2025 11:14:06 -0500 Subject: [PATCH 2/2] Refs #36315 -- Replaced manual task and cancellation handling with TaskGroup in ASGIHandler. --- django/core/handlers/asgi.py | 56 +++++++++++------------------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/django/core/handlers/asgi.py b/django/core/handlers/asgi.py index dae545f3b329..af8582d539e7 100644 --- a/django/core/handlers/asgi.py +++ b/django/core/handlers/asgi.py @@ -187,49 +187,25 @@ async def handle(self, scope, receive, send): await sync_to_async(error_response.close)() return - async def process_request(request, send): - response = await self.run_get_response(request) + class RequestProcessed(Exception): + pass + + response = None + try: try: - await self.send_response(response, send) - except asyncio.CancelledError: - # Client disconnected during send_response (ignore exception). + 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 - 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() - try: - await task - except asyncio.CancelledError: - # Task re-raised the CancelledError as expected. - pass - - try: - response = tasks[1].result() - except asyncio.CancelledError: + if response is None: await signals.request_finished.asend(sender=self.__class__) else: await sync_to_async(response.close)()