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).



Discussion:

Resolving patch:
diff --git a/falcon/api_helpers.py b/falcon/api_helpers.py
--- 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
 


Test patch:
diff --git a/tests/test_after_hooks.py b/tests/test_after_hooks.py
--- 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 @@ def test_global_hook(self):
         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 @@ def test_multiple_global_hook(self):
         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
--- 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 @@ def test_multiple_global_hook(self):
         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)