55``Set-Cookie`` headers from the upgrade response and echo them back as a
66``Cookie`` request header for the socket lifetime.
77
8- This is intentionally minimal: it stores name→value pairs without attribute
9- parsing (domain/path/expiry), matching the affinity-only use case in the RFD.
8+ This is intentionally minimal: it stores name→value pairs and only honors
9+ expiration attributes that remove a cookie, matching the affinity-only use case
10+ in the RFD.
1011"""
1112
1213from __future__ import annotations
1314
15+ from datetime import datetime , timezone
16+ from email .utils import parsedate_to_datetime
17+
1418__all__ = ["MemoryAcpCookieStore" ]
1519
1620
@@ -23,16 +27,23 @@ def __init__(self) -> None:
2327 def store_set_cookie (self , header_value : str ) -> None :
2428 """Ingest a single ``Set-Cookie`` header value.
2529
26- Only the leading ``name=value`` pair is retained; cookie attributes
27- (``; Path=/``, ``; HttpOnly`` etc.) are ignored.
30+ Only the leading ``name=value`` pair is retained; most cookie attributes
31+ (``; Path=/``, ``; HttpOnly`` etc.) are ignored. Expiration attributes
32+ that explicitly clear a cookie (non-positive ``Max-Age`` or a past
33+ ``Expires`` value) remove any stored cookie with the same name.
2834 """
29- first = header_value .split (";" , 1 )[0 ].strip ()
35+ parts = [part .strip () for part in header_value .split (";" )]
36+ first = parts [0 ]
3037 if not first or "=" not in first :
3138 return
3239 name , _ , value = first .partition ("=" )
3340 name = name .strip ()
34- if name :
35- self ._cookies [name ] = value .strip ()
41+ if not name :
42+ return
43+ if _is_deletion_cookie (parts [1 :]):
44+ self ._cookies .pop (name , None )
45+ return
46+ self ._cookies [name ] = value .strip ()
3647
3748 def store_set_cookies (self , header_values : list [str ]) -> None :
3849 """Ingest multiple ``Set-Cookie`` header values."""
@@ -51,3 +62,37 @@ def clear(self) -> None:
5162
5263 def __len__ (self ) -> int :
5364 return len (self ._cookies )
65+
66+
67+ def _is_deletion_cookie (attributes : list [str ]) -> bool :
68+ return _expiry_decision (attributes ) is True
69+
70+
71+ def _expiry_decision (attributes : list [str ]) -> bool | None :
72+ """Return whether attributes expire the cookie now.
73+
74+ ``True`` means delete now, ``False`` means keep the cookie, and ``None``
75+ means no usable expiration attribute was present. Per RFC 6265 section 5.3,
76+ a valid ``Max-Age`` attribute takes precedence over ``Expires``.
77+ """
78+ expires_verdict : bool | None = None
79+ for attribute in attributes :
80+ key , separator , value = attribute .partition ("=" )
81+ if not separator :
82+ continue
83+ key = key .strip ().lower ()
84+ value = value .strip ()
85+ if key == "max-age" :
86+ try :
87+ return int (value ) <= 0
88+ except ValueError :
89+ continue
90+ if key == "expires" and expires_verdict is None :
91+ try :
92+ expires_at = parsedate_to_datetime (value )
93+ except (TypeError , ValueError ):
94+ continue
95+ if expires_at .tzinfo is None :
96+ expires_at = expires_at .replace (tzinfo = timezone .utc )
97+ expires_verdict = expires_at <= datetime .now (timezone .utc )
98+ return expires_verdict
0 commit comments