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, + ); + }); }