diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index c018dba6177..f9f99c7d09f 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -75,7 +75,7 @@ jobs: with: python-version: 3.11 - name: Cache PyPI - uses: actions/cache@v6.1.0 + uses: actions/cache@v5.0.5 with: key: pip-lint-${{ hashFiles('requirements/*.txt') }} path: ~/.cache/pip @@ -158,7 +158,7 @@ jobs: with: submodules: true - name: Cache llhttp generated files - uses: actions/cache@v6.1.0 + uses: actions/cache@v5.0.5 id: cache with: key: llhttp-${{ hashFiles('vendor/llhttp/package*.json', 'vendor/llhttp/src/**/*') }} diff --git a/CHANGES/12994.bugfix.rst b/CHANGES/12994.bugfix.rst new file mode 100644 index 00000000000..6221cf43a3a --- /dev/null +++ b/CHANGES/12994.bugfix.rst @@ -0,0 +1 @@ +Fixed :py:class:`~aiohttp.http_parser.DeflateBuffer` to treat an empty ``feed_data`` call as a diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 65c20d023e7..719f3eac01e 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -165,6 +165,7 @@ Hans Adema Harmon Y. Harry Liu Hiroshi Ogawa +Hrach Shahumyan Hrishikesh Paranjape Hu Bo Hugh Young diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index 6abbe04feae..9a8d3538e88 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -1137,7 +1137,7 @@ def feed_data(self, chunk: bytes) -> bool: # RFC1950 # bits 0..3 = CM = 0b1000 = 8 = "deflate" # bits 4..7 = CINFO = 1..7 = windows size. - if ( + if chunk and ( not self._started_decoding and self.encoding == "deflate" and chunk[0] & 0xF != 8 diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index e61af88cc16..7cd66c3e19b 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -857,15 +857,15 @@ def test_request_chunked(parser: HttpRequestParser) -> None: def test_te_header_non_ascii(parser: HttpRequestParser) -> None: - # K = Kelvin sign, not valid ascii. - text = "GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunKed\r\n\r\n" + # K = Kelvin sign, not valid ascii. + text = "GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunKed\r\n\r\n" with pytest.raises(http_exceptions.BadHttpMessage): parser.feed_data(text.encode()) def test_upgrade_header_non_ascii(parser: HttpRequestParser) -> None: - # K = Kelvin sign, not valid ascii. - text = "GET /test HTTP/1.1\r\nHost: a\r\nUpgrade: websocKet\r\n\r\n" + # K = Kelvin sign, not valid ascii. + text = "GET /test HTTP/1.1\r\nHost: a\r\nUpgrade: websocKet\r\n\r\n" messages, upgrade, tail = parser.feed_data(text.encode()) assert not upgrade @@ -2982,6 +2982,27 @@ async def test_feed_data(self, protocol: BaseProtocol) -> None: dbuf.feed_data(b"xxxx") assert [b"line"] == list(buf._buffer) + async def test_feed_data_empty(self, protocol: BaseProtocol) -> None: + """feed_data(b"") must not raise even on a fresh deflate stream. + + The chunked-transfer decoder calls feed_data(b"") to give a paused + decoder another chance to make progress. The CM-byte sniff in + feed_data would previously read chunk[0] on an empty chunk and raise + IndexError. Regression test for #12994. + """ + buf = aiohttp.StreamReader( + protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop() + ) + dbuf = DeflateBuffer(buf, "deflate") + + # Should be a no-op, returning False (no more data). + assert dbuf.feed_data(b"") is False + # No bytes pushed to the downstream stream. + assert list(buf._buffer) == [] + # Decoder was not switched to suppress_deflate_header by the empty + # chunk. + assert dbuf._started_decoding is False + async def test_feed_data_err(self, protocol: BaseProtocol) -> None: buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop()) dbuf = DeflateBuffer(buf, "deflate")