Repository: aio-libs/aiohttp

Issue:
writing nested multipart messages does not work correctly
## Long story short

Write the following multipart message:
```python
from aiohttp.multipart import MultipartWriter
with MultipartWriter('mixed') as root:
  with MultipartWriter('mixed') as subwriter1:
    subwriter1.append('first message')
  root.append(subwriter1)

  with MultipartWriter('mixed') as subwriter2:
    subwriter2.append('second message')
  root.append(subwriter2)

print(b''.join(root.serialize()).decode('ascii'))
```

## Expected behaviour

```
--0d20b463316a462d839e93130150f5ef
Content-Type: multipart/mixed; boundary="05af9536a8374279a8bccc2ea099c82b"

--05af9536a8374279a8bccc2ea099c82b
Content-Type: text/plain; charset=utf-8
Content-Length: 13

first message
--05af9536a8374279a8bccc2ea099c82b--

--0d20b463316a462d839e93130150f5ef
Content-Type: multipart/mixed; boundary="1d4fdf1ddf8e46019da125c22c247649"

--1d4fdf1ddf8e46019da125c22c247649
Content-Type: text/plain; charset=utf-8
Content-Length: 14

second message
--1d4fdf1ddf8e46019da125c22c247649--

--0d20b463316a462d839e93130150f5ef--

```

## Actual behaviour

```
--d3b608e9626a4f3cb2329fa01ebb1fce
Content-Type: application/octet-stream

--9310b9861e56425daf7ef0773fd2ae54
Content-Type: text/plain; charset=utf-8
Content-Length: 13

first message
--9310b9861e56425daf7ef0773fd2ae54--

--d3b608e9626a4f3cb2329fa01ebb1fce
Content-Type: application/octet-stream

--6fa9c511ee2d4a36bec615b15da6f1b2
Content-Type: text/plain; charset=utf-8
Content-Length: 14

second message
--6fa9c511ee2d4a36bec615b15da6f1b2--

--d3b608e9626a4f3cb2329fa01ebb1fce--

```

## Steps to reproduce

Writer a nested multipart message as shown in the documentation.

**"Fix"**: `append(subwriter, headers=subwriter.headers)`

_I'll try to commit a patch, but just reporting the issue right now_


Discussion:

Resolving patch:
diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py
--- a/aiohttp/multipart.py
+++ b/aiohttp/multipart.py
@@ -670,7 +670,11 @@ class BodyPartWriter(object):
     """Multipart writer for single body part."""
 
     def __init__(self, obj, headers=None, *, chunk_size=8192):
-        if headers is None:
+        if isinstance(obj, MultipartWriter):
+            if headers is not None:
+                obj.headers.update(headers)
+            headers = obj.headers
+        elif headers is None:
             headers = CIMultiDict()
         elif not isinstance(headers, CIMultiDict):
             headers = CIMultiDict(headers)


Test patch:
diff --git a/tests/test_multipart.py b/tests/test_multipart.py
--- a/tests/test_multipart.py
+++ b/tests/test_multipart.py
@@ -838,6 +838,12 @@ def test_serialize_multipart(self):
         multipart = aiohttp.multipart.MultipartWriter(boundary=':')
         multipart.append('foo-bar-baz')
         multipart.append_json({'test': 'passed'})
+        multipart.append_form({'test': 'passed'})
+        multipart.append_form([('one', 1), ('two', 2)])
+        sub_multipart = aiohttp.multipart.MultipartWriter(boundary='::')
+        sub_multipart.append('nested content')
+        sub_multipart.headers['X-CUSTOM'] = 'test'
+        multipart.append(sub_multipart)
         self.assertEqual(
             [b'--:\r\n',
              b'Content-Type: text/plain; charset=utf-8\r\n'
@@ -845,11 +851,37 @@ def test_serialize_multipart(self):
              b'\r\n\r\n',
              b'foo-bar-baz',
              b'\r\n',
+
              b'--:\r\n',
              b'Content-Type: application/json',
              b'\r\n\r\n',
              b'{"test": "passed"}',
              b'\r\n',
+
+             b'--:\r\n',
+             b'Content-Type: application/x-www-form-urlencoded',
+             b'\r\n\r\n',
+             b'test=passed',
+             b'\r\n',
+
+             b'--:\r\n',
+             b'Content-Type: application/x-www-form-urlencoded',
+             b'\r\n\r\n',
+             b'one=1&two=2',
+             b'\r\n',
+
+             b'--:\r\n',
+             b'Content-Type: multipart/mixed; boundary="::"\r\nX-Custom: test',
+             b'\r\n\r\n',
+             b'--::\r\n',
+             b'Content-Type: text/plain; charset=utf-8\r\n'
+             b'Content-Length: 14',
+             b'\r\n\r\n',
+             b'nested content',
+             b'\r\n',
+             b'--::--\r\n',
+             b'',
+             b'\r\n',
              b'--:--\r\n',
              b''],
             list(self.part._serialize_multipart(multipart))
@@ -982,6 +1014,13 @@ def test_filename(self):
         self.part.set_content_disposition('related', filename='foo.html')
         self.assertEqual('foo.html', self.part.filename)
 
+    def test_wrap_multipart(self):
+        writer = aiohttp.multipart.MultipartWriter(boundary=':')
+        part = aiohttp.multipart.BodyPartWriter(writer)
+        self.assertEqual(part.headers, writer.headers)
+        part.headers['X-Custom'] = 'test'
+        self.assertEqual(part.headers, writer.headers)
+
 
 class MultipartWriterTestCase(unittest.TestCase):
 
@@ -1048,6 +1087,14 @@ def test_append_form(self):
         self.assertEqual(part.headers[CONTENT_TYPE],
                          'application/x-www-form-urlencoded')
 
+    def test_append_multipart(self):
+        subwriter = aiohttp.multipart.MultipartWriter(boundary=':')
+        subwriter.append_json({'foo': 'bar'})
+        self.writer.append(subwriter, {CONTENT_TYPE: 'test/passed'})
+        self.assertEqual(1, len(self.writer))
+        part = self.writer.parts[0]
+        self.assertEqual(part.headers[CONTENT_TYPE], 'test/passed')
+
     def test_serialize(self):
         self.assertEqual([b''], list(self.writer.serialize()))