diff --git a/django/contrib/staticfiles/handlers.py b/django/contrib/staticfiles/handlers.py index da53149e788..b7d9b5c18dc 100644 --- a/django/contrib/staticfiles/handlers.py +++ b/django/contrib/staticfiles/handlers.py @@ -99,3 +99,8 @@ class ASGIStaticFilesHandler(StaticFilesHandlerMixin, ASGIHandler): return await super().__call__(scope, receive, send) # Hand off to the main app return await self.application(scope, receive, send) + + async def get_response_async(self, request): + response = await super().get_response_async(request) + response._resource_closers.append(request.close) + return response diff --git a/django/core/handlers/asgi.py b/django/core/handlers/asgi.py index cfe5101ea49..b5372a1d493 100644 --- a/django/core/handlers/asgi.py +++ b/django/core/handlers/asgi.py @@ -128,6 +128,10 @@ class ASGIRequest(HttpRequest): def COOKIES(self): return parse_cookie(self.META.get("HTTP_COOKIE", "")) + def close(self): + super().close() + self._stream.close() + class ASGIHandler(base.BaseHandler): """Handler for ASGI requests.""" @@ -164,21 +168,19 @@ class ASGIHandler(base.BaseHandler): except RequestAborted: return # Request is complete and can be served. - try: - set_script_prefix(self.get_script_prefix(scope)) - await sync_to_async(signals.request_started.send, thread_sensitive=True)( - 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: - await self.send_response(error_response, send) - return - # Get the response, using the async mode of BaseHandler. - response = await self.get_response_async(request) - response._handler_class = self.__class__ - finally: + set_script_prefix(self.get_script_prefix(scope)) + await sync_to_async(signals.request_started.send, thread_sensitive=True)( + 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) + return + # Get the response, using the async mode of BaseHandler. + response = await self.get_response_async(request) + response._handler_class = self.__class__ # Increase chunk size on file responses (ASGI servers handles low-level # chunking). if isinstance(response, FileResponse): diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 126e795fab9..1c43953717c 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -59,6 +59,9 @@ class LimitedStream: self.buffer = sio.read() return line + def close(self): + pass + class WSGIRequest(HttpRequest): def __init__(self, environ): diff --git a/django/http/request.py b/django/http/request.py index 4b160bc5f4e..ad4ec0f3311 100644 --- a/django/http/request.py +++ b/django/http/request.py @@ -340,6 +340,8 @@ class HttpRequest: self._body = self.read() except OSError as e: raise UnreadablePostError(*e.args) from e + finally: + self._stream.close() self._stream = BytesIO(self._body) return self._body diff --git a/django/test/client.py b/django/test/client.py index 60f4c37c3f1..99e831aebda 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -93,6 +93,9 @@ class FakePayload: self.__content.write(content) self.__len += len(content) + def close(self): + pass + def closing_iterator_wrapper(iterable, close): try: diff --git a/tests/asgi/tests.py b/tests/asgi/tests.py index cfee11802c1..57300d18fcf 100644 --- a/tests/asgi/tests.py +++ b/tests/asgi/tests.py @@ -165,7 +165,11 @@ class ASGITest(SimpleTestCase): async def test_post_body(self): application = get_asgi_application() - scope = self.async_request_factory._base_scope(method="POST", path="/post/") + scope = self.async_request_factory._base_scope( + method="POST", + path="/post/", + query_string="echo=1", + ) communicator = ApplicationCommunicator(application, scope) await communicator.send_input({"type": "http.request", "body": b"Echo!"}) response_start = await communicator.receive_output() @@ -175,6 +179,18 @@ class ASGITest(SimpleTestCase): self.assertEqual(response_body["type"], "http.response.body") self.assertEqual(response_body["body"], b"Echo!") + async def test_untouched_request_body_gets_closed(self): + application = get_asgi_application() + scope = self.async_request_factory._base_scope(method="POST", path="/post/") + communicator = ApplicationCommunicator(application, scope) + await communicator.send_input({"type": "http.request"}) + response_start = await communicator.receive_output() + self.assertEqual(response_start["type"], "http.response.start") + self.assertEqual(response_start["status"], 204) + response_body = await communicator.receive_output() + self.assertEqual(response_body["type"], "http.response.body") + self.assertEqual(response_body["body"], b"") + async def test_get_query_string(self): application = get_asgi_application() for query_string in (b"name=Andrew", "name=Andrew"): diff --git a/tests/asgi/urls.py b/tests/asgi/urls.py index bd286c9b2fb..34595c1b6cd 100644 --- a/tests/asgi/urls.py +++ b/tests/asgi/urls.py @@ -26,7 +26,10 @@ def sync_waiter(request): @csrf_exempt def post_echo(request): - return HttpResponse(request.body) + if request.GET.get("echo"): + return HttpResponse(request.body) + else: + return HttpResponse(status=204) sync_waiter.active_threads = set()