diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9b62eb62..73f559df 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 (:pr:`1108`). + 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)