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
- Send each singleton header exactly once.
- Use assignment (headers['Host'] = ...) or setdefault instead of headers.add() for singletons.
- 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
- Prefer headers['Name'] = value (replace) over headers.add() for singletons.
- Remember lax mode tolerates duplicates; strict mode does not - test with the strict request parser.
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
- Transfer-Encoding can't be present with Content-Length
- Bad HTTP method in status line
- Invalid HTTP header
- Bad HTTP method in status line
- Bad line ending, expected CRLF
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)