diff --git a/tornado/httputil.py b/tornado/httputil.py index 4ec7b68fed..e849f0ee95 100644 --- a/tornado/httputil.py +++ b/tornado/httputil.py @@ -145,13 +145,31 @@ def add(self, name: str, value: str) -> None: """Adds a new value for the given key.""" norm_name = _normalize_header(name) self._last_key = norm_name - if norm_name in self: - self._dict[norm_name] = ( - native_str(self[norm_name]) + "," + native_str(value) - ) - self._as_list[norm_name].append(value) + + # Handle Content-Length specifically + if norm_name == 'Content-Length': + if not value.isdigit(): + raise HTTPInputError("Invalid Content-Length header, must be a non-negative integer") + if int(value) < 0: + raise HTTPInputError("Invalid Content-Length header, must be a non-negative integer") + + if norm_name in self._dict: + # Compare the existing value (which is stored as a string) to the new integer value (converted to string) + if self._dict[norm_name] != value: + raise HTTPInputError("Conflicting Content-Length headers") + + # Store the Content-Length as a string + self._dict[norm_name] = value # Overwrite the existing value as a string + self._as_list[norm_name] = [value] # Reset list with the new value + else: - self[norm_name] = value + # For all other headers, append the value + if norm_name in self._dict: + self._dict[norm_name] += ',' + value # Append with a comma + self._as_list[norm_name].append(value) # Append to the list + else: + self._dict[norm_name] = value + self._as_list[norm_name] = [value] # Initialize the list with the new value def get_list(self, name: str) -> List[str]: """Returns all values for the given header as a list.""" diff --git a/tornado/test/httputil_test.py b/tornado/test/httputil_test.py index 75c92fffd9..c56a27122d 100644 --- a/tornado/test/httputil_test.py +++ b/tornado/test/httputil_test.py @@ -261,6 +261,38 @@ def test_data_after_final_boundary(self): class HTTPHeadersTest(unittest.TestCase): + + def test_multiple_content_length_headers(self): + headers = HTTPHeaders() + + headers.parse_line("Content-Length: 123") + + headers.parse_line("Content-Length: 123") + self.assertEqual(headers.get("content-length"), "123") + with self.assertRaises(HTTPInputError): + headers.parse_line("Content-Length: 456") # Should raise error + def test_invalid_content_length(self): + headers = HTTPHeaders() + with self.assertRaises(HTTPInputError): + headers.parse_line("Content-Length: abc") # Should raise error + + def test_negative_content_length(self): + headers = HTTPHeaders() + with self.assertRaises(HTTPInputError): + headers.parse_line("Content-Length: -123") + + def test_leading_trailing_whitespace(self): + headers = HTTPHeaders() + headers.parse_line("Content-Length: 123 ") + self.assertEqual(headers.get('content-length'), '123') # Should handle whitespace correctly + + def test_zero_content_length(self): + headers = HTTPHeaders() + headers.parse_line("Content-Length: 0") + self.assertEqual(headers.get('content-length'), '0') # Should handle zero correctly + + + def test_multi_line(self): # Lines beginning with whitespace are appended to the previous line # with any leading whitespace replaced by a single space.