diff --git a/channels/exceptions.py b/channels/exceptions.py index eb299de4..b5406679 100644 --- a/channels/exceptions.py +++ b/channels/exceptions.py @@ -1,3 +1,6 @@ +from django.urls import Resolver404 + + class RequestAborted(Exception): """ Raised when the incoming request tells us it's aborted partway through @@ -63,3 +66,12 @@ class StopConsumer(Exception): """ pass + + +class RouterResolver404(Resolver404, ValueError): + """ + Raised when a router cannot resolve a path. + Backwards compatible, in future ValueError will be deprecated in favor of Resolver404 + """ + + pass diff --git a/channels/routing.py b/channels/routing.py index f48c4d33..08bc1a3f 100644 --- a/channels/routing.py +++ b/channels/routing.py @@ -5,6 +5,8 @@ from django.urls.exceptions import Resolver404 from django.urls.resolvers import RegexPattern, RoutePattern, URLResolver +from channels.exceptions import RouterResolver404 + """ All Routing instances inside this file are also valid ASGI applications - with new Channels routing, whatever you end up with as the top level object is just @@ -99,7 +101,7 @@ async def __call__(self, scope, receive, send): root_path = scope.get("root_path", "") if root_path and not path.startswith(root_path): # If root_path is present, path must start with it. - raise ValueError("No route found for path %r." % path) + raise RouterResolver404("No route found for path %r." % path) path = path[len(root_path) :] # Remove leading / to match Django's handling @@ -127,13 +129,13 @@ async def __call__(self, scope, receive, send): receive, send, ) - except Resolver404: + except RouterResolver404: pass else: if "path_remaining" in scope: - raise Resolver404("No route found for path %r." % path) + raise RouterResolver404("No route found for path %r." % path) # We are the outermost URLRouter - raise ValueError("No route found for path %r." % path) + raise RouterResolver404("No route found for path %r." % path) class ChannelNameRouter: diff --git a/tests/test_routing.py b/tests/test_routing.py index 99c76790..bef629d0 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -2,6 +2,7 @@ from django.core.exceptions import ImproperlyConfigured from django.urls import path, re_path +from channels.exceptions import RouterResolver404 from channels.routing import ChannelNameRouter, ProtocolTypeRouter, URLRouter @@ -312,3 +313,26 @@ def test_invalid_routes(): URLRouter([path("", include([]))]) assert "include() is not supported in URLRouter." in str(exc) + + +@pytest.mark.asyncio +async def test_non_existent_route_returns_not_found(): + """ + Test URLRouter route validation on non existent routes + """ + router = URLRouter([path("ws/existing", MockApplication(return_value=1))]) + + with pytest.raises(RouterResolver404): + await router( + {"type": "websocket", "path": "/", "root_path": "/"}, + None, + None, + ) + + expected_result = await router( + {"type": "websocket", "path": "/ws/existing"}, + None, + None, + ) + assert expected_result == 1 + # todo check nested routing