aio-libs/aiohttp · error · BadHttpMessage

Request has invalid `Transfer-Encoding`

Error message

Request has invalid `Transfer-Encoding`

What it means

Raised as BadHttpMessage (HTTP 400) when a Transfer-Encoding header is present and its last comma-separated token is not 'chunked'. RFC 9112 §6.3 requires that, when chunked is used, it must be the final transfer coding. The guard in _is_chunked_te returns True only if the last token (ASCII, case-insensitive) equals 'chunked'; otherwise it raises this error.

Solutions

  1. Ensure the Transfer-Encoding header, when present, ends with 'chunked' as its last token.
  2. Use Content-Encoding (not Transfer-Encoding) for gzip/deflate/br body compression.
  3. Remove the Transfer-Encoding header entirely if you want a fixed-length body and send Content-Length instead.
  4. Audit intermediaries that rewrite TE to make sure they preserve chunked-last ordering.

Example fix

// before (non-chunked last token)
Transfer-Encoding: gzip\r\n

// after (use Content-Encoding for compression)
Content-Encoding: gzip\r\nContent-Length: 123\r\n

// or, if chunked streaming is intended, chunked must be last
Transfer-Encoding: gzip, chunked\r
Defensive patterns

Strategy: validation

Validate before calling

def is_valid_te(te: str) -> bool:
    parts = [p.strip(" \t") for p in te.split(',')]
    last = parts[-1]
    return last.isascii() and last.lower() == 'chunked'

# use Content-Encoding for compression, not a non-chunked Transfer-Encoding

Try / catch

from aiohttp.http_exceptions import BadHttpMessage

try:
    parser.feed_data(raw)
except BadHttpMessage as e:
    respond_400(str(e))

Prevention

When it happens

Trigger: A request with a Transfer-Encoding whose last token is something other than 'chunked' (e.g. 'gzip', 'identity', or an unknown coding), or where 'chunked' appears but is not last. Fires inside _is_chunked_te when the TE header is non-empty and does not end in chunked.

Common situations: Client/intermediary sends 'Transfer-Encoding: gzip' (non-standard, should use Content-Encoding); a custom coding name with a typo; a server/old client using deprecated encodings; a proxy reordering TE tokens so chunked is not last; fuzzers probing the parser.

Related errors


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

Appendix: source

Thrown at aiohttp/http_parser.py:753

            upgrade,
            chunked,
            url,
        )

    def _is_chunked_te(self, te: str) -> bool:
        # https://www.rfc-editor.org/rfc/rfc9112#section-7.1-3
        # "A sender MUST NOT apply the chunked transfer coding more
        #  than once to a message body"
        parts = [p.strip(" \t") for p in te.split(",")]
        chunked_count = sum(1 for p in parts if p.isascii() and p.lower() == "chunked")
        if chunked_count > 1:
            raise BadHttpMessage("Request has duplicate `chunked` Transfer-Encoding")
        last = parts[-1]
        # .lower() transforms some non-ascii chars, so must check first.
        if last.isascii() and last.lower() == "chunked":
            return True
        # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.3
        raise BadHttpMessage("Request has invalid `Transfer-Encoding`")


class HttpResponseParser(HttpParser[RawResponseMessage]):
    """Read response status line and headers.

    BadStatusLine could be raised in case of any errors in status line.
    Returns RawResponseMessage.
    """

    protocol: "ResponseHandler"

    # Lax mode should only be enabled on response parser.
    lax = not DEBUG

    def feed_data(
        self,
        data: bytes,
        SEP: _SEP | None = None,

View on GitHub (pinned to d041d4d0fd)