S3: fix Range request handling (#6778)

This commit is contained in:
Ben Simon Hartung 2023-09-07 09:20:24 +02:00 committed by GitHub
parent 4ea51d8795
commit 3e928bcad6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 64 additions and 9 deletions

View File

@ -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)

View File

@ -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():