diff --git a/falcon/api.py b/falcon/api.py index f96c84d..637c9ed 100644 --- a/falcon/api.py +++ b/falcon/api.py @@ -123,12 +123,8 @@ class API(object): # use_body = not helpers.should_ignore_body(resp.status, req.method) if use_body: - # get_body must be called before - # set_content_length so that all - # encodings and transformations - # on the body can be applied first. - body = helpers.get_body(resp) helpers.set_content_length(resp) + body = helpers.get_body(resp) else: # Default: return an empty body body = [] diff --git a/falcon/api_helpers.py b/falcon/api_helpers.py index fee5b31..a323196 100644 --- a/falcon/api_helpers.py +++ b/falcon/api_helpers.py @@ -19,8 +19,6 @@ limitations under the License. import re from functools import wraps -import six - from falcon import responders, HTTP_METHODS import falcon.status_codes as status @@ -77,10 +75,10 @@ def set_content_length(resp): content_length = 0 - if resp.body is not None: + if resp.body_encoded is not None: # Since body is assumed to be a byte string (str in Python 2, bytes in # Python 3), figure out the length using standard functions. - content_length = len(resp.body) + content_length = len(resp.body_encoded) elif resp.data is not None: content_length = len(resp.data) elif resp.stream is not None: @@ -99,9 +97,6 @@ def set_content_length(resp): def get_body(resp): """Converts resp content into an iterable as required by PEP 333 - Post: - If resp.body is set, it'll be encoded. - Args: resp: Instance of falcon.Response @@ -114,14 +109,10 @@ def get_body(resp): """ - body = resp.body + body = resp.body_encoded if body is not None: - if isinstance(body, six.text_type): - resp.body = body.encode('utf-8') - return [resp.body] - else: - return [body] + return [body] elif resp.data is not None: return [resp.data] diff --git a/falcon/response.py b/falcon/response.py index 450a0fe..942c611 100644 --- a/falcon/response.py +++ b/falcon/response.py @@ -16,6 +16,8 @@ limitations under the License. """ +import six + from falcon.response_helpers import header_property, format_range from falcon.util import dt_to_http, percent_escape @@ -36,7 +38,8 @@ class Response(object): """ __slots__ = ( - 'body', # Stuff + '_body', # Stuff + '_body_encoded', # Stuff 'data', '_headers', 'status', @@ -55,11 +58,41 @@ class Response(object): self.status = '200 OK' self._headers = {} - self.body = None + self._body = None + self._body_encoded = None self.data = None self.stream = None self.stream_len = None + def _get_body(self): + return self._body + + def _set_body(self, value): + self._body = value + self._body_encoded = None + + # NOTE(flaper87): Lets use a property + # for the body in case its content was + # encoded and then modified. + body = property(_get_body, _set_body) + + @property + def body_encoded(self): + # NOTE(flaper87): Notice this property + # is not thread-safe. If body is modified + # before this property returns, we might + # end up returning None. + body = self._body + if body and not self._body_encoded: + + # NOTE(flaper87): Assume it is an + # encoded str, then check and encode + # if it isn't. + self._body_encoded = body + if isinstance(body, six.text_type): + self._body_encoded = body.encode('utf-8') + return self._body_encoded + def set_header(self, name, value): """Set a header for this response to a given value. diff --git a/falcon/tests/test_after_hooks.py b/falcon/tests/test_after_hooks.py index 17d831b..11dec80 100644 --- a/falcon/tests/test_after_hooks.py +++ b/falcon/tests/test_after_hooks.py @@ -85,7 +85,7 @@ class TestHooks(testing.TestBase): self.api.add_route(self.test_route, zoo_resource) self.simulate_request(self.test_route) - self.assertEqual(b'fluffy', zoo_resource.resp.body) + self.assertEqual(b'fluffy', zoo_resource.resp.body_encoded) def test_multiple_global_hook(self): self.api = falcon.API(after=[fluffiness, cuteness]) @@ -94,17 +94,17 @@ class TestHooks(testing.TestBase): self.api.add_route(self.test_route, zoo_resource) self.simulate_request(self.test_route) - self.assertEqual(b'fluffy and cute', zoo_resource.resp.body) + self.assertEqual(b'fluffy and cute', zoo_resource.resp.body_encoded) def test_output_validator(self): self.simulate_request(self.test_route) self.assertEqual(falcon.HTTP_723, self.srmock.status) - self.assertEqual(None, self.resource.resp.body) + self.assertEqual(None, self.resource.resp.body_encoded) def test_serializer(self): self.simulate_request(self.test_route, method='PUT') - actual_body = self.resource.resp.body + actual_body = self.resource.resp.body_encoded self.assertEqual(b'{"animal": "falcon"}', actual_body) def test_wrapped_resource(self): @@ -112,7 +112,7 @@ class TestHooks(testing.TestBase): self.simulate_request('/wrapped') self.assertEqual(falcon.HTTP_200, self.srmock.status) - self.assertEqual(expected, self.wrapped_resource.resp.body) + self.assertEqual(expected, self.wrapped_resource.resp.body_encoded) self.simulate_request('/wrapped', method='HEAD') self.assertEqual(falcon.HTTP_200, self.srmock.status) diff --git a/falcon/tests/test_hello.py b/falcon/tests/test_hello.py index b1626ef..59b29a8 100644 --- a/falcon/tests/test_hello.py +++ b/falcon/tests/test_hello.py @@ -101,7 +101,7 @@ class TestHelloWorld(testing.TestBase): self.assertEquals(self.srmock.status, self.resource.sample_status) self.assertEquals(resp.status, self.resource.sample_status) - self.assertEquals(resp.body, self.resource.sample_utf8) + self.assertEquals(resp.body_encoded, self.resource.sample_utf8) self.assertEquals(body, [self.resource.sample_utf8]) def test_body_bytes(self): @@ -113,7 +113,7 @@ class TestHelloWorld(testing.TestBase): self.assertEquals(self.srmock.status, self.resource.sample_status) self.assertEquals(resp.status, self.resource.sample_status) - self.assertEquals(resp.body, self.resource.sample_utf8) + self.assertEquals(resp.body_encoded, self.resource.sample_utf8) self.assertEquals(body, [self.resource.sample_utf8]) def test_data(self):