diff --git a/docs/_newsfragments/2167.bugfix.rst b/docs/_newsfragments/2167.bugfix.rst new file mode 100644 index 000000000..314ab31f2 --- /dev/null +++ b/docs/_newsfragments/2167.bugfix.rst @@ -0,0 +1,3 @@ +Resources without responder methods now emit a ``UserWarning`` for non-suffix responders. +Resources must define at least one responder method (e.g., ``on_get()``, ``on_post()``). +This will raise an error in Falcon 5.0. \ No newline at end of file diff --git a/falcon/routing/util.py b/falcon/routing/util.py index f4c5756ae..0703add59 100644 --- a/falcon/routing/util.py +++ b/falcon/routing/util.py @@ -17,6 +17,7 @@ from __future__ import annotations from typing import TYPE_CHECKING +import warnings from falcon import constants from falcon import responders @@ -69,11 +70,19 @@ def map_http_methods(resource: object, suffix: str | None = None) -> MethodDict: if callable(responder): method_map[method] = responder - # If suffix is specified and doesn't map to any methods, raise an error - if suffix and not method_map: - raise SuffixedMethodNotFoundError( - 'No responders found for the specified suffix' - ) + # If doesn't map to any methods, raise an error + if not method_map: + resource_name = resource.__class__.__name__ + if suffix: + raise SuffixedMethodNotFoundError( + f'No responders found for the specified resource: ' + f'{resource_name} and suffix: {suffix}' + ) + else: + warnings.warn( + 'No responders (on_get, on_post, etc.) ' + + f'found for the specified resource: {resource_name}' + ) return method_map diff --git a/tests/asgi/test_ws.py b/tests/asgi/test_ws.py index b68be9178..22827cab5 100644 --- a/tests/asgi/test_ws.py +++ b/tests/asgi/test_ws.py @@ -1087,7 +1087,8 @@ async def on_websocket(self, req, ws): async def test_ws_simulator_collect_edge_cases(conductor): class Resource: - pass + async def on_websocket(self, req, ws): + pass conductor.app.add_route('/', Resource()) diff --git a/tests/test_http_method_routing.py b/tests/test_http_method_routing.py index e3446bba6..057a3f8bf 100644 --- a/tests/test_http_method_routing.py +++ b/tests/test_http_method_routing.py @@ -40,11 +40,6 @@ ] -@pytest.fixture -def stonewall(): - return Stonewall() - - @pytest.fixture def resource_things(): return ThingsResource() @@ -64,8 +59,6 @@ def resource_get_with_faulty_put(): def client(asgi, util): app = util.create_app(asgi) - app.add_route('/stonewall', Stonewall()) - resource_things = ThingsResource() app.add_route('/things', resource_things) app.add_route('/things/{id}/stuff/{sid}', resource_things) @@ -115,10 +108,6 @@ def on_websocket(self, req, resp, id, sid): self.called = True -class Stonewall: - pass - - def capture(func): @wraps(func) def with_capture(*args, **kwargs): @@ -238,12 +227,6 @@ def test_misc(self, client, resource_misc, catch_wsgiref_query_warning): assert resource_misc.called assert resource_misc.req.method == method - def test_methods_not_allowed_simple(self, client, stonewall): - client.app.add_route('/stonewall', stonewall) - for method in ['GET', 'HEAD', 'PUT', 'PATCH']: - response = client.simulate_request(path='/stonewall', method=method) - assert response.status == falcon.HTTP_405 - def test_methods_not_allowed_complex( self, client, resource_things, catch_wsgiref_query_warning ): diff --git a/tests/test_uri_templates.py b/tests/test_uri_templates.py index 1558d67fc..47c60504f 100644 --- a/tests/test_uri_templates.py +++ b/tests/test_uri_templates.py @@ -602,3 +602,11 @@ def test_custom_error_on_suffix_route_not_found(client): resource_with_suffix_routes, suffix='bad-alt', ) + + +def test_custom_error_route_not_found(client): + class EmptyResource: + pass + + with pytest.warns(UserWarning): + client.app.add_route('/empty', EmptyResource())