Skip to content

Commit 37722f8

Browse files
authored
Preserve MultipartWriter parts headers on write (#3475)
* Preserve MultipartWriter parts headers on write This fixes #3035 * Mark case when payload has no headers as unreachable with FIXME This case is actually impossible because all the payload instances will have headers defined with at least `Content-Type` definition. While, it's theoretically possible to create `Payload` without headers definition and Multipart format itself allows such parts, in multipart module we already have set of assertions which wouldn't make this possible. Since `_binary_headers` is private property with unknown fate and created just to not copy-paste headers serialization logic twice and used exactly within `multipart` module, we're safe to ignore this branch. Proper fix would be refactoring the way how headers and their fragments will get handled by `Payload` instances, but this quite a work out of scope of current bugfix and will be addressed in upcoming PR.
1 parent f81b6a5 commit 37722f8

File tree

4 files changed

+35
-14
lines changed

4 files changed

+35
-14
lines changed

CHANGES/3035.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Preserve MultipartWriter parts headers on write.

aiohttp/multipart.py

+6-11
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ async def _maybe_release_last_part(self) -> None:
677677
self._last_part = None
678678

679679

680-
_Part = Tuple[Payload, 'MultiMapping[str]', str, str]
680+
_Part = Tuple[Payload, str, str]
681681

682682

683683
class MultipartWriter(Payload):
@@ -812,12 +812,7 @@ def append_payload(self, payload: Payload) -> Payload:
812812
if size is not None and not (encoding or te_encoding):
813813
payload.headers[CONTENT_LENGTH] = str(size)
814814

815-
# render headers
816-
headers = ''.join(
817-
[k + ': ' + v + '\r\n' for k, v in payload.headers.items()]
818-
).encode('utf-8') + b'\r\n'
819-
820-
self._parts.append((payload, headers, encoding, te_encoding)) # type: ignore # noqa
815+
self._parts.append((payload, encoding, te_encoding)) # type: ignore
821816
return payload
822817

823818
def append_json(
@@ -858,13 +853,13 @@ def size(self) -> Optional[int]:
858853
return 0
859854

860855
total = 0
861-
for part, headers, encoding, te_encoding in self._parts:
856+
for part, encoding, te_encoding in self._parts:
862857
if encoding or te_encoding or part.size is None:
863858
return None
864859

865860
total += int(
866861
2 + len(self._boundary) + 2 + # b'--'+self._boundary+b'\r\n'
867-
part.size + len(headers) +
862+
part.size + len(part._binary_headers) +
868863
2 # b'\r\n'
869864
)
870865

@@ -877,9 +872,9 @@ async def write(self, writer: Any,
877872
if not self._parts:
878873
return
879874

880-
for part, headers, encoding, te_encoding in self._parts:
875+
for part, encoding, te_encoding in self._parts:
881876
await writer.write(b'--' + self._boundary + b'\r\n')
882-
await writer.write(headers)
877+
await writer.write(part._binary_headers)
883878

884879
if encoding or te_encoding:
885880
w = MultipartPayloadWriter(writer)

aiohttp/payload.py

+9
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,15 @@ def headers(self) -> Optional[_CIMultiDict]:
164164
"""Custom item headers"""
165165
return self._headers
166166

167+
@property
168+
def _binary_headers(self) -> bytes:
169+
if self.headers is None:
170+
# FIXME: This case actually is unreachable.
171+
return b'' # pragma: no cover
172+
return ''.join(
173+
[k + ': ' + v + '\r\n' for k, v in self.headers.items()]
174+
).encode('utf-8') + b'\r\n'
175+
167176
@property
168177
def encoding(self) -> Optional[str]:
169178
"""Payload encoding"""

tests/test_multipart.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -1008,9 +1008,6 @@ def test_append_multipart(self, writer) -> None:
10081008
part = writer._parts[0][0]
10091009
assert part.headers[CONTENT_TYPE] == 'test/passed'
10101010

1011-
async def test_write(self, writer, stream) -> None:
1012-
await writer.write(stream)
1013-
10141011
def test_with(self) -> None:
10151012
with aiohttp.MultipartWriter(boundary=':') as writer:
10161013
writer.append('foo')
@@ -1033,6 +1030,25 @@ def test_append_none_not_allowed(self) -> None:
10331030
with aiohttp.MultipartWriter(boundary=':') as writer:
10341031
writer.append(None)
10351032

1033+
async def test_write_preserves_content_disposition(
1034+
self, buf, stream
1035+
) -> None:
1036+
with aiohttp.MultipartWriter(boundary=':') as writer:
1037+
part = writer.append(b'foo', headers={CONTENT_TYPE: 'test/passed'})
1038+
part.set_content_disposition('form-data', filename='bug')
1039+
await writer.write(stream)
1040+
1041+
headers, message = bytes(buf).split(b'\r\n\r\n', 1)
1042+
1043+
assert headers == (
1044+
b'--:\r\n'
1045+
b'Content-Type: test/passed\r\n'
1046+
b'Content-Length: 3\r\n'
1047+
b'Content-Disposition:'
1048+
b' form-data; filename="bug"; filename*=utf-8\'\'bug'
1049+
)
1050+
assert message == b'foo\r\n--:--\r\n'
1051+
10361052

10371053
async def test_async_for_reader() -> None:
10381054
data = [

0 commit comments

Comments
 (0)