{"id":"949d8f3a00373c20","repo":"aio-libs/aiohttp","slug":"transfer-encoding-can-t-be-present-with-content-le","errorCode":null,"errorMessage":"Transfer-Encoding can't be present with Content-Length","messagePattern":"Transfer-Encoding can't be present with Content-Length","errorType":"http","errorClass":"BadHttpMessage","httpStatus":400,"severity":"error","filePath":"aiohttp/http_parser.py","lineNumber":631,"sourceCode":"                close_conn = False\n\n            # https://www.rfc-editor.org/rfc/rfc9110.html#name-101-switching-protocols\n            if \"upgrade\" in conn_tokens and headers.get(hdrs.UPGRADE):\n                upgrade = True\n\n        # encoding\n        enc = headers.get(hdrs.CONTENT_ENCODING, \"\")\n        if enc.isascii() and enc.lower() in {\"gzip\", \"deflate\", \"br\", \"zstd\"}:\n            encoding = enc\n\n        # chunking\n        te = headers.get(hdrs.TRANSFER_ENCODING)\n        if te is not None:\n            if self._is_chunked_te(te):\n                chunked = True\n\n            if hdrs.CONTENT_LENGTH in headers:\n                raise BadHttpMessage(\n                    \"Transfer-Encoding can't be present with Content-Length\",\n                )\n\n        return (headers, raw_headers, close_conn, encoding, upgrade, chunked)\n\n    def set_upgraded(self, val: bool) -> None:\n        \"\"\"Set connection upgraded (to websocket) mode.\n\n        :param bool val: new state.\n        \"\"\"\n        self._upgraded = val\n\n\nclass HttpRequestParser(HttpParser[RawRequestMessage]):\n    \"\"\"Read request status line.\n\n    Exception .http_exceptions.BadStatusLine\n    could be raised in case of any errors in status line.","sourceCodeStart":613,"sourceCodeEnd":649,"githubUrl":"https://github.com/aio-libs/aiohttp/blob/c0ef574e29109210e96e652771ae4e7b88615fa4/aiohttp/http_parser.py#L613-L649","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Send exactly one of Transfer-Encoding: chunked OR Content-Length - never both.","If you operate a proxy, strip one before forwarding (prefer normalizing to a single mechanism).","Let aiohttp handle chunked encoding automatically when you stream a body; do not also set Content-Length.","On the server, aiohttp returns 400; investigate the upstream proxy chain."],"exampleFix":"# before - both set\nheaders['Transfer-Encoding'] = 'chunked'\nheaders['Content-Length'] = str(len(body))   # conflict!\n# after - pick one; when chunked, drop Content-Length\nheaders['Transfer-Encoding'] = 'chunked'\nheaders.pop('Content-Length', None)","handlingStrategy":"validation","validationCode":"def no_te_cl_conflict(headers) -> bool:\n    has_te = any(k.lower() == 'transfer-encoding' for k in headers)\n    has_cl = any(k.lower() == 'content-length' for k in headers)\n    return not (has_te and has_cl)\nassert no_te_cl_conflict(outgoing), 'TE and CL both present'","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never set both Transfer-Encoding and Content-Length on the same message","Strip one at every proxy hop","Let the library manage the framing when you stream a body"],"tags":["http","security","request-smuggling","parser","headers","transfer-encoding"],"analyzedSha":"c0ef574e29109210e96e652771ae4e7b88615fa4","analyzedAt":"2026-08-04T19:51:05.467Z","schemaVersion":2}