Repository: falconry/falcon

Issue:
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.



Discussion:
Nice catch! I think the code should be changed to reflect this, with care taken to not break existing routers that may rely on the current bahavior.


Resolving patch:
diff --git a/falcon/api.py b/falcon/api.py
--- a/falcon/api.py
+++ b/falcon/api.py
@@ -483,7 +483,18 @@ def _get_responder(self, req):
 
         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 @@ def _get_responder(self, req):
                 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
--- a/falcon/routing/compiled.py
+++ b/falcon/routing/compiled.py
@@ -93,7 +93,7 @@ def find(self, uri):
         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."""


Test patch:
diff --git a/tests/test_custom_router.py b/tests/test_custom_router.py
--- a/tests/test_custom_router.py
+++ b/tests/test_custom_router.py
@@ -24,13 +24,31 @@ def resource(req, resp, **kwargs):
             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
--- a/tests/test_default_router.py
+++ b/tests/test_default_router.py
@@ -38,8 +38,8 @@ def test_versioned_url(self):
         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 @@ def test_recipes(self):
         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 @@ def test_literal_segment(self):
         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 @@ def test_literal_segment(self):
         '/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 @@ def test_literal_vs_variable(self, path, expected_id):
         '/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(