Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finish download on seeing 416 response with zero byte range. #86

Merged
merged 3 commits into from
Aug 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions google/resumable_media/_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
flags=re.IGNORECASE)
_ACCEPTABLE_STATUS_CODES = (http_client.OK, http_client.PARTIAL_CONTENT)
_GET = u'GET'
_ZERO_CONTENT_RANGE_HEADER = u'bytes */0'


class DownloadBase(object):
Expand Down Expand Up @@ -340,6 +341,11 @@ def _process_response(self, response):
.. _sans-I/O: https://sans-io.readthedocs.io/
"""
# Verify the response before updating the current instance.
if _check_for_zero_content_range(response, self._get_status_code,
self._get_headers):
self._finished = True
return

_helpers.require_status_code(
response, _ACCEPTABLE_STATUS_CODES,
self._get_status_code, callback=self._make_invalid)
Expand Down Expand Up @@ -478,3 +484,28 @@ def get_range_info(response, get_headers, callback=_helpers.do_nothing):
int(match.group(u'end_byte')),
int(match.group(u'total_bytes'))
)


def _check_for_zero_content_range(response, get_status_code, get_headers):
""" Validate if response status code is 416 and content range is zero.

This is the special case for handling zero bytes files.

Args:
response (object): An HTTP response object.
get_status_code (Callable[Any, int]): Helper to get a status code
from a response.
get_headers (Callable[Any, Mapping[str, str]]): Helper to get headers
from an HTTP response.

Returns:
bool: True if content range total bytes is zero, false otherwise.
"""
if get_status_code(response) == http_client. \
REQUESTED_RANGE_NOT_SATISFIABLE:
content_range = _helpers.header_required(
response, _helpers.CONTENT_RANGE_HEADER,
get_headers, callback=_helpers.do_nothing)
if content_range == _ZERO_CONTENT_RANGE_HEADER:
return True
return False
50 changes: 50 additions & 0 deletions tests/unit/test__download.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,25 @@ def test__process_response_when_reaching_end(self):
assert download.total_bytes == 8 * chunk_size
assert stream.getvalue() == data

def test__process_response_when_content_range_is_zero(self):
chunk_size = 10
stream = mock.Mock(spec=[u'write'])
download = _download.ChunkedDownload(
EXAMPLE_URL, chunk_size, stream)
_fix_up_virtual(download)

content_range = _download._ZERO_CONTENT_RANGE_HEADER
headers = {u'content-range': content_range}
status_code = http_client.REQUESTED_RANGE_NOT_SATISFIABLE
response = mock.Mock(headers=headers,
status_code=status_code,
spec=[u'headers', 'status_code'])
download._process_response(response)
stream.write.assert_not_called()
assert download.finished
assert download.bytes_downloaded == 0
assert download.total_bytes is None

def test_consume_next_chunk(self):
download = _download.ChunkedDownload(EXAMPLE_URL, 256, None)
with pytest.raises(NotImplementedError) as exc_info:
Expand Down Expand Up @@ -662,6 +681,37 @@ def test_missing_header_with_callback(self):
callback.assert_called_once_with()


class Test__check_for_zero_content_range(object):

@staticmethod
def _make_response(content_range, status_code):
headers = {u'content-range': content_range}
return mock.Mock(headers=headers,
status_code=status_code,
spec=[u'headers', 'status_code'])

def test_status_code_416_and_test_content_range_zero_both(self):
content_range = _download._ZERO_CONTENT_RANGE_HEADER
status_code = http_client.REQUESTED_RANGE_NOT_SATISFIABLE
response = self._make_response(content_range, status_code)
assert _download._check_for_zero_content_range(
response, _get_status_code, _get_headers)

def test_status_code_416_only(self):
content_range = u'bytes 2-5/3'
status_code = http_client.REQUESTED_RANGE_NOT_SATISFIABLE
response = self._make_response(content_range, status_code)
assert not _download._check_for_zero_content_range(
response, _get_status_code, _get_headers)

def test_content_range_zero_only(self):
content_range = _download._ZERO_CONTENT_RANGE_HEADER
status_code = http_client.OK
response = self._make_response(content_range, status_code)
assert not _download._check_for_zero_content_range(
response, _get_status_code, _get_headers)


def _get_status_code(response):
return response.status_code

Expand Down