diff --git a/CHANGES.txt b/CHANGES.txt index b0a0ef2f..e16c06b6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -134,6 +134,10 @@ Experimental Features Bugfix ~~~~~~ +- Suffix byte ranges larger than a non-empty selected representation now + return the entire representation, as required by RFC 9110 section 14.1.2, + instead of being treated as unsatisfiable and producing a 416 response. + - ``webob.cookies.serialize_cookie_date()`` now converts a timezone-aware ``datetime`` to UTC before serializing it, instead of writing its local wall-clock time out as GMT. Naive datetimes continue to be treated as UTC. diff --git a/src/webob/byterange.py b/src/webob/byterange.py index 823bd499..3646e387 100644 --- a/src/webob/byterange.py +++ b/src/webob/byterange.py @@ -28,7 +28,7 @@ def range_for_length(self, length): if end is None: end = length if start < 0: - start += length + start = max(0, start + length) if _is_content_range_valid(start, end, length): stop = min(end, length) return (start, stop) diff --git a/tests/test_byterange.py b/tests/test_byterange.py index a10a9f60..b83735f1 100644 --- a/tests/test_byterange.py +++ b/tests/test_byterange.py @@ -8,12 +8,17 @@ def test_not_satisfiable(): - range = Range.parse("bytes=-100") - assert range.range_for_length(50) is None range = Range.parse("bytes=100-") assert range.range_for_length(50) is None +@pytest.mark.parametrize("suffix", [50, 51, 100]) +def test_suffix_range_covering_entire_representation(suffix): + range = Range.parse(f"bytes=-{suffix}") + assert range.range_for_length(50) == (0, 50) + assert tuple(range.content_range(50)) == (0, 50, 50) + + def test_range_parse(): assert isinstance(Range.parse("bytes=0-99"), Range) assert isinstance(Range.parse("BYTES=0-99"), Range) diff --git a/tests/test_response.py b/tests/test_response.py index d1108d86..8ded8feb 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -360,6 +360,17 @@ def test_conditional_response_range_not_satisfiable_response(): assert resp.body == b"Requested range not satisfiable: bytes=100-200" +def test_conditional_response_suffix_range_larger_than_body(): + req = Request.blank("/", range="bytes=-100") + resp = req.get_response(Response(b"foo\n", conditional_response=True)) + assert resp.status_code == 206 + assert resp.content_range.start == 0 + assert resp.content_range.stop == 4 + assert resp.content_range.length == 4 + assert resp.content_length == 4 + assert resp.body == b"foo\n" + + def test_HEAD_conditional_response_range_not_satisfiable_response(): req = Request.blank("/", method="HEAD", range="bytes=100-200") resp = Response(app_iter=["foo\n"], content_length=4, conditional_response=True)