aio-libs/aiohttp · error · BadHttpMessage

Duplicate ' ' header found.

Error message

Duplicate '{name}' header found.

What it means

Raised in strict mode (self._lax=False, request parser default) when a header whose lowercased name is in SINGLETON_HEADERS appears more than once. Singletons per RFC 9110: content-length, content-location, content-range, content-type, etag, host, max-forwards, server, transfer-encoding, user-agent. Lax mode (response parsing) skips this check because real servers (Google APIs, Werkzeug) commonly send duplicates.

Solutions

  1. Send each singleton header exactly once.
  2. Use assignment (headers['Host'] = ...) or setdefault instead of headers.add() for singletons.
  3. Audit middleware that appends headers without checking presence.

Example fix

# before
headers.add('Host', 'a')
headers.add('Host', 'b')   # duplicate

# after - assign, which replaces
headers['Host'] = 'a'
Defensive patterns

Strategy: validation

Validate before calling

SINGLETONS = {'content-length','content-location','content-range','content-type','etag','host','max-forwards','server','transfer-encoding','user-agent'}
def dedupe_singletons(headers):
    seen = set()
    for k in list(headers):
        lk = k.lower()
        if lk in SINGLETONS:
            if lk in seen:
                del headers[k]
            else:
                seen.add(lk)

Type guard

SINGLETONS = {'content-length','content-location','content-range','content-type','etag','host','max-forwards','server','transfer-encoding','user-agent'}
def singleton_sent_once(names):
    lower = [n.lower() for n in names]
    return all(lower.count(s) <= 1 for s in SINGLETONS)

Try / catch

from aiohttp.http_exceptions import BadHttpMessage
try:
    ...parse...
except BadHttpMessage as e:
    # message includes which singleton duplicated; close the connection
    ...

Prevention

When it happens

Trigger: A client sends two Host: lines, two Content-Length: lines, or any other duplicate singleton in strict mode.

Common situations: Custom clients calling headers.add() twice for a singleton, proxies appending duplicates, malformed test fixtures, deliberate smuggling (duplicate CL is the classic technique).

Related errors


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

Appendix: 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 d041d4d0fd)