From 54f629da5d3bdf6c78b2f596cce3da5b40bc8b10 Mon Sep 17 00:00:00 2001 From: Zhihao Yuan Date: Fri, 9 Aug 2013 12:17:12 -0400 Subject: [PATCH] feat(api): default responder for OPTIONS method --- falcon/api_helpers.py | 8 ++++++++ falcon/responders.py | 21 ++++++++++++++++++- falcon/tests/test_after_hooks.py | 26 ++++++++++++++++++++++++ falcon/tests/test_http_method_routing.py | 17 ++++++++++++---- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/falcon/api_helpers.py b/falcon/api_helpers.py index a1fea12..b9aa69d 100644 --- a/falcon/api_helpers.py +++ b/falcon/api_helpers.py @@ -203,6 +203,14 @@ def create_http_method_map(resource, uri_fields, before, after): # Attach a resource for unsupported HTTP methods allowed_methods = sorted(list(method_map.keys())) + + 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( + allowed_methods) + allowed_methods.append('OPTIONS') + na_responder = responders.create_method_not_allowed(allowed_methods) for method in HTTP_METHODS: diff --git a/falcon/responders.py b/falcon/responders.py index 475c111..61a6386 100644 --- a/falcon/responders.py +++ b/falcon/responders.py @@ -16,6 +16,7 @@ limitations under the License. """ +from falcon.status_codes import HTTP_204 from falcon.status_codes import HTTP_400 from falcon.status_codes import HTTP_404 from falcon.status_codes import HTTP_405 @@ -45,9 +46,27 @@ def create_method_not_allowed(allowed_methods): returned in the Allow header. """ + allowed = ', '.join(allowed_methods) def method_not_allowed(req, resp, **kwargs): resp.status = HTTP_405 - resp.set_header('Allow', ', '.join(allowed_methods)) + resp.set_header('Allow', allowed) return method_not_allowed + + +def create_default_options(allowed_methods): + """Creates a default responder for the OPTIONS method + + Args: + allowed_methods: A list of HTTP methods (uppercase) that should be + returned in the Allow header. + + """ + allowed = ', '.join(allowed_methods) + + def on_options(req, resp, **kwargs): + resp.status = HTTP_204 + resp.set_header('Allow', allowed) + + return on_options diff --git a/falcon/tests/test_after_hooks.py b/falcon/tests/test_after_hooks.py index 11dec80..65f8892 100644 --- a/falcon/tests/test_after_hooks.py +++ b/falcon/tests/test_after_hooks.py @@ -66,6 +66,12 @@ class ZooResource(object): self.resp = resp +class SingleResource(object): + + def on_options(self, req, resp): + resp.status = falcon.HTTP_501 + + class TestHooks(testing.TestBase): def before(self): @@ -87,6 +93,11 @@ 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() @@ -122,3 +133,18 @@ class TestHooks(testing.TestBase): self.simulate_request('/wrapped', method='PATCH') self.assertEqual(falcon.HTTP_405, self.srmock.status) + + # decorator does not affect the default on_options + body = self.simulate_request('/wrapped', method='OPTIONS') + self.assertEqual(falcon.HTTP_204, self.srmock.status) + self.assertEqual([], body) + + def test_customized_options(self): + self.api = falcon.API(after=fluffiness) + + self.api.add_route('/one', SingleResource()) + + body = self.simulate_request('/one', method='OPTIONS') + self.assertEqual(falcon.HTTP_501, self.srmock.status) + self.assertEqual([b'fluffy'], body) + self.assertNotIn('Allow', self.srmock.headers_dict) diff --git a/falcon/tests/test_http_method_routing.py b/falcon/tests/test_http_method_routing.py index cd69915..efbc6ac 100644 --- a/falcon/tests/test_http_method_routing.py +++ b/falcon/tests/test_http_method_routing.py @@ -160,7 +160,7 @@ class TestHttpMethodRouting(testing.TestBase): def test_methods_not_allowed_complex(self): for method in HTTP_METHODS: - if method in ('GET', 'POST', 'HEAD'): + if method in ('GET', 'POST', 'HEAD', 'OPTIONS'): continue self.resource_things.called = False @@ -170,13 +170,13 @@ class TestHttpMethodRouting(testing.TestBase): self.assertEquals(self.srmock.status, falcon.HTTP_405) headers = self.srmock.headers - allow_header = ('Allow', 'GET, HEAD, POST') + allow_header = ('Allow', 'GET, HEAD, POST, OPTIONS') self.assertThat(headers, Contains(allow_header)) def test_method_not_allowed_with_param(self): for method in HTTP_METHODS: - if method == 'GET' or method == 'PUT': + if method in ('GET', 'PUT', 'OPTIONS'): continue self.resource_get_with_faulty_put.called = False @@ -187,10 +187,19 @@ class TestHttpMethodRouting(testing.TestBase): self.assertEquals(self.srmock.status, falcon.HTTP_405) headers = self.srmock.headers - allow_header = ('Allow', 'GET, PUT') + allow_header = ('Allow', 'GET, PUT, OPTIONS') self.assertThat(headers, Contains(allow_header)) + def test_default_on_options(self): + self.simulate_request('/things/84/stuff/65', method='OPTIONS') + self.assertEquals(self.srmock.status, falcon.HTTP_204) + + headers = self.srmock.headers + allow_header = ('Allow', 'GET, HEAD, POST') + + self.assertThat(headers, Contains(allow_header)) + def test_unexpected_type_error(self): # Suppress logging stream = io.StringIO() if six.PY3 else io.BytesIO()