From fa0f60b87b65e4b0421a50561b79c198744140fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Jun 2025 21:38:46 -0500 Subject: [PATCH 1/7] fix cookie header parser --- aiohttp/_cookie_helpers.py | 61 +++++++- aiohttp/client_reqrep.py | 10 +- aiohttp/web_request.py | 9 +- tests/test_cookie_helpers.py | 289 +++++++++++++++++++++++++++++++++++ tests/test_web_request.py | 26 ++-- 5 files changed, 378 insertions(+), 17 deletions(-) diff --git a/aiohttp/_cookie_helpers.py b/aiohttp/_cookie_helpers.py index 8184cc9bdc1..06f8f7444ff 100644 --- a/aiohttp/_cookie_helpers.py +++ b/aiohttp/_cookie_helpers.py @@ -12,7 +12,11 @@ from .log import internal_logger -__all__ = ("parse_cookie_headers", "preserve_morsel_with_coded_value") +__all__ = ( + "parse_cookie_headers", + "parse_cookie_header", + "preserve_morsel_with_coded_value", +) # Cookie parsing constants # Allow more characters in cookie names to handle real-world cookies @@ -124,6 +128,61 @@ def _unquote(text: str) -> str: return text +def parse_cookie_header(header: str) -> List[Tuple[str, Morsel[str]]]: + """ + Parse a Cookie header according to RFC 6265 Section 5.4. + + Cookie headers contain only name-value pairs separated by semicolons. + There are no attributes in Cookie headers - even names that match + attribute names (like 'path' or 'secure') should be treated as cookies. + + This parser uses the same regex-based approach as parse_cookie_headers + to properly handle quoted values that may contain semicolons. + + Args: + header: The Cookie header value to parse + + Returns: + List of (name, Morsel) tuples for compatibility with SimpleCookie.update() + """ + if not header: + return [] + + cookies: List[Tuple[str, Morsel[str]]] = [] + i = 0 + n = len(header) + + while i < n: + # Use the same pattern as parse_cookie_headers to find cookies + match = _COOKIE_PATTERN.match(header, i) + if not match: + break + + key = match.group("key") + value = match.group("val") or "" + i = match.end(0) + + # Validate the name + if not key or not _COOKIE_NAME_RE.match(key): + internal_logger.warning("Can not load cookie: Illegal cookie name %r", key) + continue + + # Create new morsel + morsel: Morsel[str] = Morsel() + # Preserve the original value as coded_value (with quotes if present) + # We use __setstate__ instead of the public set() API because it allows us to + # bypass validation and set already validated state. This is more stable than + # setting protected attributes directly and unlikely to change since it would + # break pickling. + morsel.__setstate__( # type: ignore[attr-defined] + {"key": key, "value": _unquote(value), "coded_value": value} + ) + + cookies.append((key, morsel)) + + return cookies + + def parse_cookie_headers(headers: Sequence[str]) -> List[Tuple[str, Morsel[str]]]: """ Parse cookie headers using a vendored version of SimpleCookie parsing. diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 88f81326215..eb7b6aa88ec 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -30,7 +30,11 @@ from yarl import URL from . import hdrs, helpers, http, multipart, payload -from ._cookie_helpers import parse_cookie_headers, preserve_morsel_with_coded_value +from ._cookie_helpers import ( + parse_cookie_header, + parse_cookie_headers, + preserve_morsel_with_coded_value, +) from .abc import AbstractStreamWriter from .client_exceptions import ( ClientConnectionError, @@ -1014,8 +1018,8 @@ def update_cookies(self, cookies: Optional[LooseCookies]) -> None: c = SimpleCookie() if hdrs.COOKIE in self.headers: - # parse_cookie_headers already preserves coded values - c.update(parse_cookie_headers((self.headers.get(hdrs.COOKIE, ""),))) + # parse_cookie_header for RFC 6265 compliant Cookie header parsing + c.update(parse_cookie_header(self.headers.get(hdrs.COOKIE, ""))) del self.headers[hdrs.COOKIE] if isinstance(cookies, Mapping): diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index dfd5a530e3b..5f0317954d5 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -28,7 +28,7 @@ from yarl import URL from . import hdrs -from ._cookie_helpers import parse_cookie_headers +from ._cookie_helpers import parse_cookie_header from .abc import AbstractStreamWriter from .helpers import ( _SENTINEL, @@ -556,9 +556,10 @@ def cookies(self) -> Mapping[str, str]: A read-only dictionary-like object. """ - # Use parse_cookie_headers for more lenient parsing that accepts - # special characters in cookie names (fixes #2683) - parsed = parse_cookie_headers((self.headers.get(hdrs.COOKIE, ""),)) + # Use parse_cookie_header for RFC 6265 compliant Cookie header parsing + # that accepts special characters in cookie names (fixes #2683) + parsed = parse_cookie_header(self.headers.get(hdrs.COOKIE, "")) + # Extract values from Morsel objects return MappingProxyType({name: morsel.value for name, morsel in parsed}) @reify diff --git a/tests/test_cookie_helpers.py b/tests/test_cookie_helpers.py index 7a2ac7493ee..b0693366c96 100644 --- a/tests/test_cookie_helpers.py +++ b/tests/test_cookie_helpers.py @@ -6,6 +6,7 @@ from aiohttp import _cookie_helpers as helpers from aiohttp._cookie_helpers import ( + parse_cookie_header, parse_cookie_headers, preserve_morsel_with_coded_value, ) @@ -1029,3 +1030,291 @@ def test_parse_cookie_headers_date_formats_with_attributes() -> None: assert result[1][1]["expires"] == "Wednesday, 09-Jun-30 10:18:14 GMT" assert result[1][1]["domain"] == ".example.com" assert result[1][1]["samesite"] == "Strict" + + +# Tests for parse_cookie_header (RFC 6265 compliant Cookie header parser) + + +def test_parse_cookie_header_simple() -> None: + """Test parse_cookie_header with simple cookies.""" + header = "name=value; session=abc123" + + result = parse_cookie_header(header) + + assert len(result) == 2 + assert result[0][0] == "name" + assert result[0][1].value == "value" + assert result[1][0] == "session" + assert result[1][1].value == "abc123" + + +def test_parse_cookie_header_empty() -> None: + """Test parse_cookie_header with empty header.""" + assert parse_cookie_header("") == [] + assert parse_cookie_header(" ") == [] + + +def test_parse_cookie_header_quoted_values() -> None: + """Test parse_cookie_header handles quoted values correctly.""" + header = 'name="quoted value"; session="with;semicolon"; data="with\\"escaped\\""' + + result = parse_cookie_header(header) + + assert len(result) == 3 + assert result[0][0] == "name" + assert result[0][1].value == "quoted value" + assert result[1][0] == "session" + assert result[1][1].value == "with;semicolon" + assert result[2][0] == "data" + assert result[2][1].value == 'with"escaped"' + + +def test_parse_cookie_header_special_chars() -> None: + """Test parse_cookie_header accepts special characters in names.""" + header = ( + "ISAWPLB{A7F52349-3531-4DA9-8776-F74BC6F4F1BB}=value1; cookie[index]=value2" + ) + + result = parse_cookie_header(header) + + assert len(result) == 2 + assert result[0][0] == "ISAWPLB{A7F52349-3531-4DA9-8776-F74BC6F4F1BB}" + assert result[0][1].value == "value1" + assert result[1][0] == "cookie[index]" + assert result[1][1].value == "value2" + + +def test_parse_cookie_header_invalid_names() -> None: + """Test parse_cookie_header rejects invalid cookie names.""" + # Invalid names with control characters + header = "invalid\tcookie=value; valid=cookie; invalid\ncookie=bad" + + result = parse_cookie_header(header) + + # Parse_cookie_header uses same regex as parse_cookie_headers + # Tab and newline are treated as separators, not part of names + assert len(result) == 5 + assert result[0][0] == "invalid" + assert result[0][1].value == "" + assert result[1][0] == "cookie" + assert result[1][1].value == "value" + assert result[2][0] == "valid" + assert result[2][1].value == "cookie" + assert result[3][0] == "invalid" + assert result[3][1].value == "" + assert result[4][0] == "cookie" + assert result[4][1].value == "bad" + + +def test_parse_cookie_header_no_attributes() -> None: + """Test parse_cookie_header treats all pairs as cookies (no attributes).""" + # In Cookie headers, even reserved attribute names are treated as cookies + header = ( + "session=abc123; path=/test; domain=.example.com; secure=yes; httponly=true" + ) + + result = parse_cookie_header(header) + + assert len(result) == 5 + assert result[0][0] == "session" + assert result[0][1].value == "abc123" + assert result[1][0] == "path" + assert result[1][1].value == "/test" + assert result[2][0] == "domain" + assert result[2][1].value == ".example.com" + assert result[3][0] == "secure" + assert result[3][1].value == "yes" + assert result[4][0] == "httponly" + assert result[4][1].value == "true" + + +def test_parse_cookie_header_empty_value() -> None: + """Test parse_cookie_header with empty cookie values.""" + header = "empty=; name=value; also_empty=" + + result = parse_cookie_header(header) + + assert len(result) == 3 + assert result[0][0] == "empty" + assert result[0][1].value == "" + assert result[1][0] == "name" + assert result[1][1].value == "value" + assert result[2][0] == "also_empty" + assert result[2][1].value == "" + + +def test_parse_cookie_header_spaces() -> None: + """Test parse_cookie_header handles spaces correctly.""" + header = "name1=value1 ; name2=value2 ; name3=value3" + + result = parse_cookie_header(header) + + assert len(result) == 3 + assert result[0][0] == "name1" + assert result[0][1].value == "value1" + assert result[1][0] == "name2" + assert result[1][1].value == "value2" + assert result[2][0] == "name3" + assert result[2][1].value == "value3" + + +def test_parse_cookie_header_encoded_values() -> None: + """Test parse_cookie_header preserves encoded values.""" + header = "encoded=hello%20world; url=https%3A%2F%2Fexample.com" + + result = parse_cookie_header(header) + + assert len(result) == 2 + assert result[0][0] == "encoded" + assert result[0][1].value == "hello%20world" + assert result[1][0] == "url" + assert result[1][1].value == "https%3A%2F%2Fexample.com" + + +def test_parse_cookie_header_malformed() -> None: + """Test parse_cookie_header handles malformed input.""" + # Missing value + header = "name1=value1; justname; name2=value2" + + result = parse_cookie_header(header) + + # Parser accepts cookies without values (empty value) + assert len(result) == 3 + assert result[0][0] == "name1" + assert result[0][1].value == "value1" + assert result[1][0] == "justname" + assert result[1][1].value == "" + assert result[2][0] == "name2" + assert result[2][1].value == "value2" + + # Missing name + header = "=value; name=value2" + result = parse_cookie_header(header) + assert len(result) == 2 + assert result[0][0] == "=value" + assert result[0][1].value == "" + assert result[1][0] == "name" + assert result[1][1].value == "value2" + + +def test_parse_cookie_header_complex_quoted() -> None: + """Test parse_cookie_header with complex quoted values.""" + header = 'session="abc;xyz"; data="value;with;multiple;semicolons"; simple=unquoted' + + result = parse_cookie_header(header) + + assert len(result) == 3 + assert result[0][0] == "session" + assert result[0][1].value == "abc;xyz" + assert result[1][0] == "data" + assert result[1][1].value == "value;with;multiple;semicolons" + assert result[2][0] == "simple" + assert result[2][1].value == "unquoted" + + +def test_parse_cookie_header_unmatched_quotes() -> None: + """Test parse_cookie_header handles unmatched quotes.""" + header = 'cookie1=value1; cookie2="unmatched; cookie3=value3' + + result = parse_cookie_header(header) + + # Should parse all cookies correctly + assert len(result) == 3 + assert result[0][0] == "cookie1" + assert result[0][1].value == "value1" + assert result[1][0] == "cookie2" + assert result[1][1].value == '"unmatched' + assert result[2][0] == "cookie3" + assert result[2][1].value == "value3" + + +def test_parse_cookie_header_vs_parse_cookie_headers() -> None: + """Test difference between parse_cookie_header and parse_cookie_headers.""" + # Cookie header with attribute-like pairs + cookie_header = "session=abc123; path=/test; secure=yes" + + # parse_cookie_header treats all as cookies + cookie_result = parse_cookie_header(cookie_header) + assert len(cookie_result) == 3 + assert cookie_result[0][0] == "session" + assert cookie_result[0][1].value == "abc123" + assert cookie_result[1][0] == "path" + assert cookie_result[1][1].value == "/test" + assert cookie_result[2][0] == "secure" + assert cookie_result[2][1].value == "yes" + + # parse_cookie_headers would treat path and secure as attributes + set_cookie_result = parse_cookie_headers([cookie_header]) + assert len(set_cookie_result) == 1 + assert set_cookie_result[0][0] == "session" + assert set_cookie_result[0][1].value == "abc123" + assert set_cookie_result[0][1]["path"] == "/test" + # secure with any value is treated as boolean True + assert set_cookie_result[0][1]["secure"] is True + + +def test_parse_cookie_header_compatibility_with_simple_cookie() -> None: + """Test parse_cookie_header output works with SimpleCookie.""" + header = "session=abc123; user=john; token=xyz789" + + # Parse with our function + parsed = parse_cookie_header(header) + + # Create SimpleCookie and update with our results + sc = SimpleCookie() + sc.update(parsed) + + # Verify all cookies are present + assert len(sc) == 3 + assert sc["session"].value == "abc123" + assert sc["user"].value == "john" + assert sc["token"].value == "xyz789" + + +def test_parse_cookie_header_real_world_examples() -> None: + """Test parse_cookie_header with real-world Cookie headers.""" + # Google Analytics style + header = "_ga=GA1.2.1234567890.1234567890; _gid=GA1.2.0987654321.0987654321" + result = parse_cookie_header(header) + assert len(result) == 2 + assert result[0][0] == "_ga" + assert result[0][1].value == "GA1.2.1234567890.1234567890" + assert result[1][0] == "_gid" + assert result[1][1].value == "GA1.2.0987654321.0987654321" + + # Session cookies + header = "PHPSESSID=abc123def456; csrf_token=xyz789; logged_in=true" + result = parse_cookie_header(header) + assert len(result) == 3 + assert result[0][0] == "PHPSESSID" + assert result[0][1].value == "abc123def456" + assert result[1][0] == "csrf_token" + assert result[1][1].value == "xyz789" + assert result[2][0] == "logged_in" + assert result[2][1].value == "true" + + # Complex values with proper quoting + header = r'preferences="{\"theme\":\"dark\",\"lang\":\"en\"}"; session_data=eyJhbGciOiJIUzI1NiJ9' + result = parse_cookie_header(header) + assert len(result) == 2 + assert result[0][0] == "preferences" + assert result[0][1].value == '{"theme":"dark","lang":"en"}' + assert result[1][0] == "session_data" + assert result[1][1].value == "eyJhbGciOiJIUzI1NiJ9" + + +def test_parse_cookie_header_issue_7993() -> None: + """Test parse_cookie_header handles issue #7993 correctly.""" + # This specific case from issue #7993 + header = 'foo=bar; baz="qux; foo2=bar2' + + result = parse_cookie_header(header) + + # All cookies should be parsed + assert len(result) == 3 + assert result[0][0] == "foo" + assert result[0][1].value == "bar" + assert result[1][0] == "baz" + assert result[1][1].value == '"qux' + assert result[2][0] == "foo2" + assert result[2][1].value == "bar2" diff --git a/tests/test_web_request.py b/tests/test_web_request.py index bac910ac0af..51d6e1b108b 100644 --- a/tests/test_web_request.py +++ b/tests/test_web_request.py @@ -407,27 +407,35 @@ def test_request_cookies_quoted_values() -> None: def test_request_cookies_with_attributes() -> None: - """Test that cookie attributes don't affect value parsing. + """Test that cookie attributes are parsed as cookies per RFC 6265. - Related to issue #5397 - ensures that the presence of domain or other - attributes doesn't change how cookie values are parsed. + Per RFC 6265 Section 5.4, Cookie headers contain only name-value pairs. + Names that match attribute names (Domain, Path, etc.) should be treated + as regular cookies, not as attributes. """ - # Cookie with domain attribute - quotes should still be removed + # Cookie with domain - both should be parsed as cookies headers = CIMultiDict(COOKIE='sess="quoted_value"; Domain=.example.com') req = make_mocked_request("GET", "/", headers=headers) - assert req.cookies == {"sess": "quoted_value"} + assert req.cookies == {"sess": "quoted_value", "Domain": ".example.com"} - # Cookie with multiple attributes + # Cookie with multiple attribute names - all parsed as cookies headers = CIMultiDict(COOKIE='token="abc123"; Path=/; Secure; HttpOnly') req = make_mocked_request("GET", "/", headers=headers) - assert req.cookies == {"token": "abc123"} + assert req.cookies == {"token": "abc123", "Path": "/", "Secure": "", "HttpOnly": ""} - # Multiple cookies with different attributes + # Multiple cookies with attribute names mixed in headers = CIMultiDict( COOKIE='c1="v1"; Domain=.example.com; c2="v2"; Path=/api; c3=v3; Secure' ) req = make_mocked_request("GET", "/", headers=headers) - assert req.cookies == {"c1": "v1", "c2": "v2", "c3": "v3"} + assert req.cookies == { + "c1": "v1", + "Domain": ".example.com", + "c2": "v2", + "Path": "/api", + "c3": "v3", + "Secure": "", + } def test_match_info() -> None: From 1de7cf7c3024742142b88e048baa821e79fd9f62 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Jun 2025 21:42:03 -0500 Subject: [PATCH 2/7] rename --- aiohttp/_cookie_helpers.py | 8 +- aiohttp/abc.py | 4 +- aiohttp/client_reqrep.py | 6 +- tests/test_cookie_helpers.py | 240 ++++++++++++++++++----------------- 4 files changed, 130 insertions(+), 128 deletions(-) diff --git a/aiohttp/_cookie_helpers.py b/aiohttp/_cookie_helpers.py index 06f8f7444ff..37b1cdeea36 100644 --- a/aiohttp/_cookie_helpers.py +++ b/aiohttp/_cookie_helpers.py @@ -13,7 +13,7 @@ from .log import internal_logger __all__ = ( - "parse_cookie_headers", + "parse_set_cookie_headers", "parse_cookie_header", "preserve_morsel_with_coded_value", ) @@ -136,7 +136,7 @@ def parse_cookie_header(header: str) -> List[Tuple[str, Morsel[str]]]: There are no attributes in Cookie headers - even names that match attribute names (like 'path' or 'secure') should be treated as cookies. - This parser uses the same regex-based approach as parse_cookie_headers + This parser uses the same regex-based approach as parse_set_cookie_headers to properly handle quoted values that may contain semicolons. Args: @@ -153,7 +153,7 @@ def parse_cookie_header(header: str) -> List[Tuple[str, Morsel[str]]]: n = len(header) while i < n: - # Use the same pattern as parse_cookie_headers to find cookies + # Use the same pattern as parse_set_cookie_headers to find cookies match = _COOKIE_PATTERN.match(header, i) if not match: break @@ -183,7 +183,7 @@ def parse_cookie_header(header: str) -> List[Tuple[str, Morsel[str]]]: return cookies -def parse_cookie_headers(headers: Sequence[str]) -> List[Tuple[str, Morsel[str]]]: +def parse_set_cookie_headers(headers: Sequence[str]) -> List[Tuple[str, Morsel[str]]]: """ Parse cookie headers using a vendored version of SimpleCookie parsing. diff --git a/aiohttp/abc.py b/aiohttp/abc.py index 0f396414a8e..f8a8442a7b4 100644 --- a/aiohttp/abc.py +++ b/aiohttp/abc.py @@ -22,7 +22,7 @@ from multidict import CIMultiDict from yarl import URL -from ._cookie_helpers import parse_cookie_headers +from ._cookie_helpers import parse_set_cookie_headers from .typedefs import LooseCookies if TYPE_CHECKING: @@ -194,7 +194,7 @@ def update_cookies_from_headers( self, headers: Sequence[str], response_url: URL ) -> None: """Update cookies from raw Set-Cookie headers.""" - if headers and (cookies_to_update := parse_cookie_headers(headers)): + if headers and (cookies_to_update := parse_set_cookie_headers(headers)): self.update_cookies(cookies_to_update, response_url) @abstractmethod diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index eb7b6aa88ec..aa5b220fe48 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -32,7 +32,7 @@ from . import hdrs, helpers, http, multipart, payload from ._cookie_helpers import ( parse_cookie_header, - parse_cookie_headers, + parse_set_cookie_headers, preserve_morsel_with_coded_value, ) from .abc import AbstractStreamWriter @@ -317,9 +317,9 @@ def cookies(self) -> SimpleCookie: if self._raw_cookie_headers is not None: # Parse cookies for response.cookies (SimpleCookie for backward compatibility) cookies = SimpleCookie() - # Use parse_cookie_headers for more lenient parsing that handles + # Use parse_set_cookie_headers for more lenient parsing that handles # malformed cookies better than SimpleCookie.load - cookies.update(parse_cookie_headers(self._raw_cookie_headers)) + cookies.update(parse_set_cookie_headers(self._raw_cookie_headers)) self._cookies = cookies else: self._cookies = SimpleCookie() diff --git a/tests/test_cookie_helpers.py b/tests/test_cookie_helpers.py index b0693366c96..337037290d2 100644 --- a/tests/test_cookie_helpers.py +++ b/tests/test_cookie_helpers.py @@ -7,7 +7,7 @@ from aiohttp import _cookie_helpers as helpers from aiohttp._cookie_helpers import ( parse_cookie_header, - parse_cookie_headers, + parse_set_cookie_headers, preserve_morsel_with_coded_value, ) @@ -64,11 +64,11 @@ def test_preserve_morsel_with_coded_value_no_coded_value() -> None: assert result.coded_value == "simple_value" -def test_parse_cookie_headers_simple() -> None: - """Test parse_cookie_headers with simple cookies.""" +def test_parse_set_cookie_headers_simple() -> None: + """Test parse_set_cookie_headers with simple cookies.""" headers = ["name=value", "session=abc123"] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 2 assert result[0][0] == "name" @@ -79,14 +79,14 @@ def test_parse_cookie_headers_simple() -> None: assert result[1][1].value == "abc123" -def test_parse_cookie_headers_with_attributes() -> None: - """Test parse_cookie_headers with cookie attributes.""" +def test_parse_set_cookie_headers_with_attributes() -> None: + """Test parse_set_cookie_headers with cookie attributes.""" headers = [ "sessionid=value123; Path=/; HttpOnly; Secure", "user=john; Domain=.example.com; Max-Age=3600", ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 2 @@ -106,8 +106,8 @@ def test_parse_cookie_headers_with_attributes() -> None: assert morsel2["max-age"] == "3600" -def test_parse_cookie_headers_special_chars_in_names() -> None: - """Test parse_cookie_headers accepts special characters in names (#2683).""" +def test_parse_set_cookie_headers_special_chars_in_names() -> None: + """Test parse_set_cookie_headers accepts special characters in names (#2683).""" # These should be accepted with relaxed validation headers = [ "ISAWPLB{A7F52349-3531-4DA9-8776-F74BC6F4F1BB}=value1", @@ -117,7 +117,7 @@ def test_parse_cookie_headers_special_chars_in_names() -> None: "cookie@domain=value5", ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 5 expected_names = [ @@ -134,8 +134,8 @@ def test_parse_cookie_headers_special_chars_in_names() -> None: assert morsel.value == f"value{i+1}" -def test_parse_cookie_headers_invalid_names() -> None: - """Test parse_cookie_headers rejects truly invalid cookie names.""" +def test_parse_set_cookie_headers_invalid_names() -> None: + """Test parse_set_cookie_headers rejects truly invalid cookie names.""" # These should be rejected even with relaxed validation headers = [ "invalid\tcookie=value", # Tab character @@ -145,14 +145,14 @@ def test_parse_cookie_headers_invalid_names() -> None: "name with spaces=value", # Spaces in name ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) # All should be skipped assert len(result) == 0 -def test_parse_cookie_headers_empty_and_invalid() -> None: - """Test parse_cookie_headers handles empty and invalid formats.""" +def test_parse_set_cookie_headers_empty_and_invalid() -> None: + """Test parse_set_cookie_headers handles empty and invalid formats.""" headers = [ "", # Empty header " ", # Whitespace only @@ -163,7 +163,7 @@ def test_parse_cookie_headers_empty_and_invalid() -> None: "Domain=.com", # Reserved attribute as name (should be skipped) ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) # Only "name=" should be accepted assert len(result) == 1 @@ -171,15 +171,15 @@ def test_parse_cookie_headers_empty_and_invalid() -> None: assert result[0][1].value == "" -def test_parse_cookie_headers_quoted_values() -> None: - """Test parse_cookie_headers handles quoted values correctly.""" +def test_parse_set_cookie_headers_quoted_values() -> None: + """Test parse_set_cookie_headers handles quoted values correctly.""" headers = [ 'name="quoted value"', 'session="with;semicolon"', 'data="with\\"escaped\\""', ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 3 assert result[0][1].value == "quoted value" @@ -195,7 +195,7 @@ def test_parse_cookie_headers_quoted_values() -> None: 'complex="a=b;c=d"; simple=value', ], ) -def test_parse_cookie_headers_semicolon_in_quoted_values(header: str) -> None: +def test_parse_set_cookie_headers_semicolon_in_quoted_values(header: str) -> None: """ Test that semicolons inside properly quoted values are handled correctly. @@ -207,7 +207,7 @@ def test_parse_cookie_headers_semicolon_in_quoted_values(header: str) -> None: sc.load(header) # Test with our parser - result = parse_cookie_headers([header]) + result = parse_set_cookie_headers([header]) # Should parse the same number of cookies assert len(result) == len(sc) @@ -218,12 +218,12 @@ def test_parse_cookie_headers_semicolon_in_quoted_values(header: str) -> None: assert morsel.value == sc_morsel.value -def test_parse_cookie_headers_multiple_cookies_same_header() -> None: - """Test parse_cookie_headers with multiple cookies in one header.""" +def test_parse_set_cookie_headers_multiple_cookies_same_header() -> None: + """Test parse_set_cookie_headers with multiple cookies in one header.""" # Note: SimpleCookie includes the comma as part of the first cookie's value headers = ["cookie1=value1, cookie2=value2"] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) # Should parse as two separate cookies assert len(result) == 2 @@ -248,14 +248,14 @@ def test_parse_cookie_headers_multiple_cookies_same_header() -> None: "complex=value; Domain=.example.com; Path=/app; Max-Age=3600", ], ) -def test_parse_cookie_headers_compatibility_with_simple_cookie(header: str) -> None: - """Test parse_cookie_headers is bug-for-bug compatible with SimpleCookie.load.""" +def test_parse_set_cookie_headers_compatibility_with_simple_cookie(header: str) -> None: + """Test parse_set_cookie_headers is bug-for-bug compatible with SimpleCookie.load.""" # Parse with SimpleCookie sc = SimpleCookie() sc.load(header) # Parse with our function - result = parse_cookie_headers([header]) + result = parse_set_cookie_headers([header]) # Should have same number of cookies assert len(result) == len(sc) @@ -281,8 +281,8 @@ def test_parse_cookie_headers_compatibility_with_simple_cookie(header: str) -> N assert morsel.get(bool_attr) is True -def test_parse_cookie_headers_relaxed_validation_differences() -> None: - """Test where parse_cookie_headers differs from SimpleCookie (relaxed validation).""" +def test_parse_set_cookie_headers_relaxed_validation_differences() -> None: + """Test where parse_set_cookie_headers differs from SimpleCookie (relaxed validation).""" # Test cookies that SimpleCookie rejects with CookieError rejected_by_simplecookie = [ ("cookie{with}braces=value1", "cookie{with}braces", "value1"), @@ -297,7 +297,7 @@ def test_parse_cookie_headers_relaxed_validation_differences() -> None: sc.load(header) # Our parser should accept them - result = parse_cookie_headers([header]) + result = parse_set_cookie_headers([header]) assert len(result) == 1 # We accept assert result[0][0] == expected_name assert result[0][1].value == expected_value @@ -315,20 +315,20 @@ def test_parse_cookie_headers_relaxed_validation_differences() -> None: # May or may not parse correctly in SimpleCookie # Our parser should accept them consistently - result = parse_cookie_headers([header]) + result = parse_set_cookie_headers([header]) assert len(result) == 1 assert result[0][0] == expected_name assert result[0][1].value == expected_value -def test_parse_cookie_headers_case_insensitive_attrs() -> None: +def test_parse_set_cookie_headers_case_insensitive_attrs() -> None: """Test that known attributes are handled case-insensitively.""" headers = [ "cookie1=value1; PATH=/test; DOMAIN=example.com", "cookie2=value2; Secure; HTTPONLY; max-AGE=60", ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 2 @@ -342,13 +342,13 @@ def test_parse_cookie_headers_case_insensitive_attrs() -> None: assert result[1][1]["max-age"] == "60" -def test_parse_cookie_headers_unknown_attrs_ignored() -> None: +def test_parse_set_cookie_headers_unknown_attrs_ignored() -> None: """Test that unknown attributes are treated as new cookies (same as SimpleCookie).""" headers = [ "cookie=value; Path=/; unknownattr=ignored; HttpOnly", ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) # SimpleCookie treats unknown attributes with values as new cookies assert len(result) == 2 @@ -364,8 +364,8 @@ def test_parse_cookie_headers_unknown_attrs_ignored() -> None: assert result[1][1]["httponly"] is True # HttpOnly applies to this cookie -def test_parse_cookie_headers_complex_real_world() -> None: - """Test parse_cookie_headers with complex real-world examples.""" +def test_parse_set_cookie_headers_complex_real_world() -> None: + """Test parse_set_cookie_headers with complex real-world examples.""" headers = [ # AWS ELB cookie "AWSELB=ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890; Path=/", @@ -375,7 +375,7 @@ def test_parse_cookie_headers_complex_real_world() -> None: "session_id=s%3AabcXYZ123.signature123; Path=/; Secure; HttpOnly; SameSite=Strict", ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 3 @@ -391,7 +391,7 @@ def test_parse_cookie_headers_complex_real_world() -> None: assert session_morsel.get("samesite") == "Strict" -def test_parse_cookie_headers_boolean_attrs() -> None: +def test_parse_set_cookie_headers_boolean_attrs() -> None: """Test that boolean attributes (secure, httponly) work correctly.""" # Test secure attribute variations headers = [ @@ -400,7 +400,7 @@ def test_parse_cookie_headers_boolean_attrs() -> None: "cookie3=value3; Secure=true", # Non-standard but might occur ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 3 # All should have secure=True @@ -413,7 +413,7 @@ def test_parse_cookie_headers_boolean_attrs() -> None: "cookie5=value5; HttpOnly=", ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 2 # All should have httponly=True @@ -421,7 +421,7 @@ def test_parse_cookie_headers_boolean_attrs() -> None: assert morsel.get("httponly") is True, f"{name} should have httponly=True" -def test_parse_cookie_headers_boolean_attrs_with_partitioned() -> None: +def test_parse_set_cookie_headers_boolean_attrs_with_partitioned() -> None: """Test that boolean attributes including partitioned work correctly.""" # Test secure attribute variations secure_headers = [ @@ -430,7 +430,7 @@ def test_parse_cookie_headers_boolean_attrs_with_partitioned() -> None: "cookie3=value3; Secure=true", # Non-standard but might occur ] - result = parse_cookie_headers(secure_headers) + result = parse_set_cookie_headers(secure_headers) assert len(result) == 3 for name, morsel in result: assert morsel.get("secure") is True, f"{name} should have secure=True" @@ -441,7 +441,7 @@ def test_parse_cookie_headers_boolean_attrs_with_partitioned() -> None: "cookie5=value5; HttpOnly=", ] - result = parse_cookie_headers(httponly_headers) + result = parse_set_cookie_headers(httponly_headers) assert len(result) == 2 for name, morsel in result: assert morsel.get("httponly") is True, f"{name} should have httponly=True" @@ -453,21 +453,21 @@ def test_parse_cookie_headers_boolean_attrs_with_partitioned() -> None: "cookie8=value8; Partitioned=yes", # Non-standard but might occur ] - result = parse_cookie_headers(partitioned_headers) + result = parse_set_cookie_headers(partitioned_headers) assert len(result) == 3 for name, morsel in result: assert morsel.get("partitioned") is True, f"{name} should have partitioned=True" -def test_parse_cookie_headers_encoded_values() -> None: - """Test that parse_cookie_headers preserves encoded values.""" +def test_parse_set_cookie_headers_encoded_values() -> None: + """Test that parse_set_cookie_headers preserves encoded values.""" headers = [ "encoded=hello%20world", "url=https%3A%2F%2Fexample.com%2Fpath", "special=%21%40%23%24%25%5E%26*%28%29", ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 3 # Values should be preserved as-is (not decoded) @@ -476,9 +476,9 @@ def test_parse_cookie_headers_encoded_values() -> None: assert result[2][1].value == "%21%40%23%24%25%5E%26*%28%29" -def test_parse_cookie_headers_partitioned() -> None: +def test_parse_set_cookie_headers_partitioned() -> None: """ - Test that parse_cookie_headers handles partitioned attribute correctly. + Test that parse_set_cookie_headers handles partitioned attribute correctly. This tests the fix for issue #10380 - partitioned cookies support. The partitioned attribute is a boolean flag like secure and httponly. @@ -491,7 +491,7 @@ def test_parse_cookie_headers_partitioned() -> None: "cookie5=value5; Domain=.example.com; Path=/; Partitioned", ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 5 @@ -512,7 +512,7 @@ def test_parse_cookie_headers_partitioned() -> None: assert result[4][1].get("path") == "/" -def test_parse_cookie_headers_partitioned_case_insensitive() -> None: +def test_parse_set_cookie_headers_partitioned_case_insensitive() -> None: """Test that partitioned attribute is recognized case-insensitively.""" headers = [ "cookie1=value1; partitioned", # lowercase @@ -521,7 +521,7 @@ def test_parse_cookie_headers_partitioned_case_insensitive() -> None: "cookie4=value4; PaRtItIoNeD", # mixed case ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 4 @@ -532,14 +532,14 @@ def test_parse_cookie_headers_partitioned_case_insensitive() -> None: ), f"Cookie {i+1} should have partitioned=True" -def test_parse_cookie_headers_partitioned_not_set() -> None: +def test_parse_set_cookie_headers_partitioned_not_set() -> None: """Test that cookies without partitioned attribute don't have it set.""" headers = [ "normal=value; Secure; HttpOnly", "regular=cookie; Path=/", ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 2 @@ -549,7 +549,7 @@ def test_parse_cookie_headers_partitioned_not_set() -> None: # Tests that don't require partitioned support in SimpleCookie -def test_parse_cookie_headers_partitioned_with_other_attrs_manual() -> None: +def test_parse_set_cookie_headers_partitioned_with_other_attrs_manual() -> None: """ Test parsing logic for partitioned cookies combined with all other attributes. @@ -562,7 +562,7 @@ def test_parse_cookie_headers_partitioned_with_other_attrs_manual() -> None: # Test a simple case that won't trigger SimpleCookie errors headers = ["session=abc123; Secure; HttpOnly"] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 1 assert result[0][0] == "session" @@ -596,7 +596,7 @@ def test_cookie_pattern_matches_partitioned_attribute(test_string: str) -> None: assert match.group("key").lower() == "partitioned" -def test_parse_cookie_headers_issue_7993_double_quotes() -> None: +def test_parse_set_cookie_headers_issue_7993_double_quotes() -> None: """ Test that cookies with unmatched opening quotes don't break parsing of subsequent cookies. @@ -609,7 +609,7 @@ def test_parse_cookie_headers_issue_7993_double_quotes() -> None: # Test case from the issue headers = ['foo=bar; baz="qux; foo2=bar2'] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) # Should parse all cookies correctly assert len(result) == 3 @@ -621,41 +621,41 @@ def test_parse_cookie_headers_issue_7993_double_quotes() -> None: assert result[2][1].value == "bar2" -def test_parse_cookie_headers_empty_headers() -> None: +def test_parse_set_cookie_headers_empty_headers() -> None: """Test handling of empty headers in the sequence.""" # Empty header should be skipped - result = parse_cookie_headers(["", "name=value"]) + result = parse_set_cookie_headers(["", "name=value"]) assert len(result) == 1 assert result[0][0] == "name" assert result[0][1].value == "value" # Multiple empty headers - result = parse_cookie_headers(["", "", ""]) + result = parse_set_cookie_headers(["", "", ""]) assert result == [] # Empty headers mixed with valid cookies - result = parse_cookie_headers(["", "a=1", "", "b=2", ""]) + result = parse_set_cookie_headers(["", "a=1", "", "b=2", ""]) assert len(result) == 2 assert result[0][0] == "a" assert result[1][0] == "b" -def test_parse_cookie_headers_invalid_cookie_syntax() -> None: +def test_parse_set_cookie_headers_invalid_cookie_syntax() -> None: """Test handling of invalid cookie syntax.""" # No valid cookie pattern - result = parse_cookie_headers(["@#$%^&*()"]) + result = parse_set_cookie_headers(["@#$%^&*()"]) assert result == [] # Cookie name without value - result = parse_cookie_headers(["name"]) + result = parse_set_cookie_headers(["name"]) assert result == [] # Multiple invalid patterns - result = parse_cookie_headers(["!!!!", "????", "name", "@@@"]) + result = parse_set_cookie_headers(["!!!!", "????", "name", "@@@"]) assert result == [] -def test_parse_cookie_headers_illegal_cookie_names( +def test_parse_set_cookie_headers_illegal_cookie_names( caplog: pytest.LogCaptureFixture, ) -> None: """ @@ -666,103 +666,105 @@ def test_parse_cookie_headers_illegal_cookie_names( logged when illegal names appear after a valid cookie. """ # Cookie name that is a known attribute (illegal) - parsing stops early - result = parse_cookie_headers(["path=value; domain=test"]) + result = parse_set_cookie_headers(["path=value; domain=test"]) assert result == [] # Cookie name that doesn't match the pattern - result = parse_cookie_headers(["=value"]) + result = parse_set_cookie_headers(["=value"]) assert result == [] # Valid cookie after illegal one - parsing stops at illegal - result = parse_cookie_headers(["domain=bad; good=value"]) + result = parse_set_cookie_headers(["domain=bad; good=value"]) assert result == [] # Illegal cookie name that appears after a valid cookie triggers warning - result = parse_cookie_headers(["good=value; Path=/; invalid,cookie=value;"]) + result = parse_set_cookie_headers(["good=value; Path=/; invalid,cookie=value;"]) assert len(result) == 1 assert result[0][0] == "good" assert "Illegal cookie name 'invalid,cookie'" in caplog.text -def test_parse_cookie_headers_attributes_before_cookie() -> None: +def test_parse_set_cookie_headers_attributes_before_cookie() -> None: """Test that attributes before any cookie are invalid.""" # Path attribute before cookie - result = parse_cookie_headers(["Path=/; name=value"]) + result = parse_set_cookie_headers(["Path=/; name=value"]) assert result == [] # Domain attribute before cookie - result = parse_cookie_headers(["Domain=.example.com; name=value"]) + result = parse_set_cookie_headers(["Domain=.example.com; name=value"]) assert result == [] # Multiple attributes before cookie - result = parse_cookie_headers(["Path=/; Domain=.example.com; Secure; name=value"]) + result = parse_set_cookie_headers( + ["Path=/; Domain=.example.com; Secure; name=value"] + ) assert result == [] -def test_parse_cookie_headers_attributes_without_values() -> None: +def test_parse_set_cookie_headers_attributes_without_values() -> None: """Test handling of attributes with missing values.""" # Boolean attribute without value (valid) - result = parse_cookie_headers(["name=value; Secure"]) + result = parse_set_cookie_headers(["name=value; Secure"]) assert len(result) == 1 assert result[0][1]["secure"] is True # Non-boolean attribute without value (invalid, stops parsing) - result = parse_cookie_headers(["name=value; Path"]) + result = parse_set_cookie_headers(["name=value; Path"]) assert len(result) == 1 # Path without value stops further attribute parsing # Multiple cookies, invalid attribute in middle - result = parse_cookie_headers(["name=value; Path; Secure"]) + result = parse_set_cookie_headers(["name=value; Path; Secure"]) assert len(result) == 1 # Secure is not parsed because Path without value stops parsing -def test_parse_cookie_headers_dollar_prefixed_names() -> None: +def test_parse_set_cookie_headers_dollar_prefixed_names() -> None: """Test handling of cookie names starting with $.""" # $Version without preceding cookie (ignored) - result = parse_cookie_headers(["$Version=1; name=value"]) + result = parse_set_cookie_headers(["$Version=1; name=value"]) assert len(result) == 1 assert result[0][0] == "name" # Multiple $ prefixed without cookie (all ignored) - result = parse_cookie_headers(["$Version=1; $Path=/; $Domain=.com; name=value"]) + result = parse_set_cookie_headers(["$Version=1; $Path=/; $Domain=.com; name=value"]) assert len(result) == 1 assert result[0][0] == "name" # $ prefix at start is ignored, cookie follows - result = parse_cookie_headers(["$Unknown=123; valid=cookie"]) + result = parse_set_cookie_headers(["$Unknown=123; valid=cookie"]) assert len(result) == 1 assert result[0][0] == "valid" -def test_parse_cookie_headers_dollar_attributes() -> None: +def test_parse_set_cookie_headers_dollar_attributes() -> None: """Test handling of $ prefixed attributes after cookies.""" # Test multiple $ attributes with cookie (case-insensitive like SimpleCookie) - result = parse_cookie_headers(["name=value; $Path=/test; $Domain=.example.com"]) + result = parse_set_cookie_headers(["name=value; $Path=/test; $Domain=.example.com"]) assert len(result) == 1 assert result[0][0] == "name" assert result[0][1]["path"] == "/test" assert result[0][1]["domain"] == ".example.com" # Test unknown $ attribute (should be ignored) - result = parse_cookie_headers(["name=value; $Unknown=test"]) + result = parse_set_cookie_headers(["name=value; $Unknown=test"]) assert len(result) == 1 assert result[0][0] == "name" # $Unknown should not be set # Test $ attribute with empty value - result = parse_cookie_headers(["name=value; $Path="]) + result = parse_set_cookie_headers(["name=value; $Path="]) assert len(result) == 1 assert result[0][1]["path"] == "" # Test case sensitivity compatibility with SimpleCookie - result = parse_cookie_headers(["test=value; $path=/lower; $PATH=/upper"]) + result = parse_set_cookie_headers(["test=value; $path=/lower; $PATH=/upper"]) assert len(result) == 1 # Last one wins, and it's case-insensitive assert result[0][1]["path"] == "/upper" -def test_parse_cookie_headers_attributes_after_illegal_cookie() -> None: +def test_parse_set_cookie_headers_attributes_after_illegal_cookie() -> None: """ Test that attributes after an illegal cookie name are handled correctly. @@ -770,25 +772,25 @@ def test_parse_cookie_headers_attributes_after_illegal_cookie() -> None: cookie name was encountered. """ # Illegal cookie followed by $ attribute - result = parse_cookie_headers(["good=value; invalid,cookie=bad; $Path=/test"]) + result = parse_set_cookie_headers(["good=value; invalid,cookie=bad; $Path=/test"]) assert len(result) == 1 assert result[0][0] == "good" # $Path should be ignored since current_morsel is None after illegal cookie # Illegal cookie followed by boolean attribute - result = parse_cookie_headers(["good=value; invalid,cookie=bad; HttpOnly"]) + result = parse_set_cookie_headers(["good=value; invalid,cookie=bad; HttpOnly"]) assert len(result) == 1 assert result[0][0] == "good" # HttpOnly should be ignored since current_morsel is None # Illegal cookie followed by regular attribute with value - result = parse_cookie_headers(["good=value; invalid,cookie=bad; Max-Age=3600"]) + result = parse_set_cookie_headers(["good=value; invalid,cookie=bad; Max-Age=3600"]) assert len(result) == 1 assert result[0][0] == "good" # Max-Age should be ignored since current_morsel is None # Multiple attributes after illegal cookie - result = parse_cookie_headers( + result = parse_set_cookie_headers( ["good=value; invalid,cookie=bad; $Path=/; HttpOnly; Max-Age=60; Domain=.com"] ) assert len(result) == 1 @@ -796,7 +798,7 @@ def test_parse_cookie_headers_attributes_after_illegal_cookie() -> None: # All attributes should be ignored after illegal cookie -def test_parse_cookie_headers_unmatched_quotes_compatibility() -> None: +def test_parse_set_cookie_headers_unmatched_quotes_compatibility() -> None: """ Test that most unmatched quote scenarios behave like SimpleCookie. @@ -818,7 +820,7 @@ def test_parse_cookie_headers_unmatched_quotes_compatibility() -> None: sc_cookies = list(sc.items()) # Test our parser behavior - result = parse_cookie_headers([header]) + result = parse_set_cookie_headers([header]) # Both should parse the same cookies (partial parsing) assert len(result) == len(sc_cookies), ( @@ -836,7 +838,7 @@ def test_parse_cookie_headers_unmatched_quotes_compatibility() -> None: assert len(sc) == 1 # Only cookie1 # Our parser handles it better - result = parse_cookie_headers([fixed_case]) + result = parse_set_cookie_headers([fixed_case]) assert len(result) == 3 # All three cookies assert result[0][0] == "cookie1" assert result[0][1].value == "value1" @@ -846,15 +848,15 @@ def test_parse_cookie_headers_unmatched_quotes_compatibility() -> None: assert result[2][1].value == "value3" -def test_parse_cookie_headers_expires_attribute() -> None: - """Test parse_cookie_headers handles expires attribute with date formats.""" +def test_parse_set_cookie_headers_expires_attribute() -> None: + """Test parse_set_cookie_headers handles expires attribute with date formats.""" headers = [ "session=abc; Expires=Wed, 09 Jun 2021 10:18:14 GMT", "user=xyz; expires=Wednesday, 09-Jun-21 10:18:14 GMT", "token=123; EXPIRES=Wed, 09 Jun 2021 10:18:14 GMT", ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 3 for _, morsel in result: @@ -862,18 +864,18 @@ def test_parse_cookie_headers_expires_attribute() -> None: assert "GMT" in morsel["expires"] -def test_parse_cookie_headers_edge_cases() -> None: +def test_parse_set_cookie_headers_edge_cases() -> None: """Test various edge cases.""" # Very long cookie values long_value = "x" * 4096 - result = parse_cookie_headers([f"name={long_value}"]) + result = parse_set_cookie_headers([f"name={long_value}"]) assert len(result) == 1 assert result[0][1].value == long_value -def test_parse_cookie_headers_various_date_formats_issue_4327() -> None: +def test_parse_set_cookie_headers_various_date_formats_issue_4327() -> None: """ - Test that parse_cookie_headers handles various date formats per RFC 6265. + Test that parse_set_cookie_headers handles various date formats per RFC 6265. This tests the fix for issue #4327 - support for RFC 822, RFC 850, and ANSI C asctime() date formats in cookie expiration. @@ -894,7 +896,7 @@ def test_parse_cookie_headers_various_date_formats_issue_4327() -> None: "cookie7=value7; Expires=Tue, 01-Jan-30 00:00:00 GMT", ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) # All cookies should be parsed assert len(result) == 7 @@ -918,7 +920,7 @@ def test_parse_cookie_headers_various_date_formats_issue_4327() -> None: assert morsel.get("expires") == exp_expires -def test_parse_cookie_headers_ansi_c_asctime_format() -> None: +def test_parse_set_cookie_headers_ansi_c_asctime_format() -> None: """ Test parsing of ANSI C asctime() format. @@ -927,7 +929,7 @@ def test_parse_cookie_headers_ansi_c_asctime_format() -> None: """ headers = ["cookie1=value1; Expires=Wed Jun 9 10:18:14 2021"] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) # Should parse correctly with the expires attribute preserved assert len(result) == 1 @@ -936,9 +938,9 @@ def test_parse_cookie_headers_ansi_c_asctime_format() -> None: assert result[0][1]["expires"] == "Wed Jun 9 10:18:14 2021" -def test_parse_cookie_headers_rfc2822_timezone_issue_4493() -> None: +def test_parse_set_cookie_headers_rfc2822_timezone_issue_4493() -> None: """ - Test that parse_cookie_headers handles RFC 2822 timezone formats. + Test that parse_set_cookie_headers handles RFC 2822 timezone formats. This tests the fix for issue #4493 - support for RFC 2822-compliant dates with timezone offsets like -0000, +0100, etc. @@ -955,7 +957,7 @@ def test_parse_cookie_headers_rfc2822_timezone_issue_4493() -> None: "classic=cookie; expires=Sat, 03 Apr 2026 12:00:00 GMT", ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) # All cookies should be parsed assert len(result) == 4 @@ -978,14 +980,14 @@ def test_parse_cookie_headers_rfc2822_timezone_issue_4493() -> None: assert result[3][1]["expires"] == "Sat, 03 Apr 2026 12:00:00 GMT" -def test_parse_cookie_headers_rfc2822_with_attributes() -> None: +def test_parse_set_cookie_headers_rfc2822_with_attributes() -> None: """Test that RFC 2822 dates work correctly with other cookie attributes.""" headers = [ "session=abc123; expires=Wed, 15 Jan 2020 09:45:07 -0000; Path=/; HttpOnly; Secure", "token=xyz789; expires=Thu, 01 Feb 2024 14:30:00 +0100; Domain=.example.com; SameSite=Strict", ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 2 @@ -1005,14 +1007,14 @@ def test_parse_cookie_headers_rfc2822_with_attributes() -> None: assert result[1][1]["samesite"] == "Strict" -def test_parse_cookie_headers_date_formats_with_attributes() -> None: +def test_parse_set_cookie_headers_date_formats_with_attributes() -> None: """Test that date formats work correctly with other cookie attributes.""" headers = [ "session=abc123; Expires=Wed, 09 Jun 2030 10:18:14 GMT; Path=/; HttpOnly; Secure", "token=xyz789; Expires=Wednesday, 09-Jun-30 10:18:14 GMT; Domain=.example.com; SameSite=Strict", ] - result = parse_cookie_headers(headers) + result = parse_set_cookie_headers(headers) assert len(result) == 2 @@ -1091,7 +1093,7 @@ def test_parse_cookie_header_invalid_names() -> None: result = parse_cookie_header(header) - # Parse_cookie_header uses same regex as parse_cookie_headers + # Parse_cookie_header uses same regex as parse_set_cookie_headers # Tab and newline are treated as separators, not part of names assert len(result) == 5 assert result[0][0] == "invalid" @@ -1228,8 +1230,8 @@ def test_parse_cookie_header_unmatched_quotes() -> None: assert result[2][1].value == "value3" -def test_parse_cookie_header_vs_parse_cookie_headers() -> None: - """Test difference between parse_cookie_header and parse_cookie_headers.""" +def test_parse_cookie_header_vs_parse_set_cookie_headers() -> None: + """Test difference between parse_cookie_header and parse_set_cookie_headers.""" # Cookie header with attribute-like pairs cookie_header = "session=abc123; path=/test; secure=yes" @@ -1243,8 +1245,8 @@ def test_parse_cookie_header_vs_parse_cookie_headers() -> None: assert cookie_result[2][0] == "secure" assert cookie_result[2][1].value == "yes" - # parse_cookie_headers would treat path and secure as attributes - set_cookie_result = parse_cookie_headers([cookie_header]) + # parse_set_cookie_headers would treat path and secure as attributes + set_cookie_result = parse_set_cookie_headers([cookie_header]) assert len(set_cookie_result) == 1 assert set_cookie_result[0][0] == "session" assert set_cookie_result[0][1].value == "abc123" From f684b44d4425be82b40d6ccaddadfae2c15c602a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Jun 2025 02:51:36 +0000 Subject: [PATCH 3/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_cookie_helpers.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_cookie_helpers.py b/tests/test_cookie_helpers.py index 63895672761..0523228fea3 100644 --- a/tests/test_cookie_helpers.py +++ b/tests/test_cookie_helpers.py @@ -11,9 +11,9 @@ from aiohttp import _cookie_helpers as helpers from aiohttp._cookie_helpers import ( + _unquote, parse_cookie_header, parse_set_cookie_headers, - _unquote, preserve_morsel_with_coded_value, ) @@ -1039,7 +1039,6 @@ def test_parse_set_cookie_headers_date_formats_with_attributes() -> None: assert result[1][1]["domain"] == ".example.com" assert result[1][1]["samesite"] == "Strict" - @pytest.mark.parametrize( ("header", "expected_name", "expected_value", "expected_coded"), @@ -1081,10 +1080,11 @@ def test_parse_cookie_headers_uses_unquote_with_octal( # Check that coded_value preserves the original quoted string assert morsel.coded_value == expected_coded - + # Tests for parse_cookie_header (RFC 6265 compliant Cookie header parser) + def test_parse_cookie_header_simple() -> None: """Test parse_cookie_header with simple cookies.""" header = "name=value; session=abc123" @@ -1368,8 +1368,8 @@ def test_parse_cookie_header_issue_7993() -> None: assert result[1][1].value == '"qux' assert result[2][0] == "foo2" assert result[2][1].value == "bar2" - - + + @pytest.mark.parametrize( ("input_str", "expected"), [ @@ -1558,4 +1558,3 @@ def test_unquote_compatibility_with_simplecookie(test_value: str) -> None: f"our={_unquote(test_value)!r}, " f"SimpleCookie={simplecookie_unquote(test_value)!r}" ) - From 2cc43b4336b6565d2f6b0dca757674e4423b6cd4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Jun 2025 21:55:26 -0500 Subject: [PATCH 4/7] fix merge --- tests/test_cookie_helpers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_cookie_helpers.py b/tests/test_cookie_helpers.py index 0523228fea3..4fac9f57ccb 100644 --- a/tests/test_cookie_helpers.py +++ b/tests/test_cookie_helpers.py @@ -1065,11 +1065,11 @@ def test_parse_set_cookie_headers_date_formats_with_attributes() -> None: ), ], ) -def test_parse_cookie_headers_uses_unquote_with_octal( +def test_parse_set_cookie_headers_uses_unquote_with_octal( header: str, expected_name: str, expected_value: str, expected_coded: str ) -> None: - """Test that parse_cookie_headers correctly unquotes values with octal sequences and preserves coded_value.""" - result = parse_cookie_headers([header]) + """Test that parse_set_cookie_headers correctly unquotes values with octal sequences and preserves coded_value.""" + result = parse_set_cookie_headers([header]) assert len(result) == 1 name, morsel = result[0] From abd93fecefda2a5b5d34fe6eedf5a11a938bc042 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Jun 2025 22:20:29 -0500 Subject: [PATCH 5/7] cover --- tests/test_cookie_helpers.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_cookie_helpers.py b/tests/test_cookie_helpers.py index 4fac9f57ccb..6deef6544c2 100644 --- a/tests/test_cookie_helpers.py +++ b/tests/test_cookie_helpers.py @@ -1370,6 +1370,20 @@ def test_parse_cookie_header_issue_7993() -> None: assert result[2][1].value == "bar2" +def test_parse_cookie_header_illegal_names(caplog: pytest.LogCaptureFixture) -> None: + """Test parse_cookie_header warns about illegal cookie names.""" + # Cookie name with comma (not allowed in _COOKIE_NAME_RE) + header = "good=value; invalid,cookie=bad; another=test" + result = parse_cookie_header(header) + # Should skip the invalid cookie but continue parsing + assert len(result) == 2 + assert result[0][0] == "good" + assert result[0][1].value == "value" + assert result[1][0] == "another" + assert result[1][1].value == "test" + assert "Can not load cookie: Illegal cookie name 'invalid,cookie'" in caplog.text + + @pytest.mark.parametrize( ("input_str", "expected"), [ From e477f5553c1b7ece7c296875d9cfa06d232c6133 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Jun 2025 22:22:25 -0500 Subject: [PATCH 6/7] changelog --- CHANGES/11178.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 CHANGES/11178.bugfix.rst diff --git a/CHANGES/11178.bugfix.rst b/CHANGES/11178.bugfix.rst new file mode 100644 index 00000000000..04b2e596874 --- /dev/null +++ b/CHANGES/11178.bugfix.rst @@ -0,0 +1 @@ +Fixed Cookie header parsing to treat attribute names as regular cookies per RFC 6265 -- by :user:`bdraco`. From 966d200bf42a58becfa98c364093e9ced3bebab4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Jun 2025 22:46:40 -0500 Subject: [PATCH 7/7] changelog --- CHANGES/11178.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES/11178.bugfix.rst b/CHANGES/11178.bugfix.rst index 04b2e596874..dc74cddde06 100644 --- a/CHANGES/11178.bugfix.rst +++ b/CHANGES/11178.bugfix.rst @@ -1 +1 @@ -Fixed Cookie header parsing to treat attribute names as regular cookies per RFC 6265 -- by :user:`bdraco`. +Fixed ``Cookie`` header parsing to treat attribute names as regular cookies per :rfc:`6265#section-5.4` -- by :user:`bdraco`.