aio-libs/aiohttp · error · BadHttpMessage

Request has invalid `Transfer-Encoding`

Error message

Request has invalid `Transfer-Encoding`

What it means

Raised by the request parser in HttpRequestParser._is_chunked_te when an inbound request carries a Transfer-Encoding header whose final token is not 'chunked'. aiohttp only supports chunked transfer-coding for request bodies; any other coding (or a TE that does not terminate in chunked) is rejected per RFC 9112 section 6.3.

Source

Thrown at aiohttp/http_parser.py:751

            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 c0ef574e29)

Solutions

  1. Inspect the request's Transfer-Encoding header and ensure it ends with 'chunked' or remove it and use Content-Length instead.
  2. If you need a content encoding, use Content-Encoding (end-to-end) rather than Transfer-Encoding (hop-by-hop).
  3. On the server side, catch BadHttpMessage to return a clean 400 instead of crashing the handler.

Example fix

// before: client sends
//   Transfer-Encoding: gzip

# after: use Content-Length or chunked
#   Content-Length: 42
  (or)
#   Transfer-Encoding: chunked
Defensive patterns

Strategy: try-catch

Validate before calling

te = request.headers.get('Transfer-Encoding', '')
parts = [p.strip().lower() for p in te.split(',')]
if parts and parts[-1] != 'chunked' and te:
    # reject before the parser does
    raise ValueError('unsupported Transfer-Encoding')

Type guard

def is_valid_request_te(te: str) -> bool:
    if not te:
        return True
    parts = [p.strip().lower() for p in te.split(',')]
    return parts.count('chunked') <= 1 and parts[-1] == 'chunked'

Try / catch

from aiohttp.http_exceptions import BadHttpMessage
try:
    await request.read()
except BadHttpMessage:
    return web.Response(status=400, text='Bad Transfer-Encoding')

Prevention

When it happens

Trigger: A client sends a request with Transfer-Encoding set to a non-chunked value (e.g. `Transfer-Encoding: gzip`, `compress`, or `identity`) or a list that does not end in `chunked`. The request parser calls _is_chunked_te and, because the last comma-separated token is not 'chunked', raises BadHttpMessage (HTTP 400).

Common situations: Misconfigured reverse proxies or custom HTTP clients injecting a raw Transfer-Encoding; curl/scripting that sets TE manually; interop with servers that send non-standard codings; testing against a server that forwards a hop-by-hop TE.

Related errors


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