Skip to content
Closed
61 changes: 44 additions & 17 deletions tornado/httputil.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,22 @@ 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)

# Strip leading whitespace from the value
value = value.lstrip(' \t') # Remove leading spaces and tabs

# Handle Content-Length specifically
if norm_name == 'Content-Length':
self._dict[norm_name] = value # Overwrite the existing value
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."""
Expand All @@ -169,26 +178,44 @@ def get_all(self) -> Iterable[Tuple[str, str]]:
yield (name, value)

def parse_line(self, line: str) -> None:
"""Updates the dictionary with a single header line.
"""Updates the dictionary with a single header line."""

>>> h = HTTPHeaders()
>>> h.parse_line("Content-Type: text/html")
>>> h.get('content-type')
'text/html'
"""
# Check if line starts with whitespace
if line[0].isspace():
# continuation of a multi-line header
if self._last_key is None:
raise HTTPInputError("first header line cannot start with whitespace")
new_part = " " + line.lstrip(HTTP_WHITESPACE)
self._as_list[self._last_key][-1] += new_part
self._dict[self._last_key] += new_part

new_part = line.strip() # Strip leading and trailing spaces

normalized_key = _normalize_header(self._last_key)

# Handle for Content-Length
if normalized_key == 'Content-Length':
if new_part: # Only if new_part is not empty
value = new_part.split(":", 1)[-1].strip() # Get the part after the colon
self._dict[normalized_key] = value # Overwrite the existing value
self._as_list[normalized_key][-1] = value # Update last entry in the list
else:
# For other headers, we append
if new_part: # Ensure that the new part is not empty
self._as_list[normalized_key][-1] += " " + new_part
self._dict[normalized_key] += " " + new_part


else:
# Handle new headers (not continuation lines)
try:
name, value = line.split(":", 1)
name = name.strip()
value = value.strip(HTTP_WHITESPACE) # Strip leading/trailing whitespace
except ValueError:
raise HTTPInputError("no colon in header line")
self.add(name, value.strip(HTTP_WHITESPACE))

# Add or overwrite the header
self.add(name, value)

# Update the last key
self._last_key = _normalize_header(name)

@classmethod
def parse(cls, headers: str) -> "HTTPHeaders":
Expand Down
20 changes: 20 additions & 0 deletions tornado/test/httputil_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,26 @@ def test_data_after_final_boundary(self):


class HTTPHeadersTest(unittest.TestCase):

def test_parse_line_with_trailing_spaces(self):
headers = HTTPHeaders()

# Test header with multiple trailing spaces
headers.parse_line("Content-Length: 0 ")
self.assertEqual(headers.get('content-length'), '0')

# Test header with leading and trailing spaces
headers.parse_line(" Content-Length : 123 ")
self.assertEqual(headers.get('content-length'), '123') # Ensure value is overwritten

# Test continuation line with trailing spaces
headers.parse_line("Content-Length: 42")
headers.parse_line(" ") # Test multi-line continuation
self.assertEqual(headers.get('content-length'), '42') # Ensure spaces don't affect value




def test_multi_line(self):
# Lines beginning with whitespace are appended to the previous line
# with any leading whitespace replaced by a single space.
Expand Down