Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/11857.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed multipart reading failing when encountering an empty body part -- by :user:`Dreamsorcerer`.
1 change: 1 addition & 0 deletions CHANGES/11862.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A test for websocket parser was marked to fail, which was actually failing because the parser wasn't raising an exception for a continuation frame when there was no initial frame in context.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ Marco Paolini
Marcus Stojcevich
Mariano Anaya
Mariusz Masztalerczuk
Mark Larah
Marko Kohtala
Martijn Pieters
Martin Melka
Expand Down
12 changes: 7 additions & 5 deletions aiohttp/_websocket/reader_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ def _handle_frame(
) -> None:
msg: WSMessage
if opcode in {OP_CODE_TEXT, OP_CODE_BINARY, OP_CODE_CONTINUATION}:
# Validate continuation frames before processing
if opcode == OP_CODE_CONTINUATION and self._opcode == OP_CODE_NOT_SET:
raise WebSocketError(
WSCloseCode.PROTOCOL_ERROR,
"Continuation frame for non started message",
)

# load text/binary
if not fin:
# got partial frame payload
Expand All @@ -216,11 +223,6 @@ def _handle_frame(

has_partial = bool(self._partial)
if opcode == OP_CODE_CONTINUATION:
if self._opcode == OP_CODE_NOT_SET:
raise WebSocketError(
WSCloseCode.PROTOCOL_ERROR,
"Continuation frame for non started message",
)
opcode = self._opcode
self._opcode = OP_CODE_NOT_SET
# previous frame was non finished
Expand Down
10 changes: 5 additions & 5 deletions aiohttp/multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,8 @@ async def _read_chunk_from_stream(self, size: int) -> bytes:
), "Chunk size must be greater or equal than boundary length + 2"
first_chunk = self._prev_chunk is None
if first_chunk:
self._prev_chunk = await self._content.read(size)
# We need to re-add the CRLF that got removed from headers parsing.
self._prev_chunk = b"\r\n" + await self._content.read(size)

chunk = b""
# content.read() may return less than size, so we need to loop to ensure
Expand All @@ -402,12 +403,11 @@ async def _read_chunk_from_stream(self, size: int) -> bytes:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
self._content.unread_data(window[idx:])
if size > idx:
self._prev_chunk = self._prev_chunk[:idx]
self._prev_chunk = self._prev_chunk[:idx]
chunk = window[len(self._prev_chunk) : idx]
if not chunk:
self._at_eof = True
result = self._prev_chunk
result = self._prev_chunk[2 if first_chunk else 0 :] # Strip initial CRLF
self._prev_chunk = chunk
return result

Expand Down Expand Up @@ -772,7 +772,7 @@ async def _read_headers(self) -> "CIMultiDictProxy[str]":
lines = []
while True:
chunk = await self._content.readline()
chunk = chunk.strip()
chunk = chunk.rstrip(b"\r\n")
lines.append(chunk)
if not chunk:
break
Expand Down
27 changes: 27 additions & 0 deletions tests/test_multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,33 @@ async def test_reading_skips_prelude(self) -> None:
assert first.at_eof()
assert not second.at_eof()

async def test_read_empty_body_part(self) -> None:
with Stream(b"--:\r\n\r\n--:--") as stream:
reader = aiohttp.MultipartReader(
{CONTENT_TYPE: 'multipart/related;boundary=":"'},
stream,
)
body_parts = []
async for part in reader:
assert isinstance(part, BodyPartReader)
body_parts.append(await part.read())

assert body_parts == [b""]

async def test_read_body_part_headers_only(self) -> None:
with Stream(b"--:\r\nContent-Type: text/plain\r\n\r\n--:--") as stream:
reader = aiohttp.MultipartReader(
{CONTENT_TYPE: 'multipart/related;boundary=":"'},
stream,
)
body_parts = []
async for part in reader:
assert isinstance(part, BodyPartReader)
assert "Content-Type" in part.headers
body_parts.append(await part.read())

assert body_parts == [b""]

async def test_read_form_default_encoding(self) -> None:
with Stream(
b"--:\r\n"
Expand Down
11 changes: 5 additions & 6 deletions tests/test_websocket_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,11 @@ def test_parse_frame_header_control_frame(
parser.parse_frame(struct.pack("!BB", 0b00001000, 0b00000000))


@pytest.mark.xfail()
def test_parse_frame_header_new_data_err(
out: WebSocketDataQueue, parser: PatchableWebSocketReader
) -> None:
with pytest.raises(WebSocketError):
parser.parse_frame(struct.pack("!BB", 0b000000000, 0b00000000))
def test_parse_frame_header_new_data_err(parser: PatchableWebSocketReader) -> None:
with pytest.raises(WebSocketError) as msg:
parser._feed_data(struct.pack("!BB", 0b00000000, 0b00000000))
assert msg.value.code == WSCloseCode.PROTOCOL_ERROR
assert str(msg.value) == "Continuation frame for non started message"


def test_parse_frame_header_payload_size(
Expand Down
Loading