Skip to content
Open
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
7 changes: 4 additions & 3 deletions h11/_readers.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ def read_eof(self) -> NoReturn:
class ChunkedReader:
def __init__(self) -> None:
self._bytes_in_chunk = 0
self._chunk_start = False
# After reading a chunk, we have to throw away the trailing \r\n.
# This tracks the bytes that we need to match and throw away.
self._bytes_to_discard = b""
Expand Down Expand Up @@ -188,13 +189,13 @@ def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
if self._bytes_in_chunk == 0:
self._reading_trailer = True
return self(buf)
chunk_start = True
else:
chunk_start = False
self._chunk_start = True
assert self._bytes_in_chunk > 0
data = buf.maybe_extract_at_most(self._bytes_in_chunk)
if data is None:
return None
chunk_start = self._chunk_start
self._chunk_start = False
self._bytes_in_chunk -= len(data)
if self._bytes_in_chunk == 0:
self._bytes_to_discard = b"\r\n"
Expand Down
36 changes: 36 additions & 0 deletions h11/tests/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,42 @@ def test_chunk_boundaries() -> None:
assert conn.next_event() == EndOfMessage()


@pytest.mark.parametrize("role", [CLIENT, SERVER])
@pytest.mark.parametrize("chunk_header", [b"5\r\n", b"5; foo=bar\r\n"])
def test_chunk_start_after_header_only(
role: Type[Sentinel], chunk_header: bytes
) -> None:
conn = Connection(role)
if role is CLIENT:
conn.send(Request(method="GET", target="/", headers=[("Host", "example.com")]))
conn.send(EndOfMessage())
conn.receive_data(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n")
assert isinstance(conn.next_event(), Response)
else:
conn.receive_data(
b"POST / HTTP/1.1\r\nHost: example.com\r\n"
b"Transfer-Encoding: chunked\r\n\r\n"
)
assert isinstance(conn.next_event(), Request)

# A complete header can arrive without any of its payload. Polling while
# waiting for that payload must not lose the chunk's start marker.
for byte in chunk_header:
conn.receive_data(bytes([byte]))
assert conn.next_event() is NEED_DATA
assert conn.next_event() is NEED_DATA

conn.receive_data(b"he")
assert conn.next_event() == Data(data=b"he", chunk_start=True, chunk_end=False)
assert conn.next_event() is NEED_DATA
conn.receive_data(b"ll")
assert conn.next_event() == Data(data=b"ll", chunk_start=False, chunk_end=False)
conn.receive_data(b"o\r\n3\r\nbye\r\n0\r\n\r\n")
assert conn.next_event() == Data(data=b"o", chunk_start=False, chunk_end=True)
assert conn.next_event() == Data(data=b"bye", chunk_start=True, chunk_end=True)
assert conn.next_event() == EndOfMessage()


def test_client_talking_to_http10_server() -> None:
c = Connection(CLIENT)
c.send(Request(method="GET", target="/", headers=[("Host", "example.com")]))
Expand Down
3 changes: 3 additions & 0 deletions newsfragments/210.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Preserve ``Data.chunk_start`` when a chunk header is received before its
payload, so the first data event for each chunk is marked correctly
regardless of how incoming bytes are split across reads.