Repository: falconry/falcon
Pull request #1130 (2017-10-30 16:36:19)

feat(API): Support additional HTTP verbs from RFC 3253
This PR extends the list of supported HTTP verbs to encompass oft-used extension methods e.g. REPORT.  This has no impact on existing method support or routing, simply allows falcon users to implement said additional handlers if desired.  It is worth noting that many browsers do not support these additional methods yet, but they are recognized by the IANA and supported by virtually all HTTP client libraries/toolkits. 

HTTP Method listing (IANA): https://www.iana.org/assignments/http-methods/http-methods.xhtml

RFC 3253: https://tools.ietf.org/html/rfc3253

rhemz: The failing Travis CI build looks to be unrelated to this change.
codecov[bot]: # [Codecov](https://codecov.io/gh/falconry/falcon/pull/1130?src=pr&el=h1) Report
> Merging [#1130](https://codecov.io/gh/falconry/falcon/pull/1130?src=pr&el=desc) into [master](https://codecov.io/gh/falconry/falcon/commit/d4a4818be7ea5413026755ab537d8e30a03dba54?src=pr&el=desc) will **increase** coverage by `<.01%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/falconry/falcon/pull/1130/graphs/tree.svg?src=pr&width=650&token=zOtkfNQ3h6&height=150)](https://codecov.io/gh/falconry/falcon/pull/1130?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1130      +/-   ##
==========================================
+ Coverage   99.71%   99.71%   +<.01%     
==========================================
  Files          38       38              
  Lines        2422     2424       +2     
  Branches      341      341              
==========================================
+ Hits         2415     2417       +2     
  Misses          7        7
```


| [Impacted Files](https://codecov.io/gh/falconry/falcon/pull/1130?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [falcon/hooks.py](https://codecov.io/gh/falconry/falcon/pull/1130?src=pr&el=tree#diff-ZmFsY29uL2hvb2tzLnB5) | `100% <100%> (ø)` | :arrow_up: |
| [falcon/constants.py](https://codecov.io/gh/falconry/falcon/pull/1130?src=pr&el=tree#diff-ZmFsY29uL2NvbnN0YW50cy5weQ==) | `100% <100%> (ø)` | :arrow_up: |
| [falcon/routing/util.py](https://codecov.io/gh/falconry/falcon/pull/1130?src=pr&el=tree#diff-ZmFsY29uL3JvdXRpbmcvdXRpbC5weQ==) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/falconry/falcon/pull/1130?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/falconry/falcon/pull/1130?src=pr&el=footer). Last update [d4a4818...8c9d6a0](https://codecov.io/gh/falconry/falcon/pull/1130?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/falcon/constants.py b/falcon/constants.py
index 711ae0a..ab168fc 100644
--- a/falcon/constants.py
+++ b/falcon/constants.py
@@ -1,3 +1,4 @@
+# RFC 7231, 5789 methods
 HTTP_METHODS = (
     'CONNECT',
     'DELETE',
@@ -10,6 +11,19 @@ HTTP_METHODS = (
     'TRACE',
 )
 
+# RFC 3253 methods
+WEBDAV_METHODS = (
+    'CHECKIN',
+    'CHECKOUT',
+    'REPORT',
+    'UNCHECKIN',
+    'UPDATE',
+    'VERSION-CONTROL',
+)
+
+COMBINED_METHODS = HTTP_METHODS + WEBDAV_METHODS
+
+
 # NOTE(kgriffs): According to RFC 7159, most JSON parsers assume
 # UTF-8 and so it is the recommended default charset going forward,
 # and indeed, other charsets should not be specified to ensure
diff --git a/falcon/hooks.py b/falcon/hooks.py
index 6aeac28..b0e9d34 100644
--- a/falcon/hooks.py
+++ b/falcon/hooks.py
@@ -18,7 +18,7 @@ from functools import wraps
 
 import six
 
-from falcon import HTTP_METHODS
+from falcon import COMBINED_METHODS
 from falcon.util.misc import get_argnames
 
 
@@ -51,7 +51,7 @@ def before(action):
         if isinstance(responder_or_resource, six.class_types):
             resource = responder_or_resource
 
-            for method in HTTP_METHODS:
+            for method in COMBINED_METHODS:
                 responder_name = 'on_' + method.lower()
 
                 try:
@@ -100,7 +100,7 @@ def after(action):
         if isinstance(responder_or_resource, six.class_types):
             resource = responder_or_resource
 
-            for method in HTTP_METHODS:
+            for method in COMBINED_METHODS:
                 responder_name = 'on_' + method.lower()
 
                 try:
diff --git a/falcon/routing/util.py b/falcon/routing/util.py
index 8626210..6b35b48 100644
--- a/falcon/routing/util.py
+++ b/falcon/routing/util.py
@@ -18,7 +18,7 @@ import re
 
 import six
 
-from falcon import HTTP_METHODS, responders
+from falcon import COMBINED_METHODS, responders
 
 
 # NOTE(kgriffs): Published method; take care to avoid breaking changes.
@@ -101,7 +101,7 @@ def create_http_method_map(resource):  # pragma: nocover
 
     method_map = {}
 
-    for method in HTTP_METHODS:
+    for method in COMBINED_METHODS:
         try:
             responder = getattr(resource, 'on_' + method.lower())
         except AttributeError:
@@ -123,7 +123,7 @@ def create_http_method_map(resource):  # pragma: nocover
 
     na_responder = responders.create_method_not_allowed(allowed_methods)
 
-    for method in HTTP_METHODS:
+    for method in COMBINED_METHODS:
         if method not in allowed_methods:
             method_map[method] = na_responder
 
@@ -147,7 +147,7 @@ def map_http_methods(resource):
 
     method_map = {}
 
-    for method in HTTP_METHODS:
+    for method in COMBINED_METHODS:
         try:
             responder = getattr(resource, 'on_' + method.lower())
         except AttributeError:
@@ -180,6 +180,6 @@ def set_default_responders(method_map):
 
     na_responder = responders.create_method_not_allowed(allowed_methods)
 
-    for method in HTTP_METHODS:
+    for method in COMBINED_METHODS:
         if method not in allowed_methods:
             method_map[method] = na_responder


Test patch:
diff --git a/tests/test_http_method_routing.py b/tests/test_http_method_routing.py
index d7cd136..0a852f4 100644
--- a/tests/test_http_method_routing.py
+++ b/tests/test_http_method_routing.py
@@ -18,6 +18,16 @@ HTTP_METHODS = (
 )
 
 
+WEBDAV_METHODS = (
+    'CHECKIN',
+    'CHECKOUT',
+    'REPORT',
+    'UNCHECKIN',
+    'UPDATE',
+    'VERSION-CONTROL',
+)
+
+
 @pytest.fixture
 def stonewall():
     return Stonewall()
@@ -83,6 +93,12 @@ class ThingsResource(object):
         self.req, self.resp = req, resp
         resp.status = falcon.HTTP_201
 
+    def on_report(self, req, resp, id, sid):
+        self.called = True
+
+        self.req, self.resp = req, resp
+        resp.status = falcon.HTTP_204
+
 
 class Stonewall(object):
     pass
@@ -178,6 +194,13 @@ class TestHttpMethodRouting(object):
         assert response.status == falcon.HTTP_405
         assert not resource_things.called
 
+    def test_report(self, client, resource_things):
+        client.app.add_route('/things', resource_things)
+        client.app.add_route('/things/{id}/stuff/{sid}', resource_things)
+        response = client.simulate_request(path='/things/42/stuff/1337', method='REPORT')
+        assert response.status == falcon.HTTP_204
+        assert resource_things.called
+
     def test_misc(self, client, resource_misc):
         client.app.add_route('/misc', resource_misc)
         for method in ['GET', 'HEAD', 'PUT', 'PATCH']:
@@ -195,8 +218,8 @@ class TestHttpMethodRouting(object):
     def test_methods_not_allowed_complex(self, client, resource_things):
         client.app.add_route('/things', resource_things)
         client.app.add_route('/things/{id}/stuff/{sid}', resource_things)
-        for method in HTTP_METHODS:
-            if method in ('GET', 'PUT', 'HEAD', 'OPTIONS'):
+        for method in HTTP_METHODS + WEBDAV_METHODS:
+            if method in ('GET', 'PUT', 'HEAD', 'OPTIONS', 'REPORT'):
                 continue
 
             resource_things.called = False
@@ -206,11 +229,11 @@ class TestHttpMethodRouting(object):
             assert response.status == falcon.HTTP_405
 
             headers = response.headers
-            assert headers['allow'] == 'GET, HEAD, PUT, OPTIONS'
+            assert headers['allow'] == 'GET, HEAD, PUT, REPORT, OPTIONS'
 
     def test_method_not_allowed_with_param(self, client, resource_get_with_faulty_put):
         client.app.add_route('/get_with_param/{param}', resource_get_with_faulty_put)
-        for method in HTTP_METHODS:
+        for method in HTTP_METHODS + WEBDAV_METHODS:
             if method in ('GET', 'PUT', 'OPTIONS'):
                 continue
 
@@ -233,7 +256,7 @@ class TestHttpMethodRouting(object):
         assert response.status == falcon.HTTP_200
 
         headers = response.headers
-        assert headers['allow'] == 'GET, HEAD, PUT'
+        assert headers['allow'] == 'GET, HEAD, PUT, REPORT'
 
     def test_on_options(self, client):
         response = client.simulate_request(path='/misc', method='OPTIONS')