aio-libs/aiohttp · error · ValueError

Invalid boundary , expected

Error message

Invalid boundary {chunk!r}, expected {self._boundary!r}

What it means

In _read_boundary, the line read between parts must equal either the boundary or the closing boundary ('--BOUNDARY--'). If it matches neither (and is not a recoverable epilogue/parent-boundary case), ValueError is raised showing the actual vs expected boundary.

Solutions

  1. Confirm the boundary token in the body exactly matches the Content-Type boundary parameter (including dashes and case).
  2. Ensure nested multipart uses distinct boundaries that do not collide.
  3. Do not let proxies rewrite multipart bodies piecemeal.
  4. Catch ValueError and abort with 400; optionally log chunk vs boundary to diagnose corruption.

Example fix

// before
Content-Type: multipart/form-data; boundary=----A
...------B

// after
Content-Type: multipart/form-data; boundary=----A
...------A
Defensive patterns

Strategy: try-catch

Validate before calling

from aiohttp.multipart import parse_mimetype

def boundary_matches_header(content_type: str, body_first_line: bytes) -> bool:
    mt = parse_mimetype(content_type)
    expected = b'--' + mt.parameters.get('boundary', '').encode()
    return body_first_line.rstrip(b'\r\n') == expected

Try / catch

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

Prevention

When it happens

Trigger: A boundary line that differs from the one declared in Content-Type; mid-stream corruption; nested multipart where the inner boundary collides with or differs from the outer; a producer that changes boundary tokens between parts.

Common situations: Mismatched boundary strings between client and server; proxies rewriting the body but not the Content-Type boundary; truncated or interleaved multipart; attacks injecting fake boundaries.

Related errors


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

Appendix: source

Thrown at aiohttp/multipart.py:883

            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
            # it should be marked as unread and handed to the parent for
            # processing
            if next_line[:2] == b"--":
                self._unread.append(next_line)
            # otherwise the request is likely missing an epilogue and both
            # lines should be passed to the parent for processing
            # (this handles the old behavior gracefully)
            else:
                self._unread.extend([next_line, epilogue])
        else:
            raise ValueError(f"Invalid boundary {chunk!r}, expected {self._boundary!r}")

    async def _read_headers(self) -> HeadersDictProxy:
        lines = []
        while True:
            chunk = await self._content.readline(max_line_length=self._max_field_size)
            chunk = chunk.rstrip(b"\r\n")
            lines.append(chunk)
            if not chunk:
                break
            if len(lines) > self._max_headers:
                raise BadHttpMessage("Too many headers received")
        parser = HeadersParser(max_field_size=self._max_field_size)
        headers, _ = parser.parse_headers(lines)
        return headers

    async def _maybe_release_last_part(self) -> None:
        """Ensures that the last read body part is read completely."""
        if self._last_part is not None:

View on GitHub (pinned to d041d4d0fd)