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
- Ensure each part's header block ends with a blank line ('\r\n\r\n').
- Reduce the number of headers per part to well under 128, or pass a higher max_headers to MultipartReader if legitimately needed.
- Reject requests with abnormally large header blocks at the web server/reverse-proxy layer (client_max_body_size, large_client_header_buffers equivalents).
- 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
- Ensure each part's header block ends with a blank line (CRLF CRLF).
- Pass an explicit max_headers sized to your legitimate use case.
- Cap body/header sizes at the reverse proxy to bound abuse.
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
- unknown content transfer encoding
- boundary missed for Content-Type
- boundary %r is too long (70 chars max)
- boundary should contain ASCII only chars
- boundary value contains invalid characters
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)