aio-libs/aiohttp · error · ValueError

Could not find starting boundary

Error message

Could not find starting boundary {self._boundary!r}

What it means

While scanning for the opening boundary in _read_until_first_boundary, each readline() must eventually yield a non-empty line; an empty chunk means the stream ended before the boundary appeared. The reader then raises ValueError naming the boundary it expected.

Solutions

  1. Ensure the producer writes the opening boundary ('--BOUNDARY\r\n') as the first non-preamble line.
  2. Reject empty bodies early when a multipart Content-Type is declared.
  3. Verify the connection is not closed before the body is fully read; check Content-Length on the outer request.
  4. Catch ValueError and return 400 with a clear message about missing starting boundary.

Example fix

// before
(body is empty or has no boundary line)

// after
------WebKitFormBoundary\r\nContent-Disposition: form-data; name="f"\r\n\r\nv\r\n------WebKitFormBoundary--\r\n
Defensive patterns

Strategy: try-catch

Validate before calling

# reject obviously empty bodies before parsing
if request.can_read_body and request.content_length in (0, None):
    # likely empty multipart body; refuse early if a body was expected
    if request.headers.get(CONTENT_TYPE, '').startswith('multipart/'):
        return web.Response(status=400, text='Empty multipart body')

Try / catch

try:
    async for part in reader:
        process(part)
except ValueError as e:
    if 'starting boundary' in str(e):
        return web.Response(status=400, text='Missing multipart starting boundary')
    raise

Prevention

When it happens

Trigger: An empty multipart body, a body that contains only an epilogue, a body whose first line is the closing boundary ('--BOUNDARY--') with no opening, or a truncated stream with no boundary at all.

Common situations: Empty POST bodies, race conditions where the body is read after the connection closed, producers that forget the opening boundary, proxies that drop the preamble.

Related errors


AI-assisted analysis of aio-libs/aiohttp@d041d4d0fd (2026-08-11). Data as JSON: /api/errors/477e3c8814937c2b. Report an issue: GitHub.

Appendix: source

Thrown at aiohttp/multipart.py:854

            )

    def _get_boundary(self) -> str:
        boundary = self._mimetype.parameters["boundary"]
        if len(boundary) > 70:
            raise ValueError("boundary %r is too long (70 chars max)" % boundary)

        return boundary

    async def _readline(self) -> bytes:
        if self._unread:
            return self._unread.pop()
        return await self._content.readline()

    async def _read_until_first_boundary(self) -> None:
        while True:
            chunk = await self._readline()
            if chunk == b"":
                raise ValueError(f"Could not find starting boundary {self._boundary!r}")
            chunk = chunk.rstrip()
            if chunk == self._boundary:
                return
            elif chunk == self._boundary + b"--":
                self._at_eof = True
                return

    async def _read_boundary(self) -> None:
        chunk = (await self._readline()).rstrip()
        if chunk == self._boundary:
            pass
        elif chunk == self._boundary + b"--":
            self._at_eof = True
            epilogue = await self._readline()
            next_line = await self._readline()

            # the epilogue is expected and then either the end of input or the
            # parent multipart boundary, if the parent boundary is found then

View on GitHub (pinned to d041d4d0fd)