[common] stop crashing with range requests in the browser (#468)

Remove check that cannot be done in the browser
Also remove invalid header sets for HTTP requests in the browser

Fixes https://github.com/google/googleapis.dart/issues/462
diff --git a/discoveryapis_commons/CHANGELOG.md b/discoveryapis_commons/CHANGELOG.md
index 41bffe1..0db79e2 100644
--- a/discoveryapis_commons/CHANGELOG.md
+++ b/discoveryapis_commons/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 1.0.4
+
+- Eliminate the invalid header warning in the browser.
+- Fix issue with range requests from the browser.
+  ([#462](https://github.com/google/googleapis.dart/issues/462))
+
 ## 1.0.3
 
 - Throw a more helpful error message when a resumable upload fails.
diff --git a/discoveryapis_commons/lib/src/api_requester.dart b/discoveryapis_commons/lib/src/api_requester.dart
index 67bc494..444a77f 100644
--- a/discoveryapis_commons/lib/src/api_requester.dart
+++ b/discoveryapis_commons/lib/src/api_requester.dart
@@ -108,14 +108,19 @@
           'Content length of response does not match requested range length.',
         );
       }
-      final contentRange = response.headers['content-range'];
-      final expected = 'bytes ${downloadRange.start}-${downloadRange.end}/';
-      if (contentRange == null || !contentRange.startsWith(expected)) {
-        throw client_requests.ApiRequestError(
-          'Attempting partial '
-          "download but got invalid 'Content-Range' header "
-          '(was: $contentRange, expected: $expected).',
-        );
+
+      if (!isWeb) {
+        // TODO(kevmoo) on the web, should check access-control-expose-headers
+        // but this is easy for now
+        final contentRange = response.headers[_contentRangeHeader];
+        final expected = 'bytes ${downloadRange.start}-${downloadRange.end}/';
+        if (contentRange == null || !contentRange.startsWith(expected)) {
+          throw client_requests.ApiRequestError(
+            'Attempting partial '
+            "download but got invalid '$_contentRangeHeader' header "
+            '(was: $contentRange, expected: $expected).',
+          );
+        }
       }
     }
 
@@ -183,12 +188,16 @@
 
     Future<http.StreamedResponse> simpleUpload() {
       final bodyStream = uploadMedia!.stream;
-      final request = RequestImpl(method, uri, bodyStream);
-      request.headers.addAll({
-        ..._requestHeaders,
-        'content-type': uploadMedia.contentType,
-        'content-length': '${uploadMedia.length}'
-      });
+      final request = RequestImpl(
+        method,
+        uri,
+        stream: bodyStream,
+        headers: {
+          ..._requestHeaders,
+          'content-type': uploadMedia.contentType,
+          'content-length': '${uploadMedia.length}'
+        },
+      );
       return _httpClient.send(request);
     }
 
@@ -210,13 +219,12 @@
           'range': 'bytes=${downloadRange.start}-${downloadRange.end}',
       };
 
-      // Filter out headers forbidden in the browser (in calling in browser).
-      // If we don't do this, the browser will complain that we're attempting
-      // to set a header that we're not allowed to set.
-      headers.removeWhere((key, value) => _forbiddenHeaders.contains(key));
-
-      final request = RequestImpl(method, uri, bodyController.stream);
-      request.headers.addAll(headers);
+      final request = RequestImpl(
+        method,
+        uri,
+        stream: bodyController.stream,
+        headers: headers,
+      );
       return _httpClient.send(request);
     }
 
@@ -318,10 +326,4 @@
   }
 }
 
-/// List of headers that is forbidden in current execution context.
-///
-/// In a browser context we're not allowed to set `user-agent` and
-/// `content-length` headers.
-const _forbiddenHeaders = bool.fromEnvironment('dart.library.html')
-    ? <String>{'user-agent', 'content-length'}
-    : <String>{};
+const _contentRangeHeader = 'content-range';
diff --git a/discoveryapis_commons/lib/src/multipart_media_uploader.dart b/discoveryapis_commons/lib/src/multipart_media_uploader.dart
index 91dae1b..248f266 100644
--- a/discoveryapis_commons/lib/src/multipart_media_uploader.dart
+++ b/discoveryapis_commons/lib/src/multipart_media_uploader.dart
@@ -73,8 +73,8 @@
       'content-length': '$totalLength'
     };
     final bodyStream = bodyController.stream;
-    final request = RequestImpl(_method, _uri, bodyStream);
-    request.headers.addAll(headers);
+    final request =
+        RequestImpl(_method, _uri, stream: bodyStream, headers: headers);
     return _httpClient.send(request);
   }
 }
diff --git a/discoveryapis_commons/lib/src/request_impl.dart b/discoveryapis_commons/lib/src/request_impl.dart
index 9ff7df4..825086c 100644
--- a/discoveryapis_commons/lib/src/request_impl.dart
+++ b/discoveryapis_commons/lib/src/request_impl.dart
@@ -6,11 +6,25 @@
 
 import 'package:http/http.dart' as http;
 
+import 'utils.dart';
+
 class RequestImpl extends http.BaseRequest {
   final Stream<List<int>> _stream;
 
-  RequestImpl(super.method, super.url, [Stream<List<int>>? stream])
-      : _stream = stream ?? const Stream.empty();
+  RequestImpl(
+    super.method,
+    super.url, {
+    Stream<List<int>>? stream,
+    Map<String, String>? headers,
+  }) : _stream = stream ?? const Stream.empty() {
+    if (headers != null) {
+      for (var entry in headers.entries) {
+        if (!_forbiddenHeaders.contains(entry.key)) {
+          this.headers[entry.key] = entry.value;
+        }
+      }
+    }
+  }
 
   @override
   http.ByteStream finalize() {
@@ -18,3 +32,10 @@
     return http.ByteStream(_stream);
   }
 }
+
+/// List of headers that is forbidden in current execution context.
+///
+/// In a browser context we're not allowed to set `user-agent` and
+/// `content-length` headers.
+const _forbiddenHeaders =
+    isWeb ? <String>{'user-agent', 'content-length'} : <String>{};
diff --git a/discoveryapis_commons/lib/src/resumable_media_uploader.dart b/discoveryapis_commons/lib/src/resumable_media_uploader.dart
index e275a50..dd9deb8 100644
--- a/discoveryapis_commons/lib/src/resumable_media_uploader.dart
+++ b/discoveryapis_commons/lib/src/resumable_media_uploader.dart
@@ -139,14 +139,16 @@
     final bodyStream =
         bytes == null ? const Stream<List<int>>.empty() : Stream.value(bytes);
 
-    final request = RequestImpl(_method, _uri, bodyStream);
-    request.headers.addAll({
+    final headers = {
       ..._requestHeaders,
       'content-type': contentTypeJsonUtf8,
       'content-length': '$length',
       'x-upload-content-type': _uploadMedia.contentType,
       'x-upload-content-length': '${_uploadMedia.length}',
-    });
+    };
+
+    final request =
+        RequestImpl(_method, _uri, stream: bodyStream, headers: headers);
 
     final response = await _httpClient.send(request);
 
@@ -248,8 +250,7 @@
     };
 
     final stream = Stream.fromIterable(chunk.byteArrays);
-    final request = RequestImpl('PUT', uri, stream);
-    request.headers.addAll(headers);
+    final request = RequestImpl('PUT', uri, stream: stream, headers: headers);
     return _httpClient.send(request);
   }
 }
diff --git a/discoveryapis_commons/lib/src/utils.dart b/discoveryapis_commons/lib/src/utils.dart
index ab6498a..da1e404 100644
--- a/discoveryapis_commons/lib/src/utils.dart
+++ b/discoveryapis_commons/lib/src/utils.dart
@@ -17,3 +17,5 @@
 
 String escapeVariable(String name) =>
     Uri.encodeQueryComponent(name).replaceAll('+', '%20');
+
+const isWeb = bool.fromEnvironment('dart.library.html');
diff --git a/discoveryapis_commons/pubspec.yaml b/discoveryapis_commons/pubspec.yaml
index 8aa6738..739ca71 100644
--- a/discoveryapis_commons/pubspec.yaml
+++ b/discoveryapis_commons/pubspec.yaml
@@ -1,5 +1,5 @@
 name: _discoveryapis_commons
-version: 1.0.3
+version: 1.0.4
 description: Library for use by client APIs generated from Discovery Documents.
 repository: https://github.com/google/googleapis.dart/tree/master/discoveryapis_commons
 
diff --git a/test_integration/web/drive_demo.dart b/test_integration/web/drive_demo.dart
index bd07ab5..b73620c 100644
--- a/test_integration/web/drive_demo.dart
+++ b/test_integration/web/drive_demo.dart
@@ -72,6 +72,17 @@
     );
 
     logToTextArea(jsonEncode(newFile));
+
+    logToTextArea('try to read part of the file back - asking for 11 bytes!');
+
+    // regression check for https://github.com/google/googleapis.dart/issues/462
+
+    final partialRequest = await api.get(
+      newFile.id!,
+      downloadOptions: drive.PartialDownloadOptions(drive.ByteRange(0, 10)),
+    ) as drive.Media;
+
+    logToTextArea('bytes received: ${partialRequest.length}');
   } finally {
     client.close();
   }