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

  1. Check at_eof() on the BodyPartReader before calling read_chunk() again and stop iterating.
  2. Ensure the producer emits a proper closing boundary ('--BOUNDARY--') and that no upstream proxy truncates the body.
  3. Wrap the read loop in try/except ValueError and treat Reading-after-EOF as a fatal framing error, aborting the request.
  4. 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

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


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)