aio-libs/aiohttp · error · ValueError
Reading after EOF
Error message
Reading after EOF
What it means
Raised by BodyPartReader._read_chunk when the underlying content stream reports at_eof() more than twice while the boundary-search read loop tries to fill a chunk of at least _boundary_len bytes. It indicates the reader kept pulling from an exhausted stream without ever finding the boundary.
Solutions
- Check at_eof() on the BodyPartReader before calling read_chunk() again and stop iterating.
- Ensure the producer emits a proper closing boundary ('--BOUNDARY--') and that no upstream proxy truncates the body.
- Wrap the read loop in try/except ValueError and treat Reading-after-EOF as a fatal framing error, aborting the request.
- If consuming nested multipart, release each inner part fully before advancing the outer reader.
Example fix
// before
while True:
chunk = await part.read_chunk(8192)
...
// after
while not part.at_eof():
chunk = await part.read_chunk(8192)
...
if chunk == b'':
break Defensive patterns
Strategy: validation
Validate before calling
# stop iterating before pulling past EOF
while not part.at_eof():
chunk = await part.read_chunk(DEFAULT_CHUNK_SIZE)
if chunk == b'':
break
process(chunk) Type guard
async def safe_read_chunks(part) -> typing.AsyncIterator[bytes]:
while not part.at_eof():
chunk = await part.read_chunk(DEFAULT_CHUNK_SIZE)
if not chunk:
break
yield chunk Try / catch
try:
async for chunk in part:
...
except ValueError as e:
if 'EOF' in str(e):
raise web.HTTPBadRequest(text='Truncated multipart stream')
raise Prevention
- Gate read loops on at_eof() and break on empty chunks.
- Ensure producers emit a closing boundary so the stream ends cleanly.
- For nested multipart, fully release inner parts before advancing the outer reader.
When it happens
Trigger: A multipart body that ends abruptly without a closing boundary, a stream whose at_eof() flips on and off spuriously, or repeated read_chunk() calls after the part has already exhausted its content.
Common situations: Truncated uploads/downloads; a server that closes the connection mid-part; a client calling read_chunk() in a loop past EOF; nested multipart where the inner reader drains into the outer one incorrectly.
Related errors
- Could not find starting boundary
- Invalid boundary , expected
- boundary missed for Content-Type
- boundary %r is too long (70 chars max)
- boundary should contain ASCII only chars
AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11).
Data as JSON: /api/errors/05fa0b24a44f2bf7.
Report an issue: GitHub.
Appendix: source
Thrown at aiohttp/multipart.py:426
async def _read_chunk_from_stream(self, size: int) -> bytes:
# Reads content chunk of body part with unknown length.
# The Content-Length header for body part is not necessary.
assert (
size >= self._boundary_len
), "Chunk size must be greater or equal than boundary length + 2"
first_chunk = self._prev_chunk is None
if first_chunk:
# We need to re-add the CRLF that got removed from headers parsing.
self._prev_chunk = b"\r\n" + await self._content.read(size)
chunk = b""
# content.read() may return less than size, so we need to loop to ensure
# we have enough data to detect the boundary.
while len(chunk) < self._boundary_len:
chunk += await self._content.read(size)
self._content_eof += int(self._content.at_eof())
if self._content_eof > 2:
raise ValueError("Reading after EOF")
if self._content_eof:
break
if len(chunk) > size:
self._content.unread_data(chunk[size:])
chunk = chunk[:size]
assert self._prev_chunk is not None
window = self._prev_chunk + chunk
sub = b"\r\n" + self._boundary
if first_chunk:
idx = window.find(sub)
else:
idx = window.find(sub, max(0, len(self._prev_chunk) - len(sub)))
if idx >= 0:
# pushing boundary back to content
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
self._content.unread_data(window[idx:])View on GitHub (pinned to d041d4d0fd)