Skip to content

Commit e2eb195

Browse files
authored
Fix CookieJar memory leak in filter_cookies() (#11054)
1 parent 7f69167 commit e2eb195

File tree

4 files changed

+59
-0
lines changed

4 files changed

+59
-0
lines changed

CHANGES/11052.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed memory leak in :py:meth:`~aiohttp.CookieJar.filter_cookies` that caused unbounded memory growth
2+
when making requests to different URL paths -- by :user:`bdraco` and :user:`Cycloctane`.

CHANGES/11054.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
11052.bugfix.rst

aiohttp/cookiejar.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,8 @@ def filter_cookies(self, request_url: URL) -> "BaseCookie[str]":
354354
path_len = len(request_url.path)
355355
# Point 2: https://www.rfc-editor.org/rfc/rfc6265.html#section-5.4
356356
for p in pairs:
357+
if p not in self._cookies:
358+
continue
357359
for name, cookie in self._cookies[p].items():
358360
domain = cookie["domain"]
359361

tests/test_cookiejar.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,3 +1151,57 @@ async def test_treat_as_secure_origin() -> None:
11511151
assert len(jar) == 1
11521152
filtered_cookies = jar.filter_cookies(request_url=endpoint)
11531153
assert len(filtered_cookies) == 1
1154+
1155+
1156+
async def test_filter_cookies_does_not_leak_memory() -> None:
1157+
"""Test that filter_cookies doesn't create empty cookie entries.
1158+
1159+
Regression test for https://github.com/aio-libs/aiohttp/issues/11052
1160+
"""
1161+
jar = CookieJar()
1162+
1163+
# Set a cookie with Path=/
1164+
jar.update_cookies({"test_cookie": "value; Path=/"}, URL("http://example.com/"))
1165+
1166+
# Check initial state
1167+
assert len(jar) == 1
1168+
initial_storage_size = len(jar._cookies)
1169+
initial_morsel_cache_size = len(jar._morsel_cache)
1170+
1171+
# Make multiple requests with different paths
1172+
paths = [
1173+
"/",
1174+
"/api",
1175+
"/api/v1",
1176+
"/api/v1/users",
1177+
"/api/v1/users/123",
1178+
"/static/css/style.css",
1179+
"/images/logo.png",
1180+
]
1181+
1182+
for path in paths:
1183+
url = URL(f"http://example.com{path}")
1184+
filtered = jar.filter_cookies(url)
1185+
# Should still get the cookie
1186+
assert len(filtered) == 1
1187+
assert "test_cookie" in filtered
1188+
1189+
# Storage size should not grow significantly
1190+
# Only the shared cookie entry ('', '') may be added
1191+
final_storage_size = len(jar._cookies)
1192+
assert final_storage_size <= initial_storage_size + 1
1193+
1194+
# Verify _morsel_cache doesn't leak either
1195+
# It should only have entries for domains/paths where cookies exist
1196+
final_morsel_cache_size = len(jar._morsel_cache)
1197+
assert final_morsel_cache_size <= initial_morsel_cache_size + 1
1198+
1199+
# Verify no empty entries were created for domain-path combinations
1200+
for key, cookies in jar._cookies.items():
1201+
if key != ("", ""): # Skip the shared cookie entry
1202+
assert len(cookies) > 0, f"Empty cookie entry found for {key}"
1203+
1204+
# Verify _morsel_cache entries correspond to actual cookies
1205+
for key, morsels in jar._morsel_cache.items():
1206+
assert key in jar._cookies, f"Orphaned morsel cache entry for {key}"
1207+
assert len(morsels) > 0, f"Empty morsel cache entry found for {key}"

0 commit comments

Comments
 (0)