@@ -1564,6 +1564,79 @@ async def test_compressed_until_eof_with_pending(response: HttpResponseParser) -
15641564 assert result == original
15651565
15661566
1567+ async def test_content_length_eof_while_paused (response : HttpResponseParser ) -> None :
1568+ """EOF right after a fully received content-length body must complete it.
1569+
1570+ Regression test for #13348:
1571+ feeding the final body bytes pauses the parser for flow control before
1572+ the message can complete; a server closing the connection in that state
1573+ raised ContentLengthError despite received == expected.
1574+ """
1575+ # Must be large enough to exceed the high water mark so the parser
1576+ # pauses with the message not yet complete.
1577+ body = b"x" * (1024 * 1024 )
1578+ headers = b"HTTP/1.1 200 OK\r \n Content-Length: %d\r \n \r \n " % len (body )
1579+
1580+ msgs , upgrade , tail = response .feed_data (headers + body )
1581+ payload = msgs [0 ][- 1 ]
1582+ # The server has sent everything and closed the connection.
1583+ response .feed_eof ()
1584+
1585+ result = await payload .read ()
1586+ assert result == body
1587+ assert payload .is_eof ()
1588+ assert payload .exception () is None
1589+
1590+
1591+ async def test_compressed_content_length_eof_while_paused (
1592+ response : HttpResponseParser ,
1593+ ) -> None :
1594+ """EOF with pending decompressed data on a complete content-length body.
1595+
1596+ Like test_content_length_eof_while_paused, but the decompressor still
1597+ holds pending data at EOF, so completion is deferred until the reader
1598+ drains it.
1599+ """
1600+ # Must be large enough to exceed high water mark.
1601+ original = b"B" * 5 * 1024 * 1024
1602+ compressed = zlib .compress (original )
1603+ headers = (
1604+ b"HTTP/1.1 200 OK\r \n "
1605+ b"Content-Length: " + str (len (compressed )).encode () + b"\r \n "
1606+ b"Content-Encoding: deflate\r \n "
1607+ b"\r \n "
1608+ )
1609+
1610+ msgs , upgrade , tail = response .feed_data (headers + compressed )
1611+ payload = msgs [0 ][- 1 ]
1612+ response .feed_eof ()
1613+
1614+ # Check that .feed_eof() hasn't decompressed entire payload into memory.
1615+ assert sum (len (b ) for b in payload ._buffer ) <= (2 * 1024 * 1024 )
1616+
1617+ result = await payload .read ()
1618+ assert len (result ) == len (original )
1619+ assert result == original
1620+ assert payload .is_eof ()
1621+ assert payload .exception () is None
1622+
1623+
1624+ async def test_content_length_eof_while_paused_incomplete (
1625+ response : HttpResponseParser ,
1626+ ) -> None :
1627+ """EOF on a paused parser with a genuinely incomplete body still raises."""
1628+ body = b"x" * (1024 * 1024 )
1629+ headers = b"HTTP/1.1 200 OK\r \n Content-Length: %d\r \n \r \n " % (len (body ) + 1 )
1630+
1631+ response .feed_data (headers + body )
1632+
1633+ with pytest .raises (
1634+ http_exceptions .ContentLengthError ,
1635+ match = r"received 1048576 of 1048577 bytes" ,
1636+ ):
1637+ response .feed_eof ()
1638+
1639+
15671640async def test_compressed_until_eof_high_water (
15681641 response_cls : type [HttpResponseParser ],
15691642) -> None :
@@ -2809,6 +2882,41 @@ async def test_parse_length_payload_partial_data(
28092882 ):
28102883 p .feed_eof ()
28112884
2885+ async def test_parse_length_payload_eof_completes_after_pause (
2886+ self , protocol : BaseProtocol
2887+ ) -> None :
2888+ """feed_eof() completes a fully received length payload despite a pause.
2889+
2890+ Regression test for #13348:
2891+ The parser paused for flow control with pending decompressed data
2892+ when EOF arrived; the fully received body must complete instead of
2893+ raising ContentLengthError.
2894+ """
2895+ out = aiohttp .StreamReader (protocol , 2 ** 16 , loop = asyncio .get_running_loop ())
2896+ original = b"x" * (1024 * 1024 )
2897+ compressed = zlib .compress (original )
2898+
2899+ p = HttpPayloadParser (
2900+ out ,
2901+ length = len (compressed ),
2902+ compression = "deflate" ,
2903+ headers_parser = HeadersParser (),
2904+ )
2905+ p .pause_reading () # flow control kicked in before the final bytes
2906+ state , tail = p .feed_data (compressed )
2907+ assert state is PayloadState .PAYLOAD_HAS_PENDING_INPUT
2908+ assert not p .done
2909+
2910+ # All bytes were received, so EOF drains the pending data and
2911+ # completes the payload.
2912+ p .feed_eof ()
2913+
2914+ assert p .done
2915+ assert out .is_eof () # type: ignore[unreachable]
2916+ assert out .exception () is None
2917+ result = await out .read ()
2918+ assert result == original
2919+
28122920 async def test_parse_chunked_payload_size_error (
28132921 self , protocol : BaseProtocol
28142922 ) -> None :
0 commit comments