diff --git a/google/resumable_media/_download.py b/google/resumable_media/_download.py index 9fa581a4..71965992 100644 --- a/google/resumable_media/_download.py +++ b/google/resumable_media/_download.py @@ -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): @@ -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) @@ -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 diff --git a/tests/unit/test__download.py b/tests/unit/test__download.py index e9499af4..f7ac008f 100644 --- a/tests/unit/test__download.py +++ b/tests/unit/test__download.py @@ -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: @@ -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