Skip to content

Commit

Permalink
Finish download on seeing 416 response with zero byte range. (#86)
Browse files Browse the repository at this point in the history
Closes [google-cloud python #6572](googleapis/google-cloud-python#6572).
  • Loading branch information
sumit-ql authored and tseaver committed Aug 28, 2019
1 parent 2b9ffc8 commit fdf86b3
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 0 deletions.
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

0 comments on commit fdf86b3

Please sign in to comment.