From f94698d0ffab9a02b720a37b261ab1eb37586d6b Mon Sep 17 00:00:00 2001 From: simpleqt <2015900818@qq.com> Date: Wed, 4 Nov 2026 03:21:30 +0800 Subject: [PATCH 1/2] fix(tornadoparser): treat a repeated header as a single value Tornado joins repeated headers into one comma-separated string, so a header value is a str, not a sequence of values. WebArgsTornadoMultiDictProxy expanded it with a comprehension, which yielded the string's characters: loading a repeated header into a List field produced a list of single-character strings. Return the whole string as one list element instead, matching what the other parsers (e.g. Flask) report for the same input. Genuine sequences (such as tornado's query_arguments, a dict of list[bytes]) are still expanded as before. Signed-off-by: simpleqt <2015900818@qq.com> --- CHANGELOG.rst | 8 ++++++++ src/webargs/tornadoparser.py | 6 ++++++ tests/test_tornadoparser.py | 14 ++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9b62eb62..0fb03400 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,14 @@ Features: ``__version_info__`` constants from ``__init__.py`` file. Users should use ``importlib.metadata`` instead (:pr:`1024`). +Bug fixes: + +* Fix the Tornado parser treating a repeated header as a sequence of + characters: Tornado joins repeated headers into a single comma-separated + string, so loading such a header into a ``List`` field produced a list of + single-character strings. It is now loaded as a single value, matching the + other parsers. + Other changes: diff --git a/src/webargs/tornadoparser.py b/src/webargs/tornadoparser.py index 1b0c73d8..3129fbd2 100644 --- a/src/webargs/tornadoparser.py +++ b/src/webargs/tornadoparser.py @@ -55,6 +55,12 @@ def __getitem__(self, key: str) -> typing.Any: if value is core.missing: return core.missing if key in self.multiple_keys: + # A str or bytes value is a single value, not a sequence of + # them: Tornado joins repeated headers into one comma-separated + # string (e.g. "a, b"), and iterating over it would yield its + # characters. Only genuine sequences are expanded. + if isinstance(value, (str, bytes)): + return [_unicode(value)] return [ _unicode(v) if isinstance(v, (str, bytes)) else v for v in value ] diff --git a/tests/test_tornadoparser.py b/tests/test_tornadoparser.py index ee92eadd..2898ff06 100644 --- a/tests/test_tornadoparser.py +++ b/tests/test_tornadoparser.py @@ -171,6 +171,20 @@ def test_it_should_get_multiple_values(self): result = parser.load_headers(request, author_schema) assert result["works"] == ["Prometheus Bound", "Prometheus Unbound"] + def test_it_should_treat_a_repeated_header_as_one_value(self): + # Tornado collects repeated headers into a single comma-separated + # string, so a List field must see one value and not the string's + # characters. + headers = tornado.httputil.HTTPHeaders() + headers.add("name", "Euphorion") + headers.add("works", "Prometheus Bound") + headers.add("works", "Prometheus Unbound") + assert headers["works"] == "Prometheus Bound,Prometheus Unbound" + + request = make_request(headers=headers) + result = parser.load_headers(request, author_schema) + assert result["works"] == ["Prometheus Bound,Prometheus Unbound"] + def test_it_should_return_missing_if_not_present(self): request = make_request() result = parser.load_headers(request, author_schema) From 2166a71474a817ae0a553d0c8d99ed6c71d61dc6 Mon Sep 17 00:00:00 2001 From: simpleqt <2015900818@qq.com> Date: Wed, 4 Nov 2026 03:23:57 +0800 Subject: [PATCH 2/2] docs: reference the pull request in the changelog entry Signed-off-by: simpleqt <2015900818@qq.com> --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0fb03400..73f559df 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,7 +16,7 @@ Bug fixes: characters: Tornado joins repeated headers into a single comma-separated string, so loading such a header into a ``List`` field produced a list of single-character strings. It is now loaded as a single value, matching the - other parsers. + other parsers (:pr:`1108`). Other changes: