Repository: falconry/falcon
Pull request #1288 (2018-05-14 19:56:04)

Add a content_length property to Response Issue #1266
The new content_length property should be an adequate replacement for Response.stream_len, which could be deprecated. Using content_length would be preferable, as it more directly references the HTTP header that both stand in for. I have updated the documentation that relates to Response.stream_len to refer to content_length where I have found it.

Breaking Changes: Changes method signature from `stream_len` to `content_length`.

Closes: #1266

codecov[bot]: # [Codecov](https://codecov.io/gh/falconry/falcon/pull/1288?src=pr&el=h1) Report
> Merging [#1288](https://codecov.io/gh/falconry/falcon/pull/1288?src=pr&el=desc) into [master](https://codecov.io/gh/falconry/falcon/commit/899c31f228df78c45e2fd4da28df400558c85a48?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

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

```diff
@@          Coverage Diff           @@
##           master   #1288   +/-   ##
======================================
  Coverage     100%    100%           
======================================
  Files          39      39           
  Lines        2514    2515    +1     
  Branches      378     378           
======================================
+ Hits         2514    2515    +1
```


| [Impacted Files](https://codecov.io/gh/falconry/falcon/pull/1288?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [falcon/response.py](https://codecov.io/gh/falconry/falcon/pull/1288/diff?src=pr&el=tree#diff-ZmFsY29uL3Jlc3BvbnNlLnB5) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/falconry/falcon/pull/1288?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/1288?src=pr&el=footer). Last update [899c31f...c89a9bf](https://codecov.io/gh/falconry/falcon/pull/1288?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/user/tutorial.rst b/docs/user/tutorial.rst
index 0e40132..881223b 100644
--- a/docs/user/tutorial.rst
+++ b/docs/user/tutorial.rst
@@ -1149,7 +1149,7 @@ Go ahead and edit your ``images.py`` file to look something like this:
 
         def on_get(self, req, resp, name):
             resp.content_type = mimetypes.guess_type(name)[0]
-            resp.stream, resp.stream_len = self._image_store.open(name)
+            resp.stream, resp.content_length = self._image_store.open(name)
 
 
     class ImageStore(object):
@@ -1186,9 +1186,9 @@ Go ahead and edit your ``images.py`` file to look something like this:
 
             image_path = os.path.join(self._storage_path, name)
             stream = self._fopen(image_path, 'rb')
-            stream_len = os.path.getsize(image_path)
+            content_length = os.path.getsize(image_path)
 
-            return stream, stream_len
+            return stream, content_length
 
 As you can see, we renamed ``Resource`` to ``Collection`` and added a new ``Item``
 class to represent a single image resource. Alternatively, these two classes could
@@ -1203,9 +1203,9 @@ URI parameters in a moment.
 Inside the ``on_get()`` responder,
 we set the Content-Type header based on the filename extension, and then
 stream out the image directly from an open file handle. Note the use of
-``resp.stream_len``. Whenever using ``resp.stream`` instead of ``resp.body`` or
-``resp.data``, you typically also specify the expected length of the stream so
-that the web client knows how much data to read from the response.
+``resp.content_length``. Whenever using ``resp.stream`` instead of ``resp.body`` or
+``resp.data``, you typically also specify the expected length of the stream using the
+Content-Length header, so that the web client knows how much data to read from the response.
 
 .. note:: If you do not know the size of the stream in advance, you can work around
    that by using chunked encoding, but that's beyond the scope of this
@@ -1441,7 +1441,7 @@ as follows:
             resp.content_type = mimetypes.guess_type(name)[0]
 
             try:
-                resp.stream, resp.stream_len = self._image_store.open(name)
+                resp.stream, resp.content_length = self._image_store.open(name)
             except IOError:
                 # Normally you would also log the error.
                 raise falcon.HTTPNotFound()
diff --git a/falcon/api.py b/falcon/api.py
index bfb1a02..f738a17 100644
--- a/falcon/api.py
+++ b/falcon/api.py
@@ -277,8 +277,10 @@ class API(object):
             body = []
         else:
             body, length = self._get_body(resp, env.get('wsgi.file_wrapper'))
-            if length is not None:
+            if resp.content_length is None and length is not None:
                 resp._headers['content-length'] = str(length)
+            elif resp.content_length is not None:
+                resp._headers['content-length'] = str(resp.content_length)
 
         # NOTE(kgriffs): Based on wsgiref.validate's interpretation of
         # RFC 2616, as commented in that module's source code. The
@@ -739,12 +741,10 @@ class API(object):
                 * Otherwise, returns []
 
         """
-
         body = resp.body
         if body is not None:
             if not isinstance(body, bytes):
                 body = body.encode('utf-8')
-
             return [body], len(body)
 
         data = resp.data
@@ -769,9 +769,9 @@ class API(object):
             else:
                 iterable = stream
 
-            # NOTE(kgriffs): If resp.stream_len is None, content_length
-            # will be as well; the caller of _get_body must handle this
-            # case by not setting the Content-Length header.
+            # NOTE(pshello): resp.stream_len is deprecated in favor of
+            # resp.content_length. The caller of _get_body should give
+            # preference to resp.content_length if it has been set.
             return iterable, resp.stream_len
 
         return [], 0
diff --git a/falcon/response.py b/falcon/response.py
index ce29310..6924071 100644
--- a/falcon/response.py
+++ b/falcon/response.py
@@ -106,11 +106,8 @@ class Response(object):
             blocks as byte strings. Falcon will use *wsgi.file_wrapper*, if
             provided by the WSGI server, in order to efficiently serve
             file-like objects.
-        stream_len (int): Expected length of `stream`. If `stream` is set,
-            but `stream_len` is not, Falcon will not supply a
-            Content-Length header to the WSGI server. Consequently, the
-            server may choose to use chunked encoding or one of the
-            other strategies suggested by PEP-3333.
+
+        stream_len (int): Deprecated alias for :py:attr:`content_length`.
 
         context (dict): Dictionary to hold any data about the response which is
             specific to your app. Falcon itself will not interact with this
@@ -185,23 +182,28 @@ class Response(object):
     def __repr__(self):
         return '<%s: %s>' % (self.__class__.__name__, self.status)
 
-    def set_stream(self, stream, stream_len):
-        """Convenience method for setting both `stream` and `stream_len`.
+    def set_stream(self, stream, content_length):
+        """Convenience method for setting both `stream` and `content_length`.
 
-        Although the `stream` and `stream_len` properties may be set
-        directly, using this method ensures `stream_len` is not
+        Although the `stream` and `content_length` properties may be set
+        directly, using this method ensures `content_length` is not
         accidentally neglected when the length of the stream is known in
         advance.
 
         Note:
             If the stream length is unknown, you can set `stream`
-            directly, and ignore `stream_len`. In this case, the
+            directly, and ignore `content_length`. In this case, the
             WSGI server may choose to use chunked encoding or one
             of the other strategies suggested by PEP-3333.
+
+        Args:
+            stream: A readable file-like object.
+            content_length (int): Length of the stream, used for the
+                Content-Length header in the response.
         """
 
         self.stream = stream
-        self.stream_len = stream_len
+        self.stream_len = content_length  # NOTE(pshello): Deprecated in favor of `content_length`
 
     def set_cookie(self, name, value, expires=None, max_age=None,
                    domain=None, path=None, secure=None, http_only=True):
@@ -422,8 +424,8 @@ class Response(object):
         If the header was not previously set, nothing is done (no error is
         raised).
 
-        Note that calling this method is equivalent to setting the corresponding
-        header property (when said property is available) to ``None``. For
+        Note that calling this method is equivalent to setting the
+        corresponding header property (when said property is available) to ``None``. For
         example::
 
             resp.etag = None
@@ -652,6 +654,23 @@ class Response(object):
         """,
         uri_encode)
 
+    content_length = header_property(
+        'Content-Length',
+        """Set the Content-Length header.
+
+        Useful for responding to HEAD requests when you aren't actually
+        providing the response body.
+
+        Note:
+            In cases where the response content is a stream (readable
+            file-like object), Falcon will not supply a Content-Length header
+            to the WSGI server unless `content_length` is explicitly set.
+            Consequently, the server may choose to use chunked encoding or one of the
+            other strategies suggested by PEP-3333.
+
+        """,
+    )
+
     content_range = header_property(
         'Content-Range',
         """A tuple to use in constructing a value for the Content-Range header.


Test patch:
diff --git a/tests/test_headers.py b/tests/test_headers.py
index 3b1c16b..12d89d4 100644
--- a/tests/test_headers.py
+++ b/tests/test_headers.py
@@ -192,6 +192,17 @@ class RemoveHeaderResource(object):
         resp.downloadable_as = None
 
 
+class ContentLengthHeaderResource(object):
+
+    def __init__(self, content_length, body=None):
+        self._content_length = content_length
+        self._body = body
+
+    def on_get(self, req, resp):
+        resp.content_length = self._content_length
+        resp.body = self._body
+
+
 class TestHeaders(object):
 
     def test_content_length(self, client):
@@ -202,6 +213,19 @@ class TestHeaders(object):
         content_length = str(len(SAMPLE_BODY))
         assert result.headers['Content-Length'] == content_length
 
+    def test_declare_content_length(self, client):
+        client.app.add_route('/', ContentLengthHeaderResource(42))
+        result = client.simulate_get()
+
+        assert result.headers['Content-Length'] == '42'
+
+    def test_declared_content_length_not_overriden_by_body_length(self, client):
+        resource = ContentLengthHeaderResource(42, body='Hello World')
+        client.app.add_route('/', resource)
+        result = client.simulate_get()
+
+        assert result.headers['Content-Length'] == '42'
+
     def test_default_value(self, client):
         resource = testing.SimpleTestResource(body=SAMPLE_BODY)
         client.app.add_route('/', resource)