@@ -136,7 +136,8 @@ def __init__(
136136 self ._morsel_cache : defaultdict [tuple [str , str ], dict [str , Morsel [str ]]] = (
137137 defaultdict (dict )
138138 )
139- self ._host_only_cookies : set [tuple [str , str ]] = set ()
139+ # Cookie identity is (domain, path, name).
140+ self ._host_only_cookies : set [tuple [str , str , str ]] = set ()
140141 self ._unsafe = unsafe
141142 self ._quote_cookie = quote_cookie
142143 if treat_as_secure_origin is None :
@@ -168,7 +169,7 @@ def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]:
168169 return MappingProxyType (self ._cookies )
169170
170171 @property
171- def host_only_cookies (self ) -> frozenset [tuple [str , str ]]:
172+ def host_only_cookies (self ) -> frozenset [tuple [str , str , str ]]:
172173 """Return the host-only cookies stored in this jar."""
173174 return frozenset (self ._host_only_cookies )
174175
@@ -197,7 +198,7 @@ def save(self, file_path: PathLike) -> None:
197198 if attr_val :
198199 morsel_data [attr ] = attr_val
199200 # Persist or it reloads as a domain cookie and leaks to subdomains.
200- if (domain , name ) in self ._host_only_cookies :
201+ if (domain , path , name ) in self ._host_only_cookies :
201202 morsel_data ["host_only" ] = True
202203 if (exp := self ._expirations .get ((domain , path , name ))) is not None :
203204 morsel_data ["expires_timestamp" ] = exp
@@ -360,7 +361,7 @@ def _do_expiration(self) -> None:
360361
361362 def _delete_cookies (self , to_del : list [tuple [str , str , str ]]) -> None :
362363 for domain , path , name in to_del :
363- self ._host_only_cookies .discard ((domain , name ))
364+ self ._host_only_cookies .discard ((domain , path , name ))
364365 self ._cookies [(domain , path )].pop (name , None )
365366 self ._morsel_cache [(domain , path )].pop (name , None )
366367 self ._expirations .pop ((domain , path , name ), None )
@@ -414,18 +415,12 @@ def _update_cookies(
414415 domain = ""
415416 del cookie ["domain" ]
416417
417- if not domain and hostname is not None :
418- # Set the cookie's domain to the response hostname
419- # and set its host-only-flag
420- self ._host_only_cookies .add ((hostname , name ))
421- domain = cookie ["domain" ] = hostname
422-
423418 if domain and domain [0 ] == "." :
424419 # Remove leading dot
425420 domain = domain [1 :]
426421 cookie ["domain" ] = domain
427422
428- if hostname and not self ._is_domain_match (domain , hostname ):
423+ if domain and hostname and not self ._is_domain_match (domain , hostname ):
429424 # Setting cookies for different domains is not allowed
430425 continue
431426
@@ -441,6 +436,14 @@ def _update_cookies(
441436 cookie ["path" ] = path
442437 path = path .rstrip ("/" )
443438
439+ if not domain and hostname is not None :
440+ self ._host_only_cookies .add ((hostname , path , name ))
441+ domain = cookie ["domain" ] = hostname
442+ else :
443+ # A cookie with an explicit Domain attribute replaces any
444+ # host-only cookie with the same (domain, path, name) identity.
445+ self ._host_only_cookies .discard ((domain , path , name ))
446+
444447 if max_age := cookie ["max-age" ]:
445448 try :
446449 delta_seconds = int (max_age )
@@ -530,7 +533,7 @@ def filter_cookies(self, request_url: URL = URL()) -> "BaseCookie[str]":
530533 for name , cookie in self ._cookies [p ].items ():
531534 domain = cookie ["domain" ]
532535
533- if ( domain , name ) in self ._host_only_cookies and domain != hostname :
536+ if domain != hostname and p + ( name , ) in self ._host_only_cookies :
534537 continue
535538
536539 # Skip edge case when the cookie has a trailing slash but request doesn't.
@@ -680,7 +683,7 @@ def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]:
680683 return MappingProxyType ({})
681684
682685 @property
683- def host_only_cookies (self ) -> frozenset [tuple [str , str ]]:
686+ def host_only_cookies (self ) -> frozenset [tuple [str , str , str ]]:
684687 """Return an empty frozenset."""
685688 return frozenset ()
686689
0 commit comments