From 3e928bcad6552bc09d4542859180701ca508a524 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Thu, 7 Sep 2023 09:20:24 +0200 Subject: [PATCH] S3: fix Range request handling (#6778) --- moto/s3/responses.py | 28 +++++++++++++++++-------- tests/test_s3/test_s3.py | 45 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index 701400b72..1510ce112 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -1169,25 +1169,35 @@ class S3Response(BaseResponse): ) -> TYPE_RESPONSE: length = len(response_content) last = length - 1 + _, rspec = request.headers.get("range").split("=") if "," in rspec: - raise NotImplementedError("Multiple range specifiers not supported") + return 200, response_headers, response_content - def toint(i: Any) -> Optional[int]: - return int(i) if i else None + try: + begin, end = [int(i) if i else None for i in rspec.split("-")] + except ValueError: + # if we can't parse the Range header, S3 just treat the request as a non-range request + return 200, response_headers, response_content + + if (begin is None and end == 0) or (begin is not None and begin > last): + raise InvalidRange( + actual_size=str(length), range_requested=request.headers.get("range") + ) - begin, end = map(toint, rspec.split("-")) if begin is not None: # byte range end = last if end is None else min(end, last) elif end is not None: # suffix byte range begin = length - min(end, length) end = last else: - return 400, response_headers, "" - if begin < 0 or end > last or begin > min(end, last): - raise InvalidRange( - actual_size=str(length), range_requested=request.headers.get("range") - ) + # Treat as non-range request + return 200, response_headers, response_content + + if begin > min(end, last): + # Treat as non-range request if after the logic is applied, the start of the range is greater than the end + return 200, response_headers, response_content + response_headers["content-range"] = f"bytes {begin}-{end}/{length}" content = response_content[begin : end + 1] response_headers["content-length"] = len(content) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 962ea8602..766f14de1 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -1004,6 +1004,51 @@ def test_ranged_get(): assert key.content_length == 100 + assert key.get(Range="bytes=0-0")["Body"].read() == b"0" + assert key.get(Range="bytes=1-1")["Body"].read() == b"1" + + range_req = key.get(Range="bytes=1-0") + assert range_req["Body"].read() == rep * 10 + # assert that the request was not treated as a range request + assert range_req["ResponseMetadata"]["HTTPStatusCode"] == 200 + assert "ContentRange" not in range_req + + range_req = key.get(Range="bytes=-1-") + assert range_req["Body"].read() == rep * 10 + assert range_req["ResponseMetadata"]["HTTPStatusCode"] == 200 + + range_req = key.get(Range="bytes=0--1") + assert range_req["Body"].read() == rep * 10 + assert range_req["ResponseMetadata"]["HTTPStatusCode"] == 200 + + range_req = key.get(Range="bytes=0-1,3-4,7-9") + assert range_req["Body"].read() == rep * 10 + assert range_req["ResponseMetadata"]["HTTPStatusCode"] == 200 + + range_req = key.get(Range="bytes=-") + assert range_req["Body"].read() == rep * 10 + assert range_req["ResponseMetadata"]["HTTPStatusCode"] == 200 + + with pytest.raises(ClientError) as ex: + key.get(Range="bytes=-0") + assert ex.value.response["Error"]["Code"] == "InvalidRange" + assert ( + ex.value.response["Error"]["Message"] + == "The requested range is not satisfiable" + ) + assert ex.value.response["Error"]["ActualObjectSize"] == "100" + assert ex.value.response["Error"]["RangeRequested"] == "bytes=-0" + + with pytest.raises(ClientError) as ex: + key.get(Range="bytes=101-200") + assert ex.value.response["Error"]["Code"] == "InvalidRange" + assert ( + ex.value.response["Error"]["Message"] + == "The requested range is not satisfiable" + ) + assert ex.value.response["Error"]["ActualObjectSize"] == "100" + assert ex.value.response["Error"]["RangeRequested"] == "bytes=101-200" + @mock_s3 def test_policy():