Repository: falconry/falcon
Issue: Default responders aren't wrapped with global hooks

This includes the OPTIONS responder and 405 responder (method not allowed because no responder is defined for the method, for the requested resource).


Changed files:
falcon/api_helpers.py, tests/test_after_hooks.py, tests/test_before_hooks.py

diff --git a/falcon/api_helpers.py b/falcon/api_helpers.py
index 2e5dbdc..a13545e 100644
--- a/falcon/api_helpers.py
+++ b/falcon/api_helpers.py
@@ -204,18 +204,20 @@ def create_http_method_map(resource, uri_fields, before, after):
     # Attach a resource for unsupported HTTP methods
     allowed_methods = sorted(list(method_map.keys()))
 
+    # NOTE(sebasmagri): We want the OPTIONS and 405 (Not Allowed) methods
+    # responders to be wrapped on global hooks
     if 'OPTIONS' not in method_map:
         # OPTIONS itself is intentionally excluded from the Allow header
-        # This default responder does not run the hooks
-        method_map['OPTIONS'] = responders.create_default_options(
+        responder = responders.create_default_options(
             allowed_methods)
+        method_map['OPTIONS'] = _wrap_with_hooks(before, after, responder)
         allowed_methods.append('OPTIONS')
 
     na_responder = responders.create_method_not_allowed(allowed_methods)
 
     for method in HTTP_METHODS:
         if method not in allowed_methods:
-            method_map[method] = na_responder
+            method_map[method] = _wrap_with_hooks(before, after, na_responder)
 
     return method_map
 
diff --git a/tests/test_after_hooks.py b/tests/test_after_hooks.py
index f5a4d92..43e30fc 100644
--- a/tests/test_after_hooks.py
+++ b/tests/test_after_hooks.py
@@ -25,6 +25,14 @@ def cuteness(req, resp):
         resp.body += ' and cute'
 
 
+def fluffiness_in_the_head(req, resp):
+    resp.set_header('X-Fluffiness', 'fluffy')
+
+
+def cuteness_in_the_head(req, resp):
+    resp.set_header('X-Cuteness', 'cute')
+
+
 class WrappedRespondersResource(object):
 
     @falcon.after(serialize_body)
@@ -93,11 +101,6 @@ class TestHooks(testing.TestBase):
         self.simulate_request(self.test_route)
         self.assertEqual(b'fluffy', zoo_resource.resp.body_encoded)
 
-        # hook does not affect the default on_options
-        body = self.simulate_request(self.test_route, method='OPTIONS')
-        self.assertEqual(falcon.HTTP_204, self.srmock.status)
-        self.assertEqual([], body)
-
     def test_multiple_global_hook(self):
         self.api = falcon.API(after=[fluffiness, cuteness])
         zoo_resource = ZooResource()
@@ -107,6 +110,54 @@ class TestHooks(testing.TestBase):
         self.simulate_request(self.test_route)
         self.assertEqual(b'fluffy and cute', zoo_resource.resp.body_encoded)
 
+    def test_global_hook_wrap_default_on_options(self):
+        self.api = falcon.API(after=fluffiness_in_the_head)
+        zoo_resource = ZooResource()
+
+        self.api.add_route(self.test_route, zoo_resource)
+
+        self.simulate_request(self.test_route, method='OPTIONS')
+
+        self.assertEqual(falcon.HTTP_204, self.srmock.status)
+        self.assertEqual('fluffy', self.srmock.headers_dict['X-Fluffiness'])
+
+    def test_global_hook_wrap_default_405(self):
+        self.api = falcon.API(after=fluffiness_in_the_head)
+        zoo_resource = ZooResource()
+
+        self.api.add_route(self.test_route, zoo_resource)
+
+        self.simulate_request(self.test_route, method='POST')
+
+        self.assertEqual(falcon.HTTP_405, self.srmock.status)
+        self.assertEqual('fluffy', self.srmock.headers_dict['X-Fluffiness'])
+
+    def test_multiple_global_hooks_wrap_default_on_options(self):
+        self.api = falcon.API(after=[fluffiness_in_the_head,
+                                     cuteness_in_the_head])
+        zoo_resource = ZooResource()
+
+        self.api.add_route(self.test_route, zoo_resource)
+
+        self.simulate_request(self.test_route, method='OPTIONS')
+
+        self.assertEqual(falcon.HTTP_204, self.srmock.status)
+        self.assertEqual('fluffy', self.srmock.headers_dict['X-Fluffiness'])
+        self.assertEqual('cute', self.srmock.headers_dict['X-Cuteness'])
+
+    def test_multiple_global_hooks_wrap_default_405(self):
+        self.api = falcon.API(after=[fluffiness_in_the_head,
+                                     cuteness_in_the_head])
+        zoo_resource = ZooResource()
+
+        self.api.add_route(self.test_route, zoo_resource)
+
+        self.simulate_request(self.test_route, method='POST')
+
+        self.assertEqual(falcon.HTTP_405, self.srmock.status)
+        self.assertEqual('fluffy', self.srmock.headers_dict['X-Fluffiness'])
+        self.assertEqual('cute', self.srmock.headers_dict['X-Cuteness'])
+
     def test_output_validator(self):
         self.simulate_request(self.test_route)
         self.assertEqual(falcon.HTTP_723, self.srmock.status)
diff --git a/tests/test_before_hooks.py b/tests/test_before_hooks.py
index 2e51750..bd41364 100644
--- a/tests/test_before_hooks.py
+++ b/tests/test_before_hooks.py
@@ -40,6 +40,14 @@ def frogs(req, resp, params):
     params['frogs'] = 'not fluffy'
 
 
+def bunnies_in_the_head(req, resp, params):
+    resp.set_header('X-Bunnies', 'fluffy')
+
+
+def frogs_in_the_head(req, resp, params):
+    resp.set_header('X-Frogs', 'not fluffy')
+
+
 class WrappedRespondersResource(object):
 
     @falcon.before(validate_param)
@@ -128,6 +136,48 @@ class TestHooks(testing.TestBase):
         self.assertEqual('fluffy', zoo_resource.bunnies)
         self.assertEqual('not fluffy', zoo_resource.frogs)
 
+    def test_global_hook_wrap_default_on_options(self):
+        self.api = falcon.API(before=frogs_in_the_head)
+        bunny_resource = BunnyResource()
+
+        self.api.add_route(self.test_route, bunny_resource)
+
+        self.simulate_request(self.test_route, method='OPTIONS')
+        self.assertEqual(falcon.HTTP_204, self.srmock.status)
+        self.assertEqual('not fluffy', self.srmock.headers_dict['X-Frogs'])
+
+    def test_global_hook_wrap_default_405(self):
+        self.api = falcon.API(before=[frogs_in_the_head])
+        bunny_resource = BunnyResource()
+
+        self.api.add_route(self.test_route, bunny_resource)
+
+        # on_post is not defined in ZooResource
+        self.simulate_request(self.test_route, method='POST')
+        self.assertEqual(falcon.HTTP_405, self.srmock.status)
+        self.assertEqual('not fluffy', self.srmock.headers_dict['X-Frogs'])
+
+    def test_multiple_global_hooks_wrap_default_on_options(self):
+        self.api = falcon.API(before=[frogs_in_the_head, bunnies_in_the_head])
+        bunny_resource = BunnyResource()
+
+        self.api.add_route(self.test_route, bunny_resource)
+
+        self.simulate_request(self.test_route, method='OPTIONS')
+        self.assertEqual('not fluffy', self.srmock.headers_dict['X-Frogs'])
+        self.assertEqual('fluffy', self.srmock.headers_dict['X-Bunnies'])
+
+    def test_multiple_global_hooks_wrap_default_405(self):
+        self.api = falcon.API(before=[frogs_in_the_head, bunnies_in_the_head])
+        bunny_resource = BunnyResource()
+
+        self.api.add_route(self.test_route, bunny_resource)
+
+        # on_post is not defined in ZooResource
+        self.simulate_request(self.test_route, method='POST')
+        self.assertEqual('not fluffy', self.srmock.headers_dict['X-Frogs'])
+        self.assertEqual('fluffy', self.srmock.headers_dict['X-Bunnies'])
+
     def test_input_validator(self):
         self.simulate_request(self.test_route, method='PUT')
         self.assertEqual(falcon.HTTP_400, self.srmock.status)