aio-libs/aiohttp · error · BadHttpMessage

Duplicate '{name}' header found.

Error message

Duplicate '{name}' header found.

What it means

Raised by HeadersParser.parse_headers (aiohttp/http_parser.py:237) in strict mode (request parser) when a header name appears more than once AND its lowercased name is in SINGLETON_HEADERS: content-length, content-location, content-range, content-type, etag, host, max-forwards, server, transfer-encoding, user-agent. RFC 9110 sections 8.1/8.3 require these appear at most once. The lax response parser skips this check because real-world servers (Google APIs, Werkzeug, etc.) commonly send duplicate Content-Type/Server.

Source

Thrown at aiohttp/http_parser.py:237

                        if line:
                            continuation = line[0] in (32, 9)  # (' ', '\t')
                    else:
                        line = b""
                        break
                bvalue = b"".join(bvalue_lst)

            bvalue = bvalue.strip(b" \t")
            value = bvalue.decode("utf-8", "surrogateescape")

            # https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-5
            if self._lax:
                if "\n" in value or "\r" in value or "\x00" in value:
                    raise InvalidHeader(bvalue)
            elif _FIELD_VALUE_FORBIDDEN_CTL_RE.search(value):
                raise InvalidHeader(bvalue)

            if not self._lax and name in headers and name.lower() in SINGLETON_HEADERS:
                raise BadHttpMessage(f"Duplicate '{name}' header found.")
            headers.add(name, value)
            raw_headers.append((bname, bvalue))

        return (HeadersDictProxy(headers), tuple(raw_headers))


def _is_supported_upgrade(headers: HeadersDictProxy) -> bool:
    """Check if the upgrade header is supported."""
    u = headers.get(hdrs.UPGRADE, "")
    # .lower() can transform non-ascii characters.
    return u.isascii() and u.lower() in {"tcp", "websocket"}


class HttpParser(abc.ABC, Generic[_MsgT]):
    lax: ClassVar[bool] = False

    def __init__(
        self,

View on GitHub (pinned to c0ef574e29)

Solutions

  1. Send each singleton header exactly once; use assignment (headers[name] = value) rather than headers.add() for these fields.
  2. If you combine headers from multiple sources, deduplicate singleton names before sending.
  3. On the server side, aiohttp returns 400 automatically; log and alert on duplicates since they often indicate smuggling.
  4. Audit any proxy in the chain that may append a duplicate singleton.

Example fix

# before - .add() can stack duplicates
headers.add('Host', 'a.com')
headers.add('Host', 'b.com')   # duplicate -> rejected
# after - assignment replaces
headers['Host'] = 'a.com'
Defensive patterns

Strategy: validation

Validate before calling

from aiohttp.http_parser import SINGLETON_HEADERS
def has_duplicate_singletons(headers):
    seen, dups = set(), set()
    for k in headers.keys():
        lk = k.lower()
        if lk in SINGLETON_HEADERS:
            (dups if lk in seen else seen).add(lk)
    return dups
if has_duplicate_singletons(outgoing):
    raise ValueError('duplicate singleton header')

Prevention

When it happens

Trigger: A request containing two of any singleton header - e.g. two Host: lines, two Content-Length: lines, or two Content-Type: lines. Duplicate Content-Length is the classic request-smuggling vector; duplicate User-Agent often comes from proxies appending their own.

Common situations: Request-smuggling attacks (duplicate Content-Length), proxies or SDKs that append a second User-Agent/Host, naive clients that call headers.add() instead of assignment, or buggy gateways that duplicate Content-Type.

Related errors


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