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:
@kgriffs Can you tell me how to reproduce this bug ?

For the script below, I am getting the correct `Content-Length` ie "0".

``` python
import falcon

class ThingsResource(object):
    def on_get(self, req, resp):
        resp.status = falcon.HTTP_200
        resp.body = ('Hello World\n')

app = falcon.API()
app.add_route('/things', ThingsResource)
```

Is there anything I am missing ? ( I am running the latest falcon dev version)

```
$ curl -XOPTIONS -i localhost:8000/things
HTTP/1.0 204 No Content
Date: Thu, 26 May 2016 20:32:55 GMT
Server: WSGIServer/0.1 Python/2.7.9
allow: GET
Content-Length: 0
```

Hi @yashmehrotra, thanks for taking a look! Perhaps the web server is adding that header? The framework isn't adding it per the repo below:

``` py
import falcon
from falcon import testing

class ThingsResource(object):
    def on_get(self, req, resp):
        resp.status = falcon.HTTP_200
        resp.body = ('Hello World\n')

app = falcon.API()
app.add_route('/things', ThingsResource)

env = testing.create_environ('/things', method='OPTIONS')
srmock = testing.StartResponseMock()

app(env, srmock)
print(srmock.headers)
```

@kgriffs You were right, the server I was using was automatically adding it. I have created a patch for this one at: https://github.com/falconry/falcon/pull/801 

@kgriffs per https://tools.ietf.org/html/rfc7230

>   A server MUST NOT send a Content-Length header field in any response
>    with a status code of 1xx (Informational) or 204 (No Content).

I think RFC 7231 really means: For HTTP code not include 1xx and 204, a server MUST generate a Content-Length field with a value of "0" if no payload body is to be sent in the response.

https://tools.ietf.org/html/rfc2616#section-9.2 mentioned 200 for OPTIONS, but RFC 7231 removed that wording. So we can return 204 as well as 200. But when they write the document, I think they may still think 200 is the only code it returns, thus causing this confusion.

Anyway, it determined by which document we take precedence. And I think we should take rfc7230, which makes more sense.

Please help to confirm whether my understanding is correct.

Hi @w1z2g3, that's a good insight. Logically, it would make sense to use 204 for an OPTIONS response when no payload is given, and in that case Content-Length should not be specified. Falcon is currently returning 204 as well as setting Content-Length, so we should probably either switch to 200 or remove Content-Length. Given that RFC 7231 specifically states that Content-Length should be zero, I'm inclined to say that 200 was assumed by the authors, but that's just speculation. 

This may be a question for ietf-http-wg@w3.org (http://www.rfc-editor.org/info/rfc7231). I couldn't find an existing erratum about this.


Resolving patch:
diff --git a/falcon/responders.py b/falcon/responders.py
--- a/falcon/responders.py
+++ b/falcon/responders.py
@@ -17,7 +17,7 @@
 from falcon.errors import HTTPBadRequest
 from falcon.errors import HTTPMethodNotAllowed
 from falcon.errors import HTTPNotFound
-from falcon.status_codes import HTTP_204
+from falcon.status_codes import HTTP_200
 
 
 def path_not_found(req, resp, **kwargs):
@@ -56,7 +56,7 @@ def create_default_options(allowed_methods):
     allowed = ', '.join(allowed_methods)
 
     def on_options(req, resp, **kwargs):
-        resp.status = HTTP_204
+        resp.status = HTTP_200
         resp.set_header('Allow', allowed)
         resp.set_header('Content-Length', '0')
 


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
@@ -210,7 +210,7 @@ def test_wrapped_resource(self):
 
         # Decorator should not affect the default on_options responder
         result = self.simulate_options('/wrapped')
-        self.assertEqual(result.status_code, 204)
+        self.assertEqual(result.status_code, 200)
         self.assertFalse(result.text)
 
     def test_wrapped_resource_with_hooks_aware_of_resource(self):
@@ -230,5 +230,5 @@ def test_wrapped_resource_with_hooks_aware_of_resource(self):
 
         # Decorator should not affect the default on_options responder
         result = self.simulate_options('/wrapped_aware')
-        self.assertEqual(result.status_code, 204)
+        self.assertEqual(result.status_code, 200)
         self.assertFalse(result.text)
diff --git a/tests/test_http_method_routing.py b/tests/test_http_method_routing.py
--- a/tests/test_http_method_routing.py
+++ b/tests/test_http_method_routing.py
@@ -89,6 +89,7 @@ def on_patch(self, req, resp):
         pass
 
     def on_options(self, req, resp):
+        # NOTE(kgriffs): The default responder returns 200
         resp.status = falcon.HTTP_204
 
         # NOTE(kgriffs): This is incorrect, but only return GET so
@@ -192,16 +193,16 @@ def test_method_not_allowed_with_param(self):
 
             self.assertThat(headers, Contains(allow_header))
 
-    def test_on_options(self):
+    def test_default_on_options(self):
         self.simulate_request('/things/84/stuff/65', method='OPTIONS')
-        self.assertEqual(self.srmock.status, falcon.HTTP_204)
+        self.assertEqual(self.srmock.status, falcon.HTTP_200)
 
         headers = self.srmock.headers
         allow_header = ('allow', 'GET, HEAD, PUT')
 
         self.assertThat(headers, Contains(allow_header))
 
-    def test_default_on_options(self):
+    def test_on_options(self):
         self.simulate_request('/misc', method='OPTIONS')
         self.assertEqual(self.srmock.status, falcon.HTTP_204)