Skip to content

Commit

Permalink
Merge pull request #162 from lichray/default_options
Browse files Browse the repository at this point in the history
feat(api): default responder for OPTIONS method

Closes issue #68
  • Loading branch information
Kurt Griffiths committed Aug 14, 2013
2 parents e8705c0 + 54f629d commit 47ce800
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 5 deletions.
8 changes: 8 additions & 0 deletions falcon/api_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
21 changes: 20 additions & 1 deletion falcon/responders.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"""

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
Expand Down Expand Up @@ -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
26 changes: 26 additions & 0 deletions falcon/tests/test_after_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ def on_get(self, req, resp):
self.resp = resp


class SingleResource(object):

def on_options(self, req, resp):
resp.status = falcon.HTTP_501


class TestHooks(testing.TestBase):

def before(self):
Expand All @@ -87,6 +93,11 @@ 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()
Expand Down Expand Up @@ -122,3 +133,18 @@ def test_wrapped_resource(self):

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)
17 changes: 13 additions & 4 deletions falcon/tests/test_http_method_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def test_methods_not_allowed_simple(self):

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
Expand All @@ -170,13 +170,13 @@ def test_methods_not_allowed_complex(self):
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
Expand All @@ -187,10 +187,19 @@ def test_method_not_allowed_with_param(self):
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()
Expand Down

0 comments on commit 47ce800

Please sign in to comment.