Handle http errors gracefully when downloading archive (#3811)
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart
index d790893..2086ed0 100644
--- a/lib/src/source/hosted.dart
+++ b/lib/src/source/hosted.dart
@@ -1146,39 +1146,43 @@
// download.
final expectedSha256 = versionInfo.archiveSha256;
- await withAuthenticatedClient(cache, Uri.parse(description.url),
- (client) async {
- // In addition to HTTP errors, this will retry crc32c/sha256 errors as
- // well because [PackageIntegrityException] subclasses
- // [PubHttpException].
- await retryForHttp('downloading "$archiveUrl"', () async {
- final request = http.Request('GET', archiveUrl);
- request.attachMetadataHeaders();
- final response = await client.fetchAsStream(request);
+ try {
+ await withAuthenticatedClient(cache, Uri.parse(description.url),
+ (client) async {
+ // In addition to HTTP errors, this will retry crc32c/sha256 errors as
+ // well because [PackageIntegrityException] subclasses
+ // [PubHttpException].
+ await retryForHttp('downloading "$archiveUrl"', () async {
+ final request = http.Request('GET', archiveUrl);
+ request.attachMetadataHeaders();
+ final response = await client.fetchAsStream(request);
- Stream<List<int>> stream = response.stream;
- final expectedCrc32c = _parseCrc32c(response.headers, fileName);
- if (expectedCrc32c != null) {
- stream = _validateCrc32c(
- response.stream,
- expectedCrc32c,
- id,
- archiveUrl,
+ Stream<List<int>> stream = response.stream;
+ final expectedCrc32c = _parseCrc32c(response.headers, fileName);
+ if (expectedCrc32c != null) {
+ stream = _validateCrc32c(
+ response.stream,
+ expectedCrc32c,
+ id,
+ archiveUrl,
+ );
+ }
+ stream = validateSha256(
+ stream,
+ (expectedSha256 == null) ? null : Digest(expectedSha256),
);
- }
- stream = validateSha256(
- stream,
- (expectedSha256 == null) ? null : Digest(expectedSha256),
- );
- // We download the archive to disk instead of streaming it directly
- // into the tar unpacking. This simplifies stream handling.
- // Package:tar cancels the stream when it reaches end-of-archive, and
- // cancelling a http stream makes it not reusable.
- // There are ways around this, and we might revisit this later.
- await createFileFromStream(stream, archivePath);
+ // We download the archive to disk instead of streaming it directly
+ // into the tar unpacking. This simplifies stream handling.
+ // Package:tar cancels the stream when it reaches end-of-archive, and
+ // cancelling a http stream makes it not reusable.
+ // There are ways around this, and we might revisit this later.
+ await createFileFromStream(stream, archivePath);
+ });
});
- });
+ } on Exception catch (error, stackTrace) {
+ _throwFriendlyError(error, stackTrace, id.name, description.url);
+ }
var tempDir = cache.createTempDir();
try {
@@ -1286,7 +1290,7 @@
/// this tries to translate into a more user friendly error message.
///
/// Always throws an error, either the original one or a better one.
- Never _throwFriendlyError(
+ static Never _throwFriendlyError(
Exception error,
StackTrace stackTrace,
String package,
diff --git a/test/get/hosted/get_test.dart b/test/get/hosted/get_test.dart
index 692b605..c6141ab 100644
--- a/test/get/hosted/get_test.dart
+++ b/test/get/hosted/get_test.dart
@@ -6,6 +6,7 @@
import 'package:pub/src/exit_codes.dart' as exit_codes;
import 'package:pub/src/exit_codes.dart';
import 'package:pub/src/io.dart';
+import 'package:shelf/shelf.dart';
import 'package:test/test.dart';
import 'package:yaml/yaml.dart';
@@ -406,4 +407,23 @@
exitCode: DATA,
);
});
+
+ test('Fails gracefully when downloading archive', () async {
+ final server = await servePackages();
+ server.serve(
+ 'foo',
+ '1.0.0',
+ );
+ final downloadPattern =
+ RegExp(r'/packages/([^/]*)/versions/([^/]*).tar.gz');
+ server.handle(
+ downloadPattern,
+ (request) => Response(403, body: 'Go away!'),
+ );
+ await d.appDir(dependencies: {'foo': 'any'}).create();
+ await pubGet(
+ error: contains('Package not available (authorization failed).'),
+ exitCode: UNAVAILABLE,
+ );
+ });
}