aio-libs/aiohttp · error · ValueError

invalid Content-Length

Error message

invalid Content-Length: {length!r}

What it means

Raised by BodyPartReader.__init__ when a non-form-data body part carries a Content-Length header whose value is not pure ASCII digits. The library rejects sign prefixes, underscores, whitespace, and non-ASCII digits that Python's int() would otherwise accept, in line with RFC 9110 section 8.6. form-data parts skip this check because per RFC 7578 section 4.8 their Content-Length is ignored.

Solutions

  1. Ensure every non-form-data body part's Content-Length header is a bare run of ASCII digits with no sign, whitespace, underscores, or thousands separators.
  2. If you control the producer, omit Content-Length on form-data parts entirely (the reader ignores it for form-data).
  3. If you must parse lenient/malformed input, sanitize the header before constructing the reader, or pre-validate with str.isascii() and str.isdigit().

Example fix

// before
Content-Length: +1024

// after
Content-Length: 1024
Defensive patterns

Strategy: validation

Validate before calling

def is_valid_content_length(value: str) -> bool:
    return value is not None and value.isascii() and value.isdigit()

# before constructing the reader, validate each non-form-data part header
cl = part_headers.get('Content-Length')
if cl is not None and not is_valid_content_length(cl):
    raise HTTPBadRequest(text='Malformed Content-Length')

Type guard

import re
_VALID_CONTENT_LENGTH = re.compile(r'\A[0-9]+\Z')
def is_pure_digit_content_length(v: object) -> bool:
    return isinstance(v, str) and bool(_VALID_CONTENT_LENGTH.match(v))

Try / catch

try:
    part = BodyPartReader(boundary, headers, stream)
except ValueError as e:
    # invalid Content-Length header on the body part
    return web.Response(status=400, text=f'Malformed part: {e}')

Prevention

When it happens

Trigger: A multipart body part (subtype != form-data) with a Content-Length header containing a '+'/'-' sign, leading/trailing whitespace, an underscore, or non-ASCII digit characters (e.g. ' 100', '+50', '1_000', fullwidth digits).

Common situations: A custom or buggy multipart producer emitting lenient Content-Length values; proxies/gateways that normalize headers in a way int() tolerates but the spec forbids; hand-crafted multipart bodies in tests.

Related errors


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

Appendix: source

Thrown at aiohttp/multipart.py:304

        default_charset: str | None = None,
        max_decompress_size: int = DEFAULT_CHUNK_SIZE,
        client_max_size: int = sys.maxsize,
        max_size_error_cls: type[Exception] = ValueError,
    ) -> None:
        self.headers = headers
        self._boundary = boundary
        self._boundary_len = len(boundary) + 2  # Boundary + \r\n
        self._content = content
        self._default_charset = default_charset
        self._at_eof = False
        self._is_form_data = subtype == "form-data"
        # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8
        length = None if self._is_form_data else self.headers.get(CONTENT_LENGTH, None)
        if length is not None and not (length.isascii() and length.isdigit()):
            # Reject sign prefixes, underscores, whitespace and non-ASCII
            # digits that int() would otherwise accept.
            # https://www.rfc-editor.org/rfc/rfc9110#section-8.6
            raise ValueError(f"invalid Content-Length: {length!r}")
        self._length = int(length) if length is not None else None
        self._read_bytes = 0
        self._unread: deque[bytes] = deque()
        self._prev_chunk: bytes | None = None
        self._content_eof = 0
        self._cache: dict[str, Any] = {}
        self._max_decompress_size = max_decompress_size
        self._client_max_size = client_max_size
        self._max_size_error_cls = max_size_error_cls

    def __aiter__(self) -> Self:
        return self

    async def __anext__(self) -> bytes:
        part = await self.next()
        if part is None:
            raise StopAsyncIteration
        return part

View on GitHub (pinned to d041d4d0fd)