From 418e5bb53ca1d52ee46029033b2d78a0238bc2a3 Mon Sep 17 00:00:00 2001 From: Soney Date: Sun, 2 Nov 2025 18:21:46 -0500 Subject: [PATCH 1/8] fix: raise error for responders not found for resources without suffix - Change from SuffixedMethodNotFoundError to MethodNotFoundError - Added Indicators for the resource and suffix --- falcon/routing/util.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/falcon/routing/util.py b/falcon/routing/util.py index f4c5756ae..9572a456d 100644 --- a/falcon/routing/util.py +++ b/falcon/routing/util.py @@ -25,7 +25,7 @@ from falcon._typing import MethodDict -class SuffixedMethodNotFoundError(Exception): +class MethodNotFoundError(Exception): def __init__(self, message: str) -> None: super().__init__(message) self.message = message @@ -69,11 +69,18 @@ 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 MethodNotFoundError( + f'No responders found for the specified resource: ' + f'{resource_name} and suffix: {suffix}' + ) + else: + raise MethodNotFoundError( + f'No responders found for the specified resource: {resource_name}' + ) return method_map From a7261aea798aad914350513b9879c351b6ed2ecd Mon Sep 17 00:00:00 2001 From: Soney Date: Sun, 2 Nov 2025 18:25:59 -0500 Subject: [PATCH 2/8] test: add test_custom_error_route_not_found test --- tests/test_uri_templates.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_uri_templates.py b/tests/test_uri_templates.py index 1558d67fc..4da7acfc7 100644 --- a/tests/test_uri_templates.py +++ b/tests/test_uri_templates.py @@ -14,7 +14,7 @@ import falcon from falcon import testing -from falcon.routing.util import SuffixedMethodNotFoundError +from falcon.routing.util import MethodNotFoundError _TEST_UUID = uuid.uuid4() _TEST_UUID_2 = uuid.uuid4() @@ -596,9 +596,17 @@ def test_with_and_without_trailing_slash(client, reverse): def test_custom_error_on_suffix_route_not_found(client): resource_with_suffix_routes = ResourceWithSuffixRoutes() - with pytest.raises(SuffixedMethodNotFoundError): + with pytest.raises(MethodNotFoundError): client.app.add_route( '/collections/{collection_id}/items', resource_with_suffix_routes, suffix='bad-alt', ) + + +def test_custom_error_route_not_found(client): + class EmptyResource: + pass + + with pytest.raises(MethodNotFoundError): + client.app.add_route('/empty', EmptyResource()) From 9cb2225b3d6c34f530778f3c13f8e7496e31054b Mon Sep 17 00:00:00 2001 From: Soney Date: Sun, 2 Nov 2025 18:36:25 -0500 Subject: [PATCH 3/8] test: remove redundant stonewall setup test cases - Stonewall related test cases removed - Already covered by test_custom_error_route_not_found which tests that empty resources raise MethodNotFoundError --- tests/test_http_method_routing.py | 17 ----------------- 1 file changed, 17 deletions(-) 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 ): From 39034a4d2937293f740bfef51c605c63120c415b Mon Sep 17 00:00:00 2001 From: Soney Date: Sun, 2 Nov 2025 18:53:49 -0500 Subject: [PATCH 4/8] test: fix ws Resource to include on_websocket --- tests/asgi/test_ws.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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()) From 4cc4dc7f46d95b11b259080e6b415a622538f5c4 Mon Sep 17 00:00:00 2001 From: Soney Date: Tue, 11 Nov 2025 17:03:09 -0500 Subject: [PATCH 5/8] docs(newsfragments): add rst with fixes --- docs/_newsfragments/2167.bugfix.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/_newsfragments/2167.bugfix.rst 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 From 4c34c0b248f7d8a7a93548a1a9fd4ca35aba6c9e Mon Sep 17 00:00:00 2001 From: Soney Date: Tue, 11 Nov 2025 17:04:53 -0500 Subject: [PATCH 6/8] fix: change non-suffix responders to emit a warning instead of error MethodNotFoundError change back to SuffixedMethodNotFoundError warnings.warn use emit UserWarning --- falcon/routing/util.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/falcon/routing/util.py b/falcon/routing/util.py index 9572a456d..e6c9e29e9 100644 --- a/falcon/routing/util.py +++ b/falcon/routing/util.py @@ -21,11 +21,13 @@ from falcon import constants from falcon import responders +import warnings + if TYPE_CHECKING: from falcon._typing import MethodDict -class MethodNotFoundError(Exception): +class SuffixedMethodNotFoundError(Exception): def __init__(self, message: str) -> None: super().__init__(message) self.message = message @@ -73,13 +75,13 @@ def map_http_methods(resource: object, suffix: str | None = None) -> MethodDict: if not method_map: resource_name = resource.__class__.__name__ if suffix: - raise MethodNotFoundError( + raise SuffixedMethodNotFoundError( f'No responders found for the specified resource: ' f'{resource_name} and suffix: {suffix}' ) else: - raise MethodNotFoundError( - f'No responders found for the specified resource: {resource_name}' + warnings.warn( + f'No responders (on_get, on_post, etc.) found for the specified resource: {resource_name}' ) return method_map From 58eca029c9ccccaaac061597edecd445884f2576 Mon Sep 17 00:00:00 2001 From: Soney Date: Tue, 11 Nov 2025 17:15:01 -0500 Subject: [PATCH 7/8] test: update test to verify UserWarning instead of MethodNotFoundError --- tests/test_uri_templates.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_uri_templates.py b/tests/test_uri_templates.py index 4da7acfc7..47c60504f 100644 --- a/tests/test_uri_templates.py +++ b/tests/test_uri_templates.py @@ -14,7 +14,7 @@ import falcon from falcon import testing -from falcon.routing.util import MethodNotFoundError +from falcon.routing.util import SuffixedMethodNotFoundError _TEST_UUID = uuid.uuid4() _TEST_UUID_2 = uuid.uuid4() @@ -596,7 +596,7 @@ def test_with_and_without_trailing_slash(client, reverse): def test_custom_error_on_suffix_route_not_found(client): resource_with_suffix_routes = ResourceWithSuffixRoutes() - with pytest.raises(MethodNotFoundError): + with pytest.raises(SuffixedMethodNotFoundError): client.app.add_route( '/collections/{collection_id}/items', resource_with_suffix_routes, @@ -608,5 +608,5 @@ def test_custom_error_route_not_found(client): class EmptyResource: pass - with pytest.raises(MethodNotFoundError): + with pytest.warns(UserWarning): client.app.add_route('/empty', EmptyResource()) From b92b4960a1a8733d8864d9cdeedb321b5112908f Mon Sep 17 00:00:00 2001 From: Soney Date: Tue, 11 Nov 2025 17:17:38 -0500 Subject: [PATCH 8/8] chore: run tox -e reformat --- falcon/routing/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/falcon/routing/util.py b/falcon/routing/util.py index e6c9e29e9..0703add59 100644 --- a/falcon/routing/util.py +++ b/falcon/routing/util.py @@ -17,12 +17,11 @@ from __future__ import annotations from typing import TYPE_CHECKING +import warnings from falcon import constants from falcon import responders -import warnings - if TYPE_CHECKING: from falcon._typing import MethodDict @@ -81,7 +80,8 @@ def map_http_methods(resource: object, suffix: str | None = None) -> MethodDict: ) else: warnings.warn( - f'No responders (on_get, on_post, etc.) found for the specified resource: {resource_name}' + 'No responders (on_get, on_post, etc.) ' + + f'found for the specified resource: {resource_name}' ) return method_map