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