The Fix
Fix headers being mutated if passed to web.Response as a CIMultiDict.
Based on closed aio-libs/aiohttp issue #10670 · PR/commit linked
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+Fixed :class:`multidict.CIMultiDict` being mutated when passed to :class:`aiohttp.web.Response` -- by :user:`bdraco`.
diff --git a/aiohttp/web_response.py b/aiohttp/web_response.py
index d1bb401a5e6..56596905a35 100644
import asyncio
import json
import random
import aiohttp
from aiohttp import web, ClientSession
from multidict import CIMultiDict
default_headers = CIMultiDict({"Content-Type": "application/json"})
async def handle(request):
print(aiohttp.__version__)
print(default_headers)
return web.Response(
text=json.dumps({"message": random.randbytes(random.randint(1, 1000)).hex()}),
headers=default_headers
)
async def run_server():
app = web.Application()
app.router.add_get("/", handle)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "localhost", 8080)
await site.start()
async def run_client():
async with ClientSession() as session:
for i in range(10):
await asyncio.sleep(1)
async with session.get("http://localhost:8080") as resp:
print(await resp.json())
async def main():
server_task = asyncio.create_task(run_server())
client_task = asyncio.create_task(run_client())
await client_task # Run client once
server_task.cancel() # Stop server
if __name__ == "__main__":
asyncio.run(main())
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Option A — Apply the official fix\nFix headers being mutated if passed to web.Response as a CIMultiDict.\nWhen NOT to use: Do not use this fix if headers should remain mutable for other responses.\n\nOption C — Workaround\nis to pass dict instead of CIMultiDict\nWhen NOT to use: Do not use this fix if headers should remain mutable for other responses.\n\n
Why This Fix Works in Production
- Trigger: 3.11.15
- Mechanism: Fix headers being mutated if passed to web.Response as a CIMultiDict.
- If left unfixed, this can cause silent data inconsistencies that propagate (bad cache entries, incorrect downstream decisions).
Why This Breaks in Prod
- Shows up under Python 3.13 in real deployments (not just unit tests).
- Surfaces as: 3.11.15
Proof / Evidence
- GitHub issue: #10670
- Fix PR: https://github.com/aio-libs/aiohttp/pull/10672
- Reproduced locally: No (not executed)
- Last verified: 2026-02-09
- Confidence: 0.70
- Did this fix it?: Yes (upstream fix exists)
- Own content ratio: 0.29
Discussion
High-signal excerpts from the issue thread (symptoms, repros, edge-cases).
“I am using the same version of multidict==6.3.0”
“OK, maybe it's related to a performance improvement recently. @bdraco?”
“I don't see anything that'd cause that though”
“Right, so it's been there for some time then. Looks like that could be the cause. At a quick glance, looks like we may need…”
Failure Signature (Search String)
- 3.11.15
Error Message
Stack trace
Error Message
-------------
3.11.15
<CIMultiDict('Content-Type': 'application/json')>
{'message': '7fd6a48fe3d8'}
3.11.15
<CIMultiDict('Content-Type': 'application/json; charset=utf-8', 'Content-Length': '1763', 'Date': 'Tue, 01 Apr 2025 13:08:07 GMT', 'Server': 'Python/3.13 aiohttp/3.11.15')>
Traceback (most recent call last):
File "/src/auth/.venv/lib/python3.13/site-packages/aiohttp/client_proto.py", line 264, in data_received
messages, upgraded, tail = self._parser.feed_data(data)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "aiohttp/_http_parser.pyx", line 558, in aiohttp._http_parser.HttpParser.feed_data
aiohttp.http_exceptions.BadHttpMessage: 400, message:
Expected HTTP/:
b'{
Minimal Reproduction
import asyncio
import json
import random
import aiohttp
from aiohttp import web, ClientSession
from multidict import CIMultiDict
default_headers = CIMultiDict({"Content-Type": "application/json"})
async def handle(request):
print(aiohttp.__version__)
print(default_headers)
return web.Response(
text=json.dumps({"message": random.randbytes(random.randint(1, 1000)).hex()}),
headers=default_headers
)
async def run_server():
app = web.Application()
app.router.add_get("/", handle)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "localhost", 8080)
await site.start()
async def run_client():
async with ClientSession() as session:
for i in range(10):
await asyncio.sleep(1)
async with session.get("http://localhost:8080") as resp:
print(await resp.json())
async def main():
server_task = asyncio.create_task(run_server())
client_task = asyncio.create_task(run_client())
await client_task # Run client once
server_task.cancel() # Stop server
if __name__ == "__main__":
asyncio.run(main())
Environment
- Python: 3.13
What Broke
Responses failed due to incorrect Content-Length, leading to BadHttpMessage errors.
Fix Options (Details)
Option A — Apply the official fix
Fix headers being mutated if passed to web.Response as a CIMultiDict.
Option C — Workaround Temporary workaround
is to pass dict instead of CIMultiDict
Use only if you cannot change versions today. Treat this as a stopgap and remove once upgraded.
Fix reference: https://github.com/aio-libs/aiohttp/pull/10672
Last verified: 2026-02-09. Validate in your environment.
When NOT to Use This Fix
- Do not use this fix if headers should remain mutable for other responses.
Verify Fix
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Did This Fix Work in Your Case?
Quick signal helps us prioritize which fixes to verify and improve.
Prevention
- Capture the exact failing error string in logs and tests so you can reproduce via a minimal script.
- Pin production dependencies and upgrade only with a reproducible test that hits the failing path.
Version Compatibility Table
| Version | Status |
|---|---|
| 3.11.8 | Broken |
Related Issues
No related fixes found.
Sources
We don’t republish the full GitHub discussion text. Use the links above for context.