diff --git a/h11/_readers.py b/h11/_readers.py index 576804c..408f64e 100644 --- a/h11/_readers.py +++ b/h11/_readers.py @@ -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"" @@ -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" diff --git a/h11/tests/test_connection.py b/h11/tests/test_connection.py index 01260dc..c150b93 100644 --- a/h11/tests/test_connection.py +++ b/h11/tests/test_connection.py @@ -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")])) diff --git a/newsfragments/210.bugfix.rst b/newsfragments/210.bugfix.rst new file mode 100644 index 0000000..620f887 --- /dev/null +++ b/newsfragments/210.bugfix.rst @@ -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.