Repository: falconry/falcon

Issue:
`api.__call__()` will call `process_resource()` even if routing fails
 Run into a strange bug that a middleware was failing, and traced it back to this unusual anomaly which we found in comments on `api.py`:

```
                                resource: Resource object to which the request was
                                routed. May be None if no route was found for
                                the request.
```

It's not documented in the middleware documentation that this is the behavior, but it also seems odd since every `process_resource()` would then have to start with `if resource is not None`. If routing fails, why not immediately raise `HTTPNotFound`?



Discussion:
Good point, but I think that this behavior could help decide how to handle the situation, for example recorded in a log which are commonly URIS Requested and not founded.
However write <code>if resource is not None</code> in each process_resource seems annoying.

It would be easy enough to skip `process_resource()` when no route is found. A sink could still be used to take action in that case, if desired. All that's needed is an `if resource is not None:` above [api.py#L181](https://github.com/falconry/falcon/blob/master/falcon/api.py#L181).

@kgriffs This seems like a sensible change and I support it. 


Resolving patch:
diff --git a/falcon/api.py b/falcon/api.py
--- a/falcon/api.py
+++ b/falcon/api.py
@@ -58,6 +58,10 @@ def process_request(self, req, resp):
                     def process_resource(self, req, resp, resource):
                         \"""Process the request and resource *after* routing.
 
+                        Note:
+                            This method is only called when the request matches
+                            a route to a resource.
+
                         Args:
                             req: Request object that will be passed to the
                                 routed responder.
@@ -178,8 +182,12 @@ def __call__(self, env, start_response):
                 # e.g. a 404.
                 responder, params, resource = self._get_responder(req)
 
-                self._call_rsrc_mw(middleware_stack, req, resp, resource,
-                                   params)
+                # NOTE(kgriffs): If the request did not match any route,
+                # a default responder is returned and the resource is
+                # None.
+                if resource is not None:
+                    self._call_rsrc_mw(middleware_stack, req, resp, resource,
+                                       params)
 
                 responder(req, resp, **params)
                 self._call_resp_mw(middleware_stack, req, resp, resource)


Test patch:
diff --git a/tests/test_middlewares.py b/tests/test_middleware.py
similarity index 97%
rename from tests/test_middlewares.py
rename to tests/test_middleware.py
--- a/tests/test_middlewares.py
+++ b/tests/test_middleware.py
@@ -90,6 +90,18 @@ def simulate_json_request(self, *args, **kwargs):
 
 class TestRequestTimeMiddleware(TestMiddleware):
 
+    def test_skip_process_resource(self):
+        global context
+        self.api = falcon.API(middleware=[RequestTimeMiddleware()])
+
+        self.api.add_route('/', MiddlewareClassResource())
+
+        self.simulate_request('/404')
+        self.assertEqual(self.srmock.status, falcon.HTTP_404)
+        self.assertIn('start_time', context)
+        self.assertNotIn('mid_time', context)
+        self.assertIn('end_time', context)
+
     def test_add_invalid_middleware(self):
         """Test than an invalid class can not be added as middleware"""
         class InvalidMiddleware():