Skip to content

Commit 4f12a66

Browse files
Fix _sendfile_fallback over-reading beyond requested count (#12096) (#12123)
(cherry picked from commit 291d969) Co-authored-by: Kadir Can Ozden <[email protected]>
1 parent 4bcb942 commit 4f12a66

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
@@ -118,11 +118,11 @@ async def _sendfile_fallback(
118118
chunk_size = self._chunk_size
119119
loop = asyncio.get_event_loop()
120120
chunk = await loop.run_in_executor(
121-
None, self._seek_and_read, fobj, offset, chunk_size
121+
None, self._seek_and_read, fobj, offset, min(chunk_size, count)
122122
)
123123
while chunk:
124124
await writer.write(chunk)
125-
count = count - chunk_size
125+
count = count - len(chunk)
126126
if count <= 0:
127127
break
128128
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,3 +1,4 @@
1+
import io
12
from pathlib import Path
23
from stat import S_IFREG, S_IRUSR, S_IWUSR
34
from unittest import mock
@@ -155,3 +156,32 @@ async def test_file_response_sends_headers_immediately() -> None:
155156

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

0 commit comments

Comments
 (0)