Repository: googleapis/google-api-python-client

Issue:
MediaIoBaseDownload next_chunk() Range header off-by-one
#### Environment details

  - OS type and version:
  ```
  Ubuntu 20.04.3 LTS
  ```
  - Python version: `python --version`
  ```
  Python 3.9.7
  ```
  - pip version: `pip --version`
  ```
  pip 21.3.1
  ```
  - `google-api-python-client` version: `pip show google-api-python-client`
  ```
  Name: google-api-python-client
  Version: 2.29.0
  Summary: Google API Client Library for Python
  Home-page: https://github.com/googleapis/google-api-python-client/
  Author: Google LLC
  Author-email: googleapis-packages@google.com
  License: Apache 2.0
  Requires: google-api-core, google-auth, google-auth-httplib2, httplib2, uritemplate
  ```

#### Steps to reproduce

  1. Use `MediaIoBaseDownload(..., chunksize=1024)` for downloading a file from Google Drive (see https://developers.google.com/drive/api/v3/manage-downloads#python)
  2. Received chunk is 1025 bytes
  3. See examples / behavior defined in https://httpwg.org/specs/rfc7233.html#rule.ranges-specifier

#### Code example

```python
import logging
from io import BytesIO

logging.basicConfig(level="DEBUG")

# example / skipping drive, auth setup for obvious reasons

file_id = "foobar123456789"
chunk_size = 1024
file_obj = BytesIO()
request = drive.files().get_media(fileId=file_id)
downloader =  MediaIoBaseDownload(file_obj, request, chunksize=chunk_size)
done = False
while not done:
    # debug log the lower level HTTP request headers?
    status, done = downloader.next_chunk()
    logging.debug(
        "Download status %r %s/%s bytes (%.1f%%)",
        file_id,
        status.resumable_progress,
        status.total_size,
        status.progress() * 100,
    )
    if not done:
        assert file_obj.tell() == status.resumable_progress
        assert file_obj.tell() % chunk_size == 0
    else:
        assert file_obj.tell() == status.total_size
```

In real life chunk size should be few megabytes at least. Default seems to be 100MiB:
```
googleapiclient/http.py:DEFAULT_CHUNK_SIZE = 100 * 1024 * 1024
```

#### Stack trace

.

#### Patch

Is it worth making a PR, signing CLA? :D


```diff
diff --git a/googleapiclient/http.py b/googleapiclient/http.py
index 1b661e1b..927464e9 100644
--- a/googleapiclient/http.py
+++ b/googleapiclient/http.py
@@ -733,7 +733,7 @@ class MediaIoBaseDownload(object):
         headers = self._headers.copy()
         headers["range"] = "bytes=%d-%d" % (
             self._progress,
-            self._progress + self._chunksize,
+            self._progress + self._chunksize - 1,
         )
         http = self._request.http
 
```




Discussion:
Hi @jvtm , I'm looking into it. In the meantime, please feel free to open a PR.

Resolving patch:
diff --git a/googleapiclient/http.py b/googleapiclient/http.py
--- a/googleapiclient/http.py
+++ b/googleapiclient/http.py
@@ -733,7 +733,7 @@ def next_chunk(self, num_retries=0):
         headers = self._headers.copy()
         headers["range"] = "bytes=%d-%d" % (
             self._progress,
-            self._progress + self._chunksize,
+            self._progress + self._chunksize - 1,
         )
         http = self._request.http
 


Test patch:
diff --git a/tests/test_http.py b/tests/test_http.py
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -488,6 +488,23 @@ def test_media_io_base_download(self):
         self.assertEqual(5, download._progress)
         self.assertEqual(5, download._total_size)
 
+    def test_media_io_base_download_range_request_header(self):
+        self.request.http = HttpMockSequence(
+            [
+                (
+                    {"status": "200", "content-range": "0-2/5"},
+                    "echo_request_headers_as_json",
+                ),
+            ]
+        )
+
+        download = MediaIoBaseDownload(fd=self.fd, request=self.request, chunksize=3)
+
+        status, done = download.next_chunk()
+        result = json.loads(self.fd.getvalue().decode("utf-8"))
+
+        self.assertEqual(result.get("range"), "bytes=0-2")
+
     def test_media_io_base_download_custom_request_headers(self):
         self.request.http = HttpMockSequence(
             [