Skip to content

Commit 291d969

Browse files
authored
Fix _sendfile_fallback over-reading beyond requested count (#12096)
1 parent 345ac4d commit 291d969

3 files changed

Lines changed: 33 additions & 2 deletions

File tree

CHANGES/12096.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed _sendfile_fallback over-reading beyond requested count -- by :user:`bysiber`.

aiohttp/web_fileresponse.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,11 @@ async def _sendfile_fallback(
105105
chunk_size = self._chunk_size
106106
loop = asyncio.get_event_loop()
107107
chunk = await loop.run_in_executor(
108-
None, self._seek_and_read, fobj, offset, chunk_size
108+
None, self._seek_and_read, fobj, offset, min(chunk_size, count)
109109
)
110110
while chunk:
111111
await writer.write(chunk)
112-
count = count - chunk_size
112+
count = count - len(chunk)
113113
if count <= 0:
114114
break
115115
chunk = await loop.run_in_executor(None, fobj.read, min(chunk_size, count))

tests/test_web_sendfile.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import io
23
from pathlib import Path
34
from stat import S_IFREG, S_IRUSR, S_IWUSR
45
from unittest import mock
@@ -161,3 +162,32 @@ async def test_file_response_sends_headers_immediately() -> None:
161162

162163
# Headers should be sent immediately
163164
writer.send_headers.assert_called_once()
165+
166+
167+
async def test_sendfile_fallback_respects_count_boundary() -> None:
168+
"""Regression test: _sendfile_fallback should not read beyond the requested count.
169+
170+
Previously the first chunk used the full chunk_size even when count was smaller,
171+
and the loop subtracted chunk_size instead of the actual bytes read. Both bugs
172+
could cause extra data to be sent when serving range requests.
173+
"""
174+
file_data = b"A" * 100 + b"B" * 50 # 150 bytes total
175+
fobj = io.BytesIO(file_data)
176+
177+
writer = mock.AsyncMock()
178+
written = bytearray()
179+
180+
async def capture_write(data: bytes) -> None:
181+
written.extend(data)
182+
183+
writer.write = capture_write
184+
writer.drain = mock.AsyncMock()
185+
186+
file_sender = FileResponse("dummy.bin")
187+
file_sender._chunk_size = 64 # smaller than count to test multi-chunk
188+
189+
# Request only the first 100 bytes (offset=0, count=100)
190+
await file_sender._sendfile_fallback(writer, fobj, offset=0, count=100)
191+
192+
assert bytes(written) == b"A" * 100
193+
assert len(written) == 100

0 commit comments

Comments
 (0)