Describe the bug
It is common to have a TLS-terminating reverse proxy (Apache, nginx) forward client certificates to backend applications via an HTTP header such as X-CLIENT-CERT or X-SSL-CLIENT-CERT. When the client certificate uses ML-DSA-87, aiohttp's server rejects the request at the HTTP parsing layer:
aiohttp.http_exceptions.LineTooLong: 400, message:
Got more than 8190 bytes when reading header value
This is because aiohttp's HTTP parser defaults max_field_size to 8190 bytes, and post-quantum X.509 certificates are larger than the algorithms in current widespread use.
An ML-DSA-65 certificate tends to come just under the limit, but an ML-DSA-87 certificate exceeds it (roughly 10k bytes)
Admittedly client certs are not that common, but this limitation could be documented more prominently or the default increased slightly to accommodate the new algorithms.
Otherwise I just wanted to document this for posterity should anyone else run into it.
To Reproduce
Reproducer produced by Claude Opus - yes I tried it - yes it worked.
You do need a very new version of openssl for it to work though. Fedora and CentOS Stream 10 have all of the new PQC stuff enabled, I don't know which other distros would.
import asyncio
import subprocess
import tempfile
from pathlib import Path
from urllib.parse import quote
from aiohttp import web, ClientSession
async def handle(request):
cert = request.headers.get("X-CLIENT-CERT", "")
return web.Response(text=f"ok, cert length: {len(cert)}")
def generate_mldsa87_client_cert(workdir: Path) -> str:
"""Generate a self-signed ML-DSA-87 client certificate. Returns PEM string."""
key_path = workdir / "client.key"
cert_path = workdir / "client.crt"
subprocess.run(
["openssl", "req", "-x509", "-newkey", "ML-DSA-87", "-days", "1",
"-subj", "/CN=test-client", "-noenc",
"-keyout", str(key_path), "-out", str(cert_path)],
check=True, capture_output=True,
)
return cert_path.read_text()
async def main():
with tempfile.TemporaryDirectory() as td:
cert_pem = generate_mldsa87_client_cert(Path(td))
encoded_cert = quote(cert_pem)
print(f"ML-DSA-87 cert PEM: {len(cert_pem)} bytes")
print(f"URL-encoded: {len(encoded_cert)} bytes")
print(f"aiohttp default: 8190 bytes")
print()
# Start aiohttp server with default settings
app = web.Application() # max_field_size defaults to 8190
app.router.add_get("/", handle)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "127.0.0.1", 8080)
await site.start()
# Simulate what a reverse proxy does: forward the client cert as a header
async with ClientSession() as session:
resp = await session.get(
"http://127.0.0.1:8080/",
headers={"X-CLIENT-CERT": encoded_cert},
)
print(f"Response status: {resp.status}")
if resp.status == 400:
print("FAILED: aiohttp rejected the header as too large")
print(" (LineTooLong: Got more than 8190 bytes)")
else:
print(f"OK: {await resp.text()}")
await runner.cleanup()
asyncio.run(main())
Expected behavior
Request should succeed
It can be worked around by changing the default value of max_field_size:
-app = web.Application()
+# PQC (post-quantum) X.509 certificates can exceed aiohttp's default 8190-byte header
+# limit when forwarded via X-CLIENT-CERT by a reverse proxy.
+app = web.Application(handler_args={"max_field_size": 16 * 1024})
Logs/tracebacks
Traceback (most recent call last):
File "/usr/lib64/python3.12/site-packages/aiohttp/web_protocol.py", line 415, in data_received
messages, upgraded, tail = self._request_parser.feed_data(data)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "aiohttp/_http_parser.pyx", line 593, in aiohttp._http_parser.HttpParser.feed_data
File "aiohttp/_http_parser.pyx", line 769, in aiohttp._http_parser.cb_on_header_value
aiohttp.http_exceptions.LineTooLong: 400, message:
Got more than 8190 bytes when reading: b'-----BEGIN CERTIFICATE----- MIIYQTCCCz6gAwIBAgIIcNsAKyLFH3owCwYJYIZIAWUDBAMSMEAxCzAJBgNVBAYT AkNaMRA...'.
:1 [16/Jul/2026:21:44:31 +0000] "UNKNOWN / HTTP/1.0" 400 309 "-" "-"
Python Version
aiohttp Version
multidict Version
propcache Version
yarl Version
OS
CentOS Stream 10
Related component
Server, Client
Additional context
No response
Code of Conduct
Describe the bug
It is common to have a TLS-terminating reverse proxy (Apache, nginx) forward client certificates to backend applications via an HTTP header such as X-CLIENT-CERT or X-SSL-CLIENT-CERT. When the client certificate uses ML-DSA-87, aiohttp's server rejects the request at the HTTP parsing layer:
This is because aiohttp's HTTP parser defaults
max_field_sizeto 8190 bytes, and post-quantum X.509 certificates are larger than the algorithms in current widespread use.An ML-DSA-65 certificate tends to come just under the limit, but an ML-DSA-87 certificate exceeds it (roughly 10k bytes)
Admittedly client certs are not that common, but this limitation could be documented more prominently or the default increased slightly to accommodate the new algorithms.
Otherwise I just wanted to document this for posterity should anyone else run into it.
To Reproduce
Reproducer produced by Claude Opus - yes I tried it - yes it worked.
You do need a very new version of
opensslfor it to work though. Fedora and CentOS Stream 10 have all of the new PQC stuff enabled, I don't know which other distros would.Expected behavior
Request should succeed
It can be worked around by changing the default value of
max_field_size:Logs/tracebacks
Traceback (most recent call last): File "/usr/lib64/python3.12/site-packages/aiohttp/web_protocol.py", line 415, in data_received messages, upgraded, tail = self._request_parser.feed_data(data) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "aiohttp/_http_parser.pyx", line 593, in aiohttp._http_parser.HttpParser.feed_data File "aiohttp/_http_parser.pyx", line 769, in aiohttp._http_parser.cb_on_header_value aiohttp.http_exceptions.LineTooLong: 400, message: Got more than 8190 bytes when reading: b'-----BEGIN CERTIFICATE----- MIIYQTCCCz6gAwIBAgIIcNsAKyLFH3owCwYJYIZIAWUDBAMSMEAxCzAJBgNVBAYT AkNaMRA...'. :1 [16/Jul/2026:21:44:31 +0000] "UNKNOWN / HTTP/1.0" 400 309 "-" "-"Python Version
3.12aiohttp Version
Version: 3.14.3multidict Version
Version: 6.7.1propcache Version
Version: 0.5.2yarl Version
Version: 1.24.5OS
CentOS Stream 10
Related component
Server, Client
Additional context
No response
Code of Conduct