aio-libs/aiohttp · error · BadHttpMessage

Too many headers received

Error message

Too many headers received

What it means

_read_headers reads header lines of a body part until a blank line, but caps the count at max_headers (default 128). Surpassing the cap raises BadHttpMessage 'Too many headers received' to prevent unbounded memory use (header-flood DoS).

Solutions

  1. Ensure each part's header block ends with a blank line ('\r\n\r\n').
  2. Reduce the number of headers per part to well under 128, or pass a higher max_headers to MultipartReader if legitimately needed.
  3. Reject requests with abnormally large header blocks at the web server/reverse-proxy layer (client_max_body_size, large_client_header_buffers equivalents).
  4. Catch BadHttpMessage and respond 431/400.

Example fix

// before
reader = MultipartReader(response.headers, response.content)  # default max_headers=128

// after
reader = MultipartReader(response.headers, response.content, max_headers=1024)
Defensive patterns

Strategy: validation

Validate before calling

DEFAULT_MAX_HEADERS = 128

def reader_with_caps(headers, content, *, max_headers=DEFAULT_MAX_HEADERS):
    return MultipartReader(headers, content, max_headers=max_headers,
                           max_field_size=8190)

# at the request edge, reject parts with absurdly many headers up front
if request.headers.get('Content-Length', 0, type=int) > BODY_CAP:
    return web.Response(status=413, text='Payload too large')

Try / catch

from aiohttp.http_exceptions import BadHttpMessage
try:
    reader = MultipartReader(request.headers, request.content, max_headers=1024)
    async for part in reader:
        process(part)
except BadHttpMessage as e:
    if 'Too many headers' in str(e):
        return web.Response(status=431, text='Too many headers in multipart part')
    raise

Prevention

When it happens

Trigger: A single multipart body part whose header block contains more than max_headers lines before the terminating blank line; e.g. hundreds of Content-Disposition lines or a missing blank-line terminator causing the parser to consume boundary lines as headers.

Common situations: Missing CRLF CRLF terminator after part headers (parser keeps reading); malicious header flooding; a part generated by a loop that emits a header per iteration; under-sized max_headers configured by the application.

Related errors


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

Appendix: source

Thrown at aiohttp/multipart.py:894

                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:
            if not self._last_part.at_eof():
                await self._last_part.release()
            self._unread.extend(self._last_part._unread)
            self._last_part = None


_Part = tuple[Payload, str, str]


class MultipartWriter(Payload):
    """Multipart body writer."""

View on GitHub (pinned to d041d4d0fd)