diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index 60c26e58577..a053e906d49 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -584,8 +584,11 @@ def parse_headers( encoding = enc # chunking - te = headers.get(hdrs.TRANSFER_ENCODING) - if te is not None: + te_values = headers.getall(hdrs.TRANSFER_ENCODING, ()) + if te_values: + # RFC 9110 ยง5.3: Multiple header fields are equivalent to a single + # comma-separated value. Normalize before determining message framing. + te = ",".join(v.strip() for v in te_values) if self._is_chunked_te(te): chunked = True diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 8233f5d85db..e135ebbc909 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -399,6 +399,25 @@ def test_duplicate_singleton_header_accepted_in_lax_mode( assert len(messages) == 1 +async def test_response_te_split_headers_chunked( + response: HttpResponseParser, +) -> None: + text = ( + b"HTTP/1.1 200 OK\r\n" + b"Transfer-Encoding: gzip\r\n" + b"Transfer-Encoding: chunked\r\n" + b"\r\n" + b"4\r\nWiki\r\n0\r\n\r\n" + b"HTTP/1.1 204 No Content\r\n\r\n" + ) + messages, upgrade, tail = response.feed_data(text) + assert len(messages) == 2 + msg1, payload1 = messages[0] + assert msg1.chunked + assert await payload1.read() == b"Wiki" + assert messages[1][0].code == 204 + + def test_duplicate_host_header_rejected(parser: HttpRequestParser) -> None: text = ( b"GET /admin HTTP/1.1\r\n"