Skip to content

Commit

Permalink
Adds Range header support to content app
Browse files Browse the repository at this point in the history
This unskips the `Range` header and adds support for it to the content
app.

closes #8865
  • Loading branch information
bmbouter committed Jul 8, 2021
1 parent 9c1f115 commit 44d0359
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 22 deletions.
2 changes: 2 additions & 0 deletions CHANGES/8865.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fixed bug where content app would not respond to `Range` HTTP Header in requests, e.g. from Anaconda
clients.
41 changes: 36 additions & 5 deletions pulpcore/content/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ def get_remote_artifacts_blocking():
return response

except (ClientResponseError, UnsupportedDigestValidationError) as e:
log.warn(
log.warning(
_("Could not download remote artifact at '{}': {}").format(
remote_artifact.url, str(e)
)
Expand Down Expand Up @@ -809,15 +809,46 @@ def cast_remote_blocking():

remote = await loop.run_in_executor(None, cast_remote_blocking)

async def handle_headers(headers):
rng = request.http_range
if rng.start or rng.stop:
response.set_status(206)

async def handle_response_headers(headers):
for name, value in headers.items():
if name.lower() in self.hop_by_hop_headers:
lower_name = name.lower()
if lower_name in self.hop_by_hop_headers:
continue

if response.status == 206 and lower_name == "content-length":
range_bytes = int(value)
start = 0 if rng.start is None else rng.start
stop = range_bytes if rng.stop is None else rng.stop

range_bytes = range_bytes - rng.start
range_bytes = range_bytes - (int(value) - stop)
response.headers[name] = str(range_bytes)

response.headers["Content-Range"] = "bytes {0}-{1}/{2}".format(
start, stop - start + 1, int(value)
)
continue

response.headers[name] = value
await response.prepare(request)

async def handle_data(data):
await response.write(data)
if rng.start or rng.stop:
start_byte_pos = 0
end_byte_pos = len(data)
if rng.start:
start_byte_pos = max(0, rng.start - downloader._size)
if rng.stop:
end_byte_pos = min(len(data), rng.stop - downloader._size)

data_for_client = data[start_byte_pos:end_byte_pos]
await response.write(data_for_client)
else:
await response.write(data)
if remote.policy != Remote.STREAMED:
await original_handle_data(data)

Expand All @@ -826,7 +857,7 @@ async def finalize():
await original_finalize()

downloader = remote.get_downloader(
remote_artifact=remote_artifact, headers_ready_callback=handle_headers
remote_artifact=remote_artifact, headers_ready_callback=handle_response_headers
)
original_handle_data = downloader.handle_data
downloader.handle_data = handle_data
Expand Down
87 changes: 70 additions & 17 deletions pulpcore/tests/functional/api/using_plugin/test_distributions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pulp_smash.pulp3.bindings import delete_orphans
from pulp_smash.pulp3.utils import (
download_content_unit,
download_content_unit_return_requests_response,
gen_distribution,
gen_repo,
get_content,
Expand Down Expand Up @@ -329,16 +330,61 @@ def test_content_served_immediate(self):
self.setup_download_test("immediate")
self.do_test_content_served()

@unittest.skip("https://pulp.plan.io/issues/8865")
def test_content_served_on_demand_with_range_request(self):
def test_content_served_immediate_with_range_request_valid_middle_range(self):
"""Assert that downloaded content can be properly downloaded with range requests."""
self.setup_download_test("immediate")
range_headers = {"Range": "bytes=2-11"}
num_bytes = 10
self.do_range_request_download_test(range_headers, num_bytes)

def test_content_served_on_demand_with_range_request_valid_middle_range(self):
"""Assert that on_demand content can be properly downloaded with range requests."""
self.setup_download_test("on_demand")
self.do_range_request_download_test()
range_headers = {"Range": "bytes=2-11"}
num_bytes = 10
self.do_range_request_download_test(range_headers, num_bytes)

def test_content_served_immediate_with_range_request(self):
"""Assert that downloaded content can be properly downloaded with range requests."""
def test_content_served_immediate_with_multiple_different_range_requests(self):
"""Assert that multiple requests with different Range header values work as expected."""
self.setup_download_test("immediate")
range_headers = {"Range": "bytes=2-11"}
num_bytes = 10
self.do_range_request_download_test(range_headers, num_bytes)
range_headers = {"Range": "bytes=22-36"}
num_bytes = 15
self.do_range_request_download_test(range_headers, num_bytes)

def test_content_served_immediate_with_range_request_invalid_start_value(self):
"""Assert that range requests with a negative start value errors as expected."""
self.setup_download_test("immediate")

self.assertRaises(
HTTPError,
download_content_unit_return_requests_response,
self.cfg,
self.distribution.to_dict(),
"1.iso",
headers={"Range": "bytes=-1-11"},
)

def test_content_served_immediate_with_range_request_too_large_end_value(self):
"""Assert that a range request with a end value that is larger than the data works still."""
self.setup_download_test("immediate")
self.do_range_request_download_test()
range_headers = {"Range": "bytes=2-1500"}
num_bytes = 1022
self.do_range_request_download_test(range_headers, num_bytes)

def test_content_served_immediate_with_range_request_start_value_larger_than_content(self):
"""Assert that a range request with a start value larger than the content errors."""
self.setup_download_test("immediate")
self.assertRaises(
HTTPError,
download_content_unit_return_requests_response,
self.cfg,
self.distribution.to_dict(),
"1.iso",
headers={"Range": "bytes=1500-1600"},
)

def setup_download_test(self, policy):
# Create a repository
Expand Down Expand Up @@ -388,19 +434,26 @@ def do_test_content_served(self):

self.assertEqual(len(pulp_manifest), FILE_FIXTURE_COUNT, pulp_manifest)

def do_range_request_download_test(self):
def do_range_request_download_test(self, range_header, expected_bytes):
file_path = "1.iso"

headers = {"Range": "bytes=0-9"} # first 10 bytes
NUM_BYTES = 10

req1 = download_content_unit(
self.cfg, self.distribution.to_dict(), file_path, headers=headers
req1_reponse = download_content_unit_return_requests_response(
self.cfg, self.distribution.to_dict(), file_path, headers=range_header
)
req2 = download_content_unit(
self.cfg, self.distribution.to_dict(), file_path, headers=headers
req2_response = download_content_unit_return_requests_response(
self.cfg, self.distribution.to_dict(), file_path, headers=range_header
)

self.assertEqual(NUM_BYTES, len(req1))
self.assertEqual(NUM_BYTES, len(req2))
self.assertEqual(req1, req2)
self.assertEqual(expected_bytes, len(req1_reponse.content))
self.assertEqual(expected_bytes, len(req2_response.content))
self.assertEqual(req1_reponse.content, req2_response.content)

self.assertEqual(req1_reponse.status_code, 206)
self.assertEqual(req1_reponse.status_code, req2_response.status_code)

self.assertEqual(str(expected_bytes), req1_reponse.headers["Content-Length"])
self.assertEqual(str(expected_bytes), req2_response.headers["Content-Length"])

self.assertEqual(
req1_reponse.headers["Content-Range"], req2_response.headers["Content-Range"]
)

0 comments on commit 44d0359

Please sign in to comment.