fxfitz: LGTM; needs update.
jmvrbanac: :+1: 
diff --git a/falcon/api.py b/falcon/api.py
index 5b51de0..7957ca0 100644
--- a/falcon/api.py
+++ b/falcon/api.py
@@ -483,7 +483,18 @@ class API(object):
 
         path = req.path
         method = req.method
-        resource, method_map, params = self._router.find(path)
+
+        route = self._router.find(path)
+
+        if route is not None:
+            resource, method_map, params = route
+        else:
+            # NOTE(kgriffs): Older routers may indicate that no route
+            # was found by returning (None, None, None). Therefore, we
+            # normalize resource as the flag to indicate whether or not
+            # a route was found, for the sake of backwards-compat.
+            resource = None
+
         if resource is not None:
             try:
                 responder = method_map[method]
@@ -491,7 +502,6 @@ class API(object):
                 responder = falcon.responders.bad_request
         else:
             params = {}
-            resource = None
 
             for pattern, sink in self._sinks:
                 m = pattern.match(path)
diff --git a/falcon/routing/compiled.py b/falcon/routing/compiled.py
index 9177edb..057cf6e 100644
--- a/falcon/routing/compiled.py
+++ b/falcon/routing/compiled.py
@@ -93,7 +93,7 @@ class CompiledRouter(object):
         if node is not None:
             return node.resource, node.method_map, params
         else:
-            return None, None, None
+            return None
 
     def _compile_tree(self, nodes, indent=1, level=0, fast_return=True):
         """Generates Python code for a routing tree or subtree."""

router.find() cannot return None
Contrary to http://falcon.readthedocs.org/en/latest/api/routing.html it seems like custom routers `.find()` method cannot return None.  AFAICT, `_get_responder()` unpacks `self._router.find(path)` without checking for a None return value and thus an exception occurs.  However, returning `(None, None, None)` seems to work.

I don't know if the docstring is wrong or the method.
diff --git a/tests/test_custom_router.py b/tests/test_custom_router.py
index 8ddb824..cbe1b48 100644
--- a/tests/test_custom_router.py
+++ b/tests/test_custom_router.py
@@ -24,13 +24,31 @@ class TestCustomRouter(testing.TestBase):
             resp.body = '{"status": "ok"}'
 
         class CustomRouter(object):
-            def find(self, *args, **kwargs):
-                return resource, {'GET': resource}, {}
+            def __init__(self):
+                self.reached_backwards_compat = False
 
-        self.api = falcon.API(router=CustomRouter())
+            def find(self, uri):
+                if uri == '/test':
+                    return resource, {'GET': resource}, {}
+
+                if uri == '/404/backwards-compat':
+                    self.reached_backwards_compat = True
+                    return (None, None, None)
+
+                return None
+
+        router = CustomRouter()
+        self.api = falcon.API(router=router)
         body = self.simulate_request('/test')
         self.assertEqual(body, [b'{"status": "ok"}'])
 
+        for uri in ('/404', '/404/backwards-compat'):
+            body = self.simulate_request(uri)
+            self.assertFalse(body)
+            self.assertEqual(self.srmock.status, falcon.HTTP_404)
+
+        self.assertTrue(router.reached_backwards_compat)
+
     def test_can_pass_additional_params_to_add_route(self):
 
         check = []
diff --git a/tests/test_default_router.py b/tests/test_default_router.py
index 84af78f..dec8a8e 100644
--- a/tests/test_default_router.py
+++ b/tests/test_default_router.py
@@ -38,8 +38,8 @@ class TestRegressionCases(testing.TestBase):
         resource, method_map, params = self.router.find('/v1/messages')
         self.assertEqual(resource.resource_id, 2)
 
-        resource, method_map, params = self.router.find('/v1')
-        self.assertIs(resource, None)
+        route = self.router.find('/v1')
+        self.assertIs(route, None)
 
     def test_recipes(self):
         self.router.add_route(
@@ -53,8 +53,8 @@ class TestRegressionCases(testing.TestBase):
         resource, method_map, params = self.router.find('/recipes/baking')
         self.assertEqual(resource.resource_id, 2)
 
-        resource, method_map, params = self.router.find('/recipes/grilling')
-        self.assertIs(resource, None)
+        route = self.router.find('/recipes/grilling')
+        self.assertIs(route, None)
 
 
 @ddt.ddt
@@ -166,8 +166,8 @@ class TestComplexRouting(testing.TestBase):
         resource, method_map, params = self.router.find('/emojis/signs/42/small')
         self.assertEqual(resource.resource_id, 14.1)
 
-        resource, method_map, params = self.router.find('/emojis/signs/1/small')
-        self.assertEqual(resource, None)
+        route = self.router.find('/emojis/signs/1/small')
+        self.assertEqual(route, None)
 
     @ddt.data(
         '/teams',
@@ -176,17 +176,17 @@ class TestComplexRouting(testing.TestBase):
         '/gists/42',
     )
     def test_dead_segment(self, template):
-        resource, method_map, params = self.router.find(template)
-        self.assertIs(resource, None)
+        route = self.router.find(template)
+        self.assertIs(route, None)
 
     def test_malformed_pattern(self):
-        resource, method_map, params = self.router.find(
+        route = self.router.find(
             '/repos/racker/falcon/compare/foo')
-        self.assertIs(resource, None)
+        self.assertIs(route, None)
 
-        resource, method_map, params = self.router.find(
+        route = self.router.find(
             '/repos/racker/falcon/compare/foo/full')
-        self.assertIs(resource, None)
+        self.assertIs(route, None)
 
     def test_literal(self):
         resource, method_map, params = self.router.find('/user/memberships')
@@ -248,12 +248,12 @@ class TestComplexRouting(testing.TestBase):
         '/emojis/signs/78/undefined',
     )
     def test_not_found(self, path):
-        resource, method_map, params = self.router.find(path)
-        self.assertIs(resource, None)
+        route = self.router.find(path)
+        self.assertIs(route, None)
 
     def test_subsegment_not_found(self):
-        resource, method_map, params = self.router.find('/emojis/signs/0/x')
-        self.assertIs(resource, None)
+        route = self.router.find('/emojis/signs/0/x')
+        self.assertIs(route, None)
 
     def test_multivar(self):
         resource, method_map, params = self.router.find(

pip install -e .
parse_log_pytest
requirements.txt
nose coveralls pytest
apt-get update apt-get install -y gcc
3.7
tools/test-requires
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
coverage==6.5.0
coveralls==3.3.1
ddt==1.7.2
docopt==0.6.2
exceptiongroup==1.2.2
-e git+https://github.com/falconry/falcon.git@f44fba57ef88ec9c11223c2765d49fb3573305a0#egg=falcon
idna==3.10
importlib-metadata==6.7.0
iniconfig==2.0.0
nose==1.3.7
packaging==24.0
pluggy==1.2.0
pytest==7.4.4
python-mimeparse==1.6.0
PyYAML==6.0.1
requests==2.31.0
six==1.17.0
testtools==2.7.1
tomli==2.0.1
typing_extensions==4.7.1
urllib3==2.0.7
zipp==3.15.0

name: falcon
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - charset-normalizer==3.4.1
      - coverage==6.5.0
      - coveralls==3.3.1
      - ddt==1.7.2
      - docopt==0.6.2
      - exceptiongroup==1.2.2
      - idna==3.10
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - nose==1.3.7
      - packaging==24.0
      - pluggy==1.2.0
      - pytest==7.4.4
      - python-mimeparse==1.6.0
      - pyyaml==6.0.1
      - requests==2.31.0
      - six==1.17.0
      - testtools==2.7.1
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - zipp==3.15.0
prefix: /opt/conda/envs/falcon

tests/test_custom_router.py::TestCustomRouter::test_custom_router_find_should_be_used tests/test_default_router.py::TestRegressionCases::test_recipes tests/test_default_router.py::TestRegressionCases::test_versioned_url tests/test_default_router.py::TestComplexRouting::test_dead_segment_1__teams tests/test_default_router.py::TestComplexRouting::test_dead_segment_2__emojis_signs tests/test_default_router.py::TestComplexRouting::test_dead_segment_3__gists tests/test_default_router.py::TestComplexRouting::test_dead_segment_4__gists_42 tests/test_default_router.py::TestComplexRouting::test_literal_segment tests/test_default_router.py::TestComplexRouting::test_malformed_pattern tests/test_default_router.py::TestComplexRouting::test_not_found_01__this_does_not_exist tests/test_default_router.py::TestComplexRouting::test_not_found_02__user_bogus tests/test_default_router.py::TestComplexRouting::test_not_found_03__repos_racker_falcon_compare_johndoe_master___janedoe_dev_bogus tests/test_default_router.py::TestComplexRouting::test_not_found_04__teams tests/test_default_router.py::TestComplexRouting::test_not_found_05__teams_42_members_undefined tests/test_default_router.py::TestComplexRouting::test_not_found_06__teams_42_undefined tests/test_default_router.py::TestComplexRouting::test_not_found_07__teams_42_undefined_segments tests/test_default_router.py::TestComplexRouting::test_not_found_08__teams_default_members_undefined tests/test_default_router.py::TestComplexRouting::test_not_found_09__teams_default_members_thing_undefined tests/test_default_router.py::TestComplexRouting::test_not_found_10__teams_default_members_thing_undefined_segments tests/test_default_router.py::TestComplexRouting::test_not_found_11__teams_default_undefined tests/test_default_router.py::TestComplexRouting::test_not_found_12__teams_default_undefined_segments tests/test_default_router.py::TestComplexRouting::test_not_found_13__emojis_signs tests/test_default_router.py::TestComplexRouting::test_not_found_14__emojis_signs_0_small tests/test_default_router.py::TestComplexRouting::test_not_found_15__emojis_signs_0_undefined tests/test_default_router.py::TestComplexRouting::test_not_found_16__emojis_signs_0_undefined_segments tests/test_default_router.py::TestComplexRouting::test_not_found_17__emojis_signs_20_small tests/test_default_router.py::TestComplexRouting::test_not_found_18__emojis_signs_20_undefined tests/test_default_router.py::TestComplexRouting::test_not_found_19__emojis_signs_42_undefined tests/test_default_router.py::TestComplexRouting::test_not_found_20__emojis_signs_78_undefined tests/test_default_router.py::TestComplexRouting::test_subsegment_not_found
tests/test_custom_router.py::TestCustomRouter::test_can_pass_additional_params_to_add_route tests/test_custom_router.py::TestCustomRouter::test_custom_router_add_route_should_be_used tests/test_default_router.py::TestComplexRouting::test_collision_1__teams__collision_ tests/test_default_router.py::TestComplexRouting::test_collision_2__emojis_signs__id_too_ tests/test_default_router.py::TestComplexRouting::test_collision_3__repos__org___repo__compare__complex___vs_____complex2___collision_ tests/test_default_router.py::TestComplexRouting::test_complex_1______5_ tests/test_default_router.py::TestComplexRouting::test_complex_2____full___10_ tests/test_default_router.py::TestComplexRouting::test_complex_3____part___15_ tests/test_default_router.py::TestComplexRouting::test_complex_alt_1______16_ tests/test_default_router.py::TestComplexRouting::test_complex_alt_2____full___17_ tests/test_default_router.py::TestComplexRouting::test_dump tests/test_default_router.py::TestComplexRouting::test_literal tests/test_default_router.py::TestComplexRouting::test_literal_vs_variable_01____teams_default___19_ tests/test_default_router.py::TestComplexRouting::test_literal_vs_variable_02____teams_default_members___7_ tests/test_default_router.py::TestComplexRouting::test_literal_vs_variable_03____teams_foo___6_ tests/test_default_router.py::TestComplexRouting::test_literal_vs_variable_04____teams_foo_members___7_ tests/test_default_router.py::TestComplexRouting::test_literal_vs_variable_05____gists_first___20_ tests/test_default_router.py::TestComplexRouting::test_literal_vs_variable_06____gists_first_raw___18_ tests/test_default_router.py::TestComplexRouting::test_literal_vs_variable_07____gists_first_pdf___21_ tests/test_default_router.py::TestComplexRouting::test_literal_vs_variable_08____gists_1776_pdf___21_ tests/test_default_router.py::TestComplexRouting::test_literal_vs_variable_09____emojis_signs_78___13_ tests/test_default_router.py::TestComplexRouting::test_literal_vs_variable_10____emojis_signs_78_small___22_ tests/test_default_router.py::TestComplexRouting::test_multivar tests/test_default_router.py::TestComplexRouting::test_non_collision_1__repos__org___repo__compare__simple_vs_complex_ tests/test_default_router.py::TestComplexRouting::test_non_collision_2__repos__complex___vs___simple_ tests/test_default_router.py::TestComplexRouting::test_non_collision_3__repos__org___repo__compare__complex___vs_____complex2__full tests/test_default_router.py::TestComplexRouting::test_override tests/test_default_router.py::TestComplexRouting::test_variable
falcon/routing/compiled.py falcon/api.py
falcon/routing/compiled.py falcon/api.py