Repository: falconry/falcon

Issue:
Default OPTIONS responder does not set Content-Length to "0"
Per RFC 7231:

> A server MUST generate a Content-Length field with a value of "0" if no payload body is to be sent in the response.


Discussion:
painterjd: This looks good. 

It would be nice to see a test for this, however. 
codecov-io: ## [Current coverage][cc-pull] is **100%**
> Merging [#801][cc-pull] into [master][cc-base-branch] will not change coverage

```diff
@@             master       #801   diff @@
==========================================
  Files            29         29          
  Lines          1777       1778     +1   
  Methods           0          0          
  Messages          0          0          
  Branches        299        299          
==========================================
+ Hits           1777       1778     +1   
  Misses            0          0          
  Partials          0          0          
```

> Powered by [Codecov](https://codecov.io?src=pr). Last updated by [53b198c...1b84eeb][cc-compare]
[cc-base-branch]: https://codecov.io/gh/falconry/falcon/branch/master?src=pr
[cc-compare]: https://codecov.io/gh/falconry/falcon/compare/53b198c6d83cb04f051424e9cc33b593c52e65d7...1b84eeb8f3d8941113801958b76939d9bc726841
[cc-pull]: https://codecov.io/gh/falconry/falcon/pull/801?src=pr
yashmehrotra: @painterjd Hi, I have added the test for this. Please Review.
Thanks.

Patch:
diff --git a/falcon/responders.py b/falcon/responders.py
index b5f6186..34da807 100644
--- a/falcon/responders.py
+++ b/falcon/responders.py
@@ -58,5 +58,6 @@ def create_default_options(allowed_methods):
     def on_options(req, resp, **kwargs):
         resp.status = HTTP_204
         resp.set_header('Allow', allowed)
+        resp.set_header('Content-Length', '0')
 
     return on_options
diff --git a/falcon/response.py b/falcon/response.py
index f3e2c9b..452432d 100644
--- a/falcon/response.py
+++ b/falcon/response.py
@@ -481,7 +481,12 @@ class Response(object):
 
     content_location = header_property(
         'Content-Location',
-        'Sets the Content-Location header.',
+        """Sets the Content-Location header.
+
+        This value will be URI encoded per RFC 3986. If the value that is
+        being set is already URI encoded it should be decoded first or the
+        header should be set manually using the set_header method.
+        """,
         uri_encode)
 
     content_range = header_property(
@@ -523,7 +528,12 @@ class Response(object):
 
     location = header_property(
         'Location',
-        'Sets the Location header.',
+        """Sets the Location header.
+
+        This value will be URI encoded per RFC 3986. If the value that is
+        being set is already URI encoded it should be decoded first or the
+        header should be set manually using the set_header method.
+        """,
         uri_encode)
 
     retry_after = header_property(
diff --git a/falcon/util/misc.py b/falcon/util/misc.py
index c01c05c..5b02f05 100644
--- a/falcon/util/misc.py
+++ b/falcon/util/misc.py
@@ -19,6 +19,8 @@ import warnings
 
 import six
 
+from falcon import status_codes
+
 __all__ = (
     'deprecated',
     'http_now',
@@ -26,6 +28,7 @@ __all__ = (
     'http_date_to_dt',
     'to_query_str',
     'get_bound_method',
+    'get_http_status'
 )
 
 
@@ -210,3 +213,36 @@ def get_bound_method(obj, method_name):
             raise AttributeError(msg)
 
     return method
+
+
+def get_http_status(status_code, default_reason='Unknown'):
+    """Gets both the http status code and description from just a code
+
+    Args:
+        status_code: integer or string that can be converted to an integer
+        default_reason: default text to be appended to the status_code
+            if the lookup does not find a result
+
+    Returns:
+        str: status code e.g. "404 Not Found"
+
+    Raises:
+        ValueError: the value entered could not be converted to an integer
+
+    """
+    # sanitize inputs
+    try:
+        code = float(status_code)  # float can validate values like "401.1"
+        code = int(code)  # converting to int removes the decimal places
+        if code < 100:
+            raise ValueError
+    except ValueError:
+        raise ValueError('get_http_status failed: "%s" is not a '
+                         'valid status code', status_code)
+
+    # lookup the status code
+    try:
+        return getattr(status_codes, 'HTTP_' + str(code))
+    except AttributeError:
+        # not found
+        return str(code) + ' ' + default_reason
diff --git a/tox.ini b/tox.ini
index 13a7b9d..7538965 100644
--- a/tox.ini
+++ b/tox.ini
@@ -27,7 +27,7 @@ commands = {toxinidir}/tools/clean.sh {toxinidir}/falcon
 whitelist_externals = bash
                       mv
 commands = {toxinidir}/tools/clean.sh {toxinidir}/falcon
-           nosetests --with-coverage
+           nosetests --with-coverage []
            bash -c "if [ ! -d .coverage_data ]; then mkdir .coverage_data; fi"
            mv {toxinidir}/.coverage {toxinidir}/.coverage_data/.coverage.{envname}
 


Test patch:
diff --git a/tests/test_headers.py b/tests/test_headers.py
index 8880992..838755d 100644
--- a/tests/test_headers.py
+++ b/tests/test_headers.py
@@ -534,6 +534,12 @@ class TestHeaders(testing.TestCase):
 
         self._check_link_header(resource, expected_value)
 
+    def test_content_length_options(self):
+        result = self.simulate_options()
+
+        content_length = '0'
+        self.assertEqual(result.headers['Content-Length'], content_length)
+
     # ----------------------------------------------------------------------
     # Helpers
     # ----------------------------------------------------------------------
diff --git a/tests/test_utils.py b/tests/test_utils.py
index fef8bec..957a959 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -297,6 +297,21 @@ class TestFalconUtils(testtools.TestCase):
         self.assertEqual(uri.parse_host('falcon.example.com:42'),
                          ('falcon.example.com', 42))
 
+    def test_get_http_status(self):
+        self.assertEqual(falcon.get_http_status(404), falcon.HTTP_404)
+        self.assertEqual(falcon.get_http_status(404.3), falcon.HTTP_404)
+        self.assertEqual(falcon.get_http_status('404.3'), falcon.HTTP_404)
+        self.assertEqual(falcon.get_http_status(404.9), falcon.HTTP_404)
+        self.assertEqual(falcon.get_http_status('404'), falcon.HTTP_404)
+        self.assertEqual(falcon.get_http_status(123), '123 Unknown')
+        self.assertRaises(ValueError, falcon.get_http_status, 'not_a_number')
+        self.assertRaises(ValueError, falcon.get_http_status, 0)
+        self.assertRaises(ValueError, falcon.get_http_status, 99)
+        self.assertRaises(ValueError, falcon.get_http_status, -404.3)
+        self.assertRaises(ValueError, falcon.get_http_status, '-404')
+        self.assertRaises(ValueError, falcon.get_http_status, '-404.3')
+        self.assertEqual(falcon.get_http_status(123, 'Go Away'), '123 Go Away')
+
 
 class TestFalconTesting(testing.TestBase):
     """Catch some uncommon branches not covered elsewhere."""