diff --git a/CHANGES/3104.feature b/CHANGES/3104.feature new file mode 100644 index 00000000000..3fee4ab32b2 --- /dev/null +++ b/CHANGES/3104.feature @@ -0,0 +1 @@ +Add `close_boundary` option in `MultipartWriter.write` method. Support streaming diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index f14d3660bef..ea34e13be7d 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -188,6 +188,7 @@ Thanos Lefteris Thijs Vermeir Thomas Grainger Tolga Tezel +Trinh Hoang Nhu Vadim Suharnikov Vaibhav Sagar Vamsi Krishna Avula diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index 1428d64a7ee..0e46ea45f76 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -796,7 +796,7 @@ def size(self): total += 2 + len(self._boundary) + 4 # b'--'+self._boundary+b'--\r\n' return total - async def write(self, writer): + async def write(self, writer, close_boundary=True): """Write body.""" if not self._parts: return @@ -818,7 +818,8 @@ async def write(self, writer): await writer.write(b'\r\n') - await writer.write(b'--' + self._boundary + b'--\r\n') + if close_boundary: + await writer.write(b'--' + self._boundary + b'--\r\n') class MultipartPayloadWriter: diff --git a/docs/multipart.rst b/docs/multipart.rst index db7672b8ba3..15cac793dc0 100644 --- a/docs/multipart.rst +++ b/docs/multipart.rst @@ -187,6 +187,27 @@ Please note, that on :meth:`MultipartWriter.write` all the file objects will be read until the end and there is no way to repeat a request without rewinding their pointers to the start. +Example MJPEG Streaming ``multipart/x-mixed-replace``. By default +:meth:`MultipartWriter.write` appends closing ``--boundary--`` and breaks your +content. Providing `close_boundary = False` prevents this.:: + + my_boundary = 'some-boundary' + response = web.StreamResponse( + status=200, + reason='OK', + headers={ + 'Content-Type': 'multipart/x-mixed-replace;boundary=--%s' % my_boundary + } + ) + while True: + frame = get_jpeg_frame() + with MultipartWriter('image/jpeg', boundary=my_boundary) as mpwriter: + mpwriter.append(frame, { + 'Content-Type': 'image/jpeg' + }) + await mpwriter.write(response, close_boundary=False) + await response.drain() + Hacking Multipart ----------------- diff --git a/docs/multipart_reference.rst b/docs/multipart_reference.rst index 1d43e6d4822..e44ab821ad6 100644 --- a/docs/multipart_reference.rst +++ b/docs/multipart_reference.rst @@ -157,7 +157,7 @@ Multipart reference Returns the next body part reader. -.. class:: MultipartWriter(subtype='mixed', boundary=None) +.. class:: MultipartWriter(subtype='mixed', boundary=None, close_boundary=True) Multipart body writer. @@ -191,6 +191,14 @@ Multipart reference Size of the payload. - .. comethod:: write(writer) + .. comethod:: write(writer, close_boundary=True) Write body. + + :param bool close_boundary: The (:class:`bool`) that will emit + boundary closing. You may want to disable + when streaming (``multipart/x-mixed-replace``) + + .. versionadded:: 3.4 + + Support ``close_boundary`` argument. diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 6730beca818..7985d1f1e85 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -806,6 +806,40 @@ async def test_writer_write(buf, stream, writer): b'--:--\r\n') == bytes(buf)) +async def test_writer_write_no_close_boundary(buf, stream): + writer = aiohttp.MultipartWriter(boundary=':') + writer.append('foo-bar-baz') + writer.append_json({'test': 'passed'}) + writer.append_form({'test': 'passed'}) + writer.append_form([('one', 1), ('two', 2)]) + await writer.write(stream, close_boundary=False) + + assert ( + (b'--:\r\n' + b'Content-Type: text/plain; charset=utf-8\r\n' + b'Content-Length: 11\r\n\r\n' + b'foo-bar-baz' + b'\r\n' + + b'--:\r\n' + b'Content-Type: application/json\r\n' + b'Content-Length: 18\r\n\r\n' + b'{"test": "passed"}' + b'\r\n' + + b'--:\r\n' + b'Content-Type: application/x-www-form-urlencoded\r\n' + b'Content-Length: 11\r\n\r\n' + b'test=passed' + b'\r\n' + + b'--:\r\n' + b'Content-Type: application/x-www-form-urlencoded\r\n' + b'Content-Length: 11\r\n\r\n' + b'one=1&two=2' + b'\r\n') == bytes(buf)) + + async def test_writer_serialize_with_content_encoding_gzip(buf, stream, writer): writer.append('Time to Relax!', {CONTENT_ENCODING: 'gzip'})