Commit ebc8e79c authored by Michael Kelly's avatar Michael Kelly Committed by Tim Graham
Browse files

Fixed #18523 -- Added stream-like API to HttpResponse.

Added getvalue() to HttpResponse to return the content of the response,
along with a few other methods to partially match io.IOBase.

Thanks Claude Paroz for the suggestion and Nick Sanford for review.
parent f7969b09
Loading
Loading
Loading
Loading
+26 −2
Original line number Diff line number Diff line
@@ -112,6 +112,7 @@ class HttpResponseBase(six.Iterator):
        # historical behavior of request_finished.
        self._handler_class = None
        self.cookies = SimpleCookie()
        self.closed = False
        if status is not None:
            self.status_code = status
        if reason is not None:
@@ -313,16 +314,26 @@ class HttpResponseBase(six.Iterator):
                closable.close()
            except Exception:
                pass
        self.closed = True
        signals.request_finished.send(sender=self._handler_class)

    def write(self, content):
        raise Exception("This %s instance is not writable" % self.__class__.__name__)
        raise IOError("This %s instance is not writable" % self.__class__.__name__)

    def flush(self):
        pass

    def tell(self):
        raise Exception("This %s instance cannot tell its position" % self.__class__.__name__)
        raise IOError("This %s instance cannot tell its position" % self.__class__.__name__)

    # These methods partially implement a stream-like object interface.
    # See https://docs.python.org/library/io.html#io.IOBase

    def writable(self):
        return False

    def writelines(self, lines):
        raise IOError("This %s instance is not writable" % self.__class__.__name__)


class HttpResponse(HttpResponseBase):
@@ -373,6 +384,16 @@ class HttpResponse(HttpResponseBase):
    def tell(self):
        return len(self.content)

    def getvalue(self):
        return self.content

    def writable(self):
        return True

    def writelines(self, lines):
        for line in lines:
            self.write(line)


class StreamingHttpResponse(HttpResponseBase):
    """
@@ -410,6 +431,9 @@ class StreamingHttpResponse(HttpResponseBase):
    def __iter__(self):
        return self.streaming_content

    def getvalue(self):
        return b''.join(self.streaming_content)


class HttpResponseRedirectBase(HttpResponse):
    allowed_schemes = ['http', 'https', 'ftp']
+27 −0
Original line number Diff line number Diff line
@@ -651,6 +651,12 @@ Attributes
    This attribute exists so middleware can treat streaming responses
    differently from regular responses.

.. attribute:: HttpResponse.closed

    .. versionadded:: 1.8

    ``True`` if the response has been closed.

Methods
-------

@@ -769,6 +775,27 @@ Methods

    This method makes an :class:`HttpResponse` instance a file-like object.

.. method:: HttpResponse.getvalue()

    .. versionadded:: 1.8

    Returns the value of :attr:`HttpResponse.content`. This method makes
    an :class:`HttpResponse` instance a stream-like object.

.. method:: HttpResponse.writable()

    .. versionadded:: 1.8

    Always ``True``. This method makes an :class:`HttpResponse` instance a
    stream-like object.

.. method:: HttpResponse.writelines(lines)

    .. versionadded:: 1.8

    Writes a list of lines to the response. Line seperators are not added. This
    method makes an :class:`HttpResponse` instance a stream-like object.

.. _HTTP status code: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10

.. _ref-httpresponse-subclasses:
+4 −0
Original line number Diff line number Diff line
@@ -385,6 +385,10 @@ Requests and Responses
  <django.http.HttpRequest.get_full_path>` method now escapes unsafe characters
  from the path portion of a Uniform Resource Identifier (URI) properly.

* :class:`~django.http.HttpResponse` now implements a few additional methods
  like :meth:`~django.http.HttpResponse.getvalue` so that instances can be used
  as stream objects.

Tests
^^^^^

+12 −0
Original line number Diff line number Diff line
@@ -407,6 +407,15 @@ class HttpResponseTests(unittest.TestCase):
        r.write(b'def')
        self.assertEqual(r.content, b'abcdef')

    def test_stream_interface(self):
        r = HttpResponse('asdf')
        self.assertEqual(r.getvalue(), b'asdf')

        r = HttpResponse()
        self.assertEqual(r.writable(), True)
        r.writelines(['foo\n', 'bar\n', 'baz\n'])
        self.assertEqual(r.content, b'foo\nbar\nbaz\n')

    def test_unsafe_redirect(self):
        bad_urls = [
            'data:text/html,<script>window.alert("xss")</script>',
@@ -537,6 +546,9 @@ class StreamingHttpResponseTests(TestCase):
        with self.assertRaises(Exception):
            r.tell()

        r = StreamingHttpResponse(iter(['hello', 'world']))
        self.assertEqual(r.getvalue(), b'helloworld')


class FileCloseTests(TestCase):

+24 −1
Original line number Diff line number Diff line
@@ -4,14 +4,37 @@ from __future__ import unicode_literals

from django.conf import settings
from django.http import HttpResponse
from django.http.response import HttpResponseBase
from django.test import SimpleTestCase

UTF8 = 'utf-8'
ISO88591 = 'iso-8859-1'


class HttpResponseTests(SimpleTestCase):
class HttpResponseBaseTests(SimpleTestCase):
    def test_closed(self):
        r = HttpResponseBase()
        self.assertIs(r.closed, False)

        r.close()
        self.assertIs(r.closed, True)

    def test_write(self):
        r = HttpResponseBase()
        self.assertIs(r.writable(), False)

        with self.assertRaisesMessage(IOError, 'This HttpResponseBase instance is not writable'):
            r.write('asdf')
        with self.assertRaisesMessage(IOError, 'This HttpResponseBase instance is not writable'):
            r.writelines(['asdf\n', 'qwer\n'])

    def test_tell(self):
        r = HttpResponseBase()
        with self.assertRaisesMessage(IOError, 'This HttpResponseBase instance cannot tell its position'):
            r.tell()


class HttpResponseTests(SimpleTestCase):
    def test_status_code(self):
        resp = HttpResponse(status=418)
        self.assertEqual(resp.status_code, 418)