Repository: tornadoweb/tornado

Issue:
6.3 set_cookie() got an unexpected keyword argument 'HttpOnly'
Getting this issue changing from Tornado 6.2 -> 6.3
```py
rtb-webapp-1     | [E 230418 15:28:08 BaseHandlers:195] Request from x.x.x.x resulted in an error code 500:
rtb-webapp-1     |     Traceback (most recent call last):
rtb-webapp-1     |       File "/usr/local/lib/python3.8/site-packages/tornado/web.py", line 1767, in _execute
rtb-webapp-1     |         result = method(*self.path_args, **self.path_kwargs)
rtb-webapp-1     |       File "/opt/rtb/libs/SecurityDecorators.py", line 106, in wrapper
rtb-webapp-1     |         return method(self, *args, **kwargs)
rtb-webapp-1     |       File "/opt/rtb/handlers/PublicHandlers.py", line 224, in post
rtb-webapp-1     |         self.valid_login(user)
rtb-webapp-1     |       File "/opt/rtb/handlers/PublicHandlers.py", line 276, in valid_login
rtb-webapp-1     |         self.successful_login(user)
rtb-webapp-1     |       File "/opt/rtb/handlers/PublicHandlers.py", line 291, in successful_login
rtb-webapp-1     |         self.start_session()
rtb-webapp-1     |       File "/opt/rtb/handlers/BaseHandlers.py", line 102, in start_session
rtb-webapp-1     |         self.set_secure_cookie("session_id", self.session.session_id, **flags)
rtb-webapp-1     |       File "/usr/local/lib/python3.8/site-packages/tornado/web.py", line 756, in set_signed_cookie
rtb-webapp-1     |         self.set_cookie(
rtb-webapp-1     |     TypeError: set_cookie() got an unexpected keyword argument 'HttpOnly'
```


Discussion:
Oops, looks like the http.cookies module has some case-insensitive magic that I missed. I'll get this fixed, but in the meantime you can work around it by changing to lowercase `httponly` (it looks like `**flags` in your code contains `HttpOnly`). 

Resolving patch:
diff --git a/tornado/web.py b/tornado/web.py
--- a/tornado/web.py
+++ b/tornado/web.py
@@ -79,6 +79,7 @@ async def main():
 import sys
 import threading
 import time
+import warnings
 import tornado
 import traceback
 import types
@@ -607,6 +608,7 @@ def set_cookie(
         httponly: bool = False,
         secure: bool = False,
         samesite: Optional[str] = None,
+        **kwargs: Any,
     ) -> None:
         """Sets an outgoing cookie name/value with the given options.
 
@@ -623,6 +625,10 @@ def set_cookie(
         to set an expiration time in days from today (if both are set, ``expires``
         is used).
 
+        .. deprecated:: 6.3
+           Keyword arguments are currently accepted case-insensitively.
+           In Tornado 7.0 this will be changed to only accept lowercase
+           arguments.
         """
         # The cookie library only accepts type str, in both python 2 and 3
         name = escape.native_str(name)
@@ -657,6 +663,17 @@ def set_cookie(
             morsel["secure"] = True
         if samesite:
             morsel["samesite"] = samesite
+        if kwargs:
+            # The setitem interface is case-insensitive, so continue to support
+            # kwargs for backwards compatibility until we can remove deprecated
+            # features.
+            for k, v in kwargs.items():
+                morsel[k] = v
+            warnings.warn(
+                f"Deprecated arguments to set_cookie: {set(kwargs.keys())} "
+                "(should be lowercase)",
+                DeprecationWarning,
+            )
 
     def clear_cookie(self, name: str, **kwargs: Any) -> None:
         """Deletes the cookie with the given name.


Test patch:
diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py
--- a/tornado/test/web_test.py
+++ b/tornado/test/web_test.py
@@ -17,6 +17,7 @@
 from tornado.simple_httpclient import SimpleAsyncHTTPClient
 from tornado.template import DictLoader
 from tornado.testing import AsyncHTTPTestCase, AsyncTestCase, ExpectLog, gen_test
+from tornado.test.util import ignore_deprecation
 from tornado.util import ObjectDict, unicode_type
 from tornado.web import (
     Application,
@@ -318,6 +319,11 @@ def get(self):
                 self.set_cookie("c", "1", httponly=True)
                 self.set_cookie("d", "1", httponly=False)
 
+        class SetCookieDeprecatedArgs(RequestHandler):
+            def get(self):
+                # Mixed case is supported, but deprecated
+                self.set_cookie("a", "b", HttpOnly=True, pATH="/foo")
+
         return [
             ("/set", SetCookieHandler),
             ("/get", GetCookieHandler),
@@ -327,6 +333,7 @@ def get(self):
             ("/set_max_age", SetCookieMaxAgeHandler),
             ("/set_expires_days", SetCookieExpiresDaysHandler),
             ("/set_falsy_flags", SetCookieFalsyFlags),
+            ("/set_deprecated", SetCookieDeprecatedArgs),
         ]
 
     def test_set_cookie(self):
@@ -413,6 +420,12 @@ def test_set_cookie_false_flags(self):
         self.assertEqual(headers[2].lower(), "c=1; httponly; path=/")
         self.assertEqual(headers[3].lower(), "d=1; path=/")
 
+    def test_set_cookie_deprecated(self):
+        with ignore_deprecation():
+            response = self.fetch("/set_deprecated")
+        header = response.headers.get("Set-Cookie")
+        self.assertEqual(header, "a=b; HttpOnly; Path=/foo")
+
 
 class AuthRedirectRequestHandler(RequestHandler):
     def initialize(self, login_url):