From 4a7866c8ad598bbe925b8229c72b7c200d7904ab Mon Sep 17 00:00:00 2001 From: Zo Bot Date: Wed, 1 Jul 2026 07:49:43 +0000 Subject: [PATCH] _parse_request_range: accept range unit in any case per RFC 7233 The pre-fix code compared the range unit with `unit != "bytes"`, which rejected any Range header that didn't spell the unit in exactly lowercase. RFC 7233 section 2.1 is explicit: "all rules derived from token are to be compared case-insensitively, like range-unit and acceptable-ranges." A conforming client that sends `Range: BYTES=1-2` was being silently rejected, and the request was treated as having no Range header at all. Switching the comparison to `unit.lower() != "bytes"` makes the parser match the RFC, while still rejecting unknown units (which is the only behaviour the rest of the function relies on downstream of this check). Added a doctest and a small ParseRequestRangeTest class covering the lowercase baseline, the uppercase case, the mixed-case form, the suffix and -N range shapes, and the negative case (an unknown unit like "items" still returns None). --- tornado/httputil.py | 4 +++- tornado/test/httputil_test.py | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tornado/httputil.py b/tornado/httputil.py index 4bd17786d..e03ba61a5 100644 --- a/tornado/httputil.py +++ b/tornado/httputil.py @@ -874,6 +874,8 @@ def _parse_request_range( (None, 0) >>> _parse_request_range("bytes=") (None, None) + >>> _parse_request_range("BYTES=1-2") + (1, 3) >>> _parse_request_range("foo=42") >>> _parse_request_range("bytes=1-2,6-10") @@ -885,7 +887,7 @@ def _parse_request_range( """ unit, _, value = range_header.partition("=") unit, value = unit.strip(), value.strip() - if unit != "bytes": + if unit.lower() != "bytes": return None start_b, _, end_b = value.partition("-") try: diff --git a/tornado/test/httputil_test.py b/tornado/test/httputil_test.py index 4e966eb50..5619ef286 100644 --- a/tornado/test/httputil_test.py +++ b/tornado/test/httputil_test.py @@ -14,6 +14,7 @@ HTTPServerRequest, ParseMultipartConfig, RequestStartLine, + _parse_request_range, format_timestamp, parse_cookie, parse_multipart_form_data, @@ -623,6 +624,31 @@ def test_parse_request_start_line(self): self.assertEqual(parsed_start_line.version, self.VERSION) +class ParseRequestRangeTest(unittest.TestCase): + """Tests for httputil._parse_request_range.""" + + def test_lowercase_unit(self): + self.assertEqual(_parse_request_range("bytes=1-2"), (1, 3)) + + def test_uppercase_unit_accepted(self): + # Per RFC 7233 section 2.1: "all rules derived from token are to be + # compared case-insensitively, like range-unit and acceptable-ranges." + # Pre-fix, an uppercase "BYTES" unit returned None. + self.assertEqual(_parse_request_range("BYTES=1-2"), (1, 3)) + + def test_mixed_case_unit_accepted(self): + self.assertEqual(_parse_request_range("Bytes=1-2"), (1, 3)) + self.assertEqual(_parse_request_range("bYtEs=1-2"), (1, 3)) + + def test_uppercase_unit_with_suffix_range(self): + self.assertEqual(_parse_request_range("BYTES=6-"), (6, None)) + self.assertEqual(_parse_request_range("BYTES=-6"), (-6, None)) + + def test_non_bytes_unit_still_rejected(self): + # An unknown unit is rejected regardless of case. + self.assertIsNone(_parse_request_range("items=1-2")) + + class ParseCookieTest(unittest.TestCase): # These tests copied from Django: # https://github.com/django/django/pull/6277/commits/da810901ada1cae9fc1f018f879f11a7fb467b28