aio-libs/aiohttp · error · BadHttpMessage

Transfer-Encoding can't be present with Content-Length

Error message

Transfer-Encoding can't be present with Content-Length

What it means

Raised by HttpParser.parse_headers (aiohttp/http_parser.py:631) when a message carries BOTH Transfer-Encoding and Content-Length. RFC 9112 section 6.3 forbids the combination because it creates the classic request-smuggling ambiguity (which header wins?). aiohttp rejects it outright in both request and response parsers, regardless of lax mode.

Source

Thrown at aiohttp/http_parser.py:631

                close_conn = False

            # https://www.rfc-editor.org/rfc/rfc9110.html#name-101-switching-protocols
            if "upgrade" in conn_tokens and headers.get(hdrs.UPGRADE):
                upgrade = True

        # encoding
        enc = headers.get(hdrs.CONTENT_ENCODING, "")
        if enc.isascii() and enc.lower() in {"gzip", "deflate", "br", "zstd"}:
            encoding = enc

        # chunking
        te = headers.get(hdrs.TRANSFER_ENCODING)
        if te is not None:
            if self._is_chunked_te(te):
                chunked = True

            if hdrs.CONTENT_LENGTH in headers:
                raise BadHttpMessage(
                    "Transfer-Encoding can't be present with Content-Length",
                )

        return (headers, raw_headers, close_conn, encoding, upgrade, chunked)

    def set_upgraded(self, val: bool) -> None:
        """Set connection upgraded (to websocket) mode.

        :param bool val: new state.
        """
        self._upgraded = val


class HttpRequestParser(HttpParser[RawRequestMessage]):
    """Read request status line.

    Exception .http_exceptions.BadStatusLine
    could be raised in case of any errors in status line.

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Send exactly one of Transfer-Encoding: chunked OR Content-Length - never both.
  2. If you operate a proxy, strip one before forwarding (prefer normalizing to a single mechanism).
  3. Let aiohttp handle chunked encoding automatically when you stream a body; do not also set Content-Length.
  4. On the server, aiohttp returns 400; investigate the upstream proxy chain.

Example fix

# before - both set
headers['Transfer-Encoding'] = 'chunked'
headers['Content-Length'] = str(len(body))   # conflict!
# after - pick one; when chunked, drop Content-Length
headers['Transfer-Encoding'] = 'chunked'
headers.pop('Content-Length', None)
Defensive patterns

Strategy: validation

Validate before calling

def no_te_cl_conflict(headers) -> bool:
    has_te = any(k.lower() == 'transfer-encoding' for k in headers)
    has_cl = any(k.lower() == 'content-length' for k in headers)
    return not (has_te and has_cl)
assert no_te_cl_conflict(outgoing), 'TE and CL both present'

Prevention

When it happens

Trigger: Any request or response whose headers contain both Transfer-Encoding (any value) and Content-Length. Even 'Transfer-Encoding: identity' alongside Content-Length triggers it because the check keys off the mere presence of both headers.

Common situations: Request-smuggling attacks against proxy chains, HTTP/1.1 downgrade attacks in front of HTTP/2 backends, or a proxy that adds one header without stripping the other when forwarding.

Related errors


AI-assisted analysis of aio-libs/aiohttp@c0ef574e29 (2026-08-04). Data as JSON: /data/errors/949d8f3a00373c20.json. Report an issue: GitHub.