Repository: aio-libs/aiohttp

Issue:
[server] Add warning when a cookie's length would exceed the RFC 6265 minimum user-agent support
🐣 **Is your feature request related to a problem? Please describe.**
We had a report in `aiohttp-session` (see aio-libs/aiohttp-session#574) that a user submitting a 'large' cookie had the cookie dropped by the receiving browser.

According to [RFC 6265 - section 6.1](https://tools.ietf.org/html/rfc6265#section-6.1):

> General-use user agents SHOULD
   provide each of the following minimum capabilities:
>   o  At least 4096 bytes per cookie (as measured by the sum of the
      length of the cookie's name, value, and attributes).

This means that cookies that exceed 4096 bytes in storage (not transmission) have a chance that the user-agent will drop them which is a case that is hard to 'guess' for a developer.


💡 **Describe the solution you'd like**
A warning log should be emitted if a cookie that would exceed the RFC minimum support is created in `aiohttp` server.

A challenge here is the definition of 'exceed' because `aiohttp` knows the transmitted size but the RFC refers to (and the user-agents seem to implement) a check on the final stored size of the cookie string, including all field names and values regardless of whether they were transmitted or filled in as defaults by the user-agent.


❓ **Describe alternatives you've considered**
It was briefly considered implementing this check in `aiohttp-session` but was rejected since this is a general issue with cookies and not specific to `aiohttp-session` so such a check could benefit all `aiohttp` users.


📋 **Additional context**
None.



Discussion:
The warning would go somewhere around:
https://github.com/aio-libs/aiohttp/blob/1a4126a2c0e52b331aa953288e844bac680290fc/aiohttp/helpers.py#L852

Maybe requires a little trial and error with a browser forcing this limit, to help figure out exactly what is included in the size limit (the linked issue mentions this being reproducible in Chrome 89).

But, at a guess, this might look something like: `len(name) + len(value) + sum(len(k) + len(v) for k, v in c.items())`
@Dreamsorcerer I came across a similar bug report in `Django`, eventually, they didn't implement a warning or anything like that (they added a warning to the documentation), because of performance reasons (calculating the whole length of every cookie in the cookies set can introduce a performance overhead).

https://code.djangoproject.com/ticket/22242
Well, that's what dev mode is for, then atleast it will catch errors for people during development.
Just wrap it in `if sys.flags.dev_mode:`

Resolving patch:
diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py
--- a/aiohttp/helpers.py
+++ b/aiohttp/helpers.py
@@ -13,6 +13,7 @@
 import re
 import sys
 import time
+import warnings
 import weakref
 from collections import namedtuple
 from contextlib import suppress
@@ -55,6 +56,7 @@
 
 PY_38 = sys.version_info >= (3, 8)
 
+COOKIE_MAX_LENGTH = 4096
 
 try:
     from typing import ContextManager
@@ -876,6 +878,15 @@ def set_cookie(
         if samesite is not None:
             c["samesite"] = samesite
 
+        if DEBUG:
+            cookie_length = len(c.output(header="")[1:])
+            if cookie_length > COOKIE_MAX_LENGTH:
+                warnings.warn(
+                    "The size of is too large, it might get ignored by the client.",
+                    UserWarning,
+                    stacklevel=2,
+                )
+
     def del_cookie(
         self, name: str, *, domain: Optional[str] = None, path: str = "/"
     ) -> None:


Test patch:
diff --git a/tests/test_web_response.py b/tests/test_web_response.py
--- a/tests/test_web_response.py
+++ b/tests/test_web_response.py
@@ -3,6 +3,7 @@
 import datetime
 import gzip
 import json
+import re
 import weakref
 from concurrent.futures import ThreadPoolExecutor
 from typing import Any, Optional
@@ -1167,3 +1168,34 @@ def test_text_is_json_encoded(self) -> None:
     def test_content_type_is_overrideable(self) -> None:
         resp = json_response({"foo": 42}, content_type="application/vnd.json+api")
         assert "application/vnd.json+api" == resp.content_type
+
+
+@pytest.mark.dev_mode
+async def test_no_warn_small_cookie(buf: Any, writer: Any) -> None:
+    resp = Response()
+    resp.set_cookie("foo", "ÿ" + "8" * 4064, max_age=2600)  # No warning
+    req = make_request("GET", "/", writer=writer)
+
+    await resp.prepare(req)
+    await resp.write_eof()
+
+    cookie = re.search(b"Set-Cookie: (.*?)\r\n", buf).group(1)
+    assert len(cookie) == 4096
+
+
+@pytest.mark.dev_mode
+async def test_warn_large_cookie(buf: Any, writer: Any) -> None:
+    resp = Response()
+
+    with pytest.warns(
+        UserWarning,
+        match="The size of is too large, it might get ignored by the client.",
+    ):
+        resp.set_cookie("foo", "ÿ" + "8" * 4065, max_age=2600)
+    req = make_request("GET", "/", writer=writer)
+
+    await resp.prepare(req)
+    await resp.write_eof()
+
+    cookie = re.search(b"Set-Cookie: (.*?)\r\n", buf).group(1)
+    assert len(cookie) == 4097