diff options
author | th3nn3ss <chuksmcdennis@yahoo.com> | 2022-12-21 14:25:24 -0500 |
---|---|---|
committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2023-04-03 14:01:48 +0200 |
commit | 1d1ddffc27cd55c011298cd09bfa4de3fa73cf7a (patch) | |
tree | c77c385a04bb16b84bab217258356cc480a85abc /tests/asgi | |
parent | 4e4eda6d6c8a5867dafd2ba9167ad8c064bb644a (diff) | |
download | django-1d1ddffc27cd55c011298cd09bfa4de3fa73cf7a.tar.gz |
Fixed #33738 -- Allowed handling ASGI http.disconnect in long-lived requests.
Diffstat (limited to 'tests/asgi')
-rw-r--r-- | tests/asgi/tests.py | 84 | ||||
-rw-r--r-- | tests/asgi/urls.py | 8 |
2 files changed, 92 insertions, 0 deletions
diff --git a/tests/asgi/tests.py b/tests/asgi/tests.py index fc22a992a7..0222b5356e 100644 --- a/tests/asgi/tests.py +++ b/tests/asgi/tests.py @@ -7,8 +7,10 @@ from asgiref.testing import ApplicationCommunicator from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler from django.core.asgi import get_asgi_application +from django.core.handlers.asgi import ASGIHandler, ASGIRequest from django.core.signals import request_finished, request_started from django.db import close_old_connections +from django.http import HttpResponse from django.test import ( AsyncRequestFactory, SimpleTestCase, @@ -16,6 +18,7 @@ from django.test import ( modify_settings, override_settings, ) +from django.urls import path from django.utils.http import http_date from .urls import sync_waiter, test_filename @@ -234,6 +237,34 @@ class ASGITest(SimpleTestCase): with self.assertRaises(asyncio.TimeoutError): await communicator.receive_output() + async def test_disconnect_with_body(self): + application = get_asgi_application() + scope = self.async_request_factory._base_scope(path="/") + communicator = ApplicationCommunicator(application, scope) + await communicator.send_input({"type": "http.request", "body": b"some body"}) + await communicator.send_input({"type": "http.disconnect"}) + with self.assertRaises(asyncio.TimeoutError): + await communicator.receive_output() + + async def test_assert_in_listen_for_disconnect(self): + application = get_asgi_application() + scope = self.async_request_factory._base_scope(path="/") + communicator = ApplicationCommunicator(application, scope) + await communicator.send_input({"type": "http.request"}) + await communicator.send_input({"type": "http.not_a_real_message"}) + msg = "Invalid ASGI message after request body: http.not_a_real_message" + with self.assertRaisesMessage(AssertionError, msg): + await communicator.receive_output() + + async def test_delayed_disconnect_with_body(self): + application = get_asgi_application() + scope = self.async_request_factory._base_scope(path="/delayed_hello/") + communicator = ApplicationCommunicator(application, scope) + await communicator.send_input({"type": "http.request", "body": b"some body"}) + await communicator.send_input({"type": "http.disconnect"}) + with self.assertRaises(asyncio.TimeoutError): + await communicator.receive_output() + async def test_wrong_connection_type(self): application = get_asgi_application() scope = self.async_request_factory._base_scope(path="/", type="other") @@ -318,3 +349,56 @@ class ASGITest(SimpleTestCase): self.assertEqual(len(sync_waiter.active_threads), 2) sync_waiter.active_threads.clear() + + async def test_asyncio_cancel_error(self): + # Flag to check if the view was cancelled. + view_did_cancel = False + + # A view that will listen for the cancelled error. + async def view(request): + nonlocal view_did_cancel + try: + await asyncio.sleep(0.2) + return HttpResponse("Hello World!") + except asyncio.CancelledError: + # Set the flag. + view_did_cancel = True + raise + + # Request class to use the view. + class TestASGIRequest(ASGIRequest): + urlconf = (path("cancel/", view),) + + # Handler to use request class. + class TestASGIHandler(ASGIHandler): + request_class = TestASGIRequest + + # Request cycle should complete since no disconnect was sent. + application = TestASGIHandler() + scope = self.async_request_factory._base_scope(path="/cancel/") + 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"], 200) + response_body = await communicator.receive_output() + self.assertEqual(response_body["type"], "http.response.body") + self.assertEqual(response_body["body"], b"Hello World!") + # Give response.close() time to finish. + await communicator.wait() + self.assertIs(view_did_cancel, False) + + # Request cycle with a disconnect before the view can respond. + application = TestASGIHandler() + scope = self.async_request_factory._base_scope(path="/cancel/") + communicator = ApplicationCommunicator(application, scope) + await communicator.send_input({"type": "http.request"}) + # Let the view actually start. + await asyncio.sleep(0.1) + # Disconnect the client. + await communicator.send_input({"type": "http.disconnect"}) + # The handler should not send a response. + with self.assertRaises(asyncio.TimeoutError): + await communicator.receive_output() + await communicator.wait() + self.assertIs(view_did_cancel, True) diff --git a/tests/asgi/urls.py b/tests/asgi/urls.py index 34595c1b6c..0f74fc9b97 100644 --- a/tests/asgi/urls.py +++ b/tests/asgi/urls.py @@ -1,4 +1,5 @@ import threading +import time from django.http import FileResponse, HttpResponse from django.urls import path @@ -10,6 +11,12 @@ def hello(request): return HttpResponse("Hello %s!" % name) +def hello_with_delay(request): + name = request.GET.get("name") or "World" + time.sleep(1) + return HttpResponse(f"Hello {name}!") + + def hello_meta(request): return HttpResponse( "From %s" % request.META.get("HTTP_REFERER") or "", @@ -46,4 +53,5 @@ urlpatterns = [ path("meta/", hello_meta), path("post/", post_echo), path("wait/", sync_waiter), + path("delayed_hello/", hello_with_delay), ] |