Handle poor package-listing responses robustly. (#2847)

diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart
index a6a4c17..39b2f95 100644
--- a/lib/src/source/hosted.dart
+++ b/lib/src/source/hosted.dart
@@ -178,20 +178,29 @@
 
   Map<PackageId, _VersionInfo> _versionInfoFromPackageListing(
       Map body, PackageRef ref, Uri location) {
-    final versions = body['versions'] as List;
-    return Map.fromEntries(versions.map((map) {
-      var pubspec = Pubspec.fromMap(map['pubspec'], systemCache.sources,
-          expectedName: ref.name, location: location);
-      var id = source.idFor(ref.name, pubspec.version,
-          url: _serverFor(ref.description));
-      final archiveUrlValue = map['archive_url'];
-      final archiveUrl =
-          archiveUrlValue is String ? Uri.tryParse(archiveUrlValue) : null;
-      final status = PackageStatus(
-          isDiscontinued: body['isDiscontinued'] as bool ?? false,
-          discontinuedReplacedBy: body['replacedBy'] as String);
-      return MapEntry(id, _VersionInfo(pubspec, archiveUrl, status));
-    }));
+    final versions = body['versions'];
+    if (versions is List) {
+      return Map.fromEntries(versions.map((map) {
+        final pubspecData = map['pubspec'];
+        if (pubspecData is Map) {
+          var pubspec = Pubspec.fromMap(pubspecData, systemCache.sources,
+              expectedName: ref.name, location: location);
+          var id = source.idFor(ref.name, pubspec.version,
+              url: _serverFor(ref.description));
+          var archiveUrl = map['archive_url'];
+          if (archiveUrl is String) {
+            final status = PackageStatus(
+                isDiscontinued: body['isDiscontinued'] as bool ?? false,
+                discontinuedReplacedBy: body['replacedBy'] as String);
+            return MapEntry(
+                id, _VersionInfo(pubspec, Uri.parse(archiveUrl), status));
+          }
+          throw FormatException('archive_url must be a String');
+        }
+        throw FormatException('pubspec must be a map');
+      }));
+    }
+    throw FormatException('versions must be a list');
   }
 
   Future<Map<PackageId, _VersionInfo>> _fetchVersions(PackageRef ref) async {
@@ -201,16 +210,17 @@
 
     String bodyText;
     Map body;
+    Map<PackageId, _VersionInfo> result;
     try {
       // TODO(sigurdm): Implement cancellation of requests. This probably
       // requires resolution of: https://github.com/dart-lang/sdk/issues/22265.
       bodyText = await httpClient.read(url, headers: pubApiHeaders);
       body = jsonDecode(bodyText);
+      result = _versionInfoFromPackageListing(body, ref, url);
     } catch (error, stackTrace) {
       var parsed = source._parseDescription(ref.description);
       _throwFriendlyError(error, stackTrace, parsed.first, parsed.last);
     }
-    final result = _versionInfoFromPackageListing(body, ref, url);
 
     // Cache the response on disk.
     // Don't cache overly big responses.
@@ -264,7 +274,10 @@
               tryDeleteEntry(cachePath);
             } else {
               return _versionInfoFromPackageListing(
-                  cachedDoc, ref, Uri.file(cachePath));
+                cachedDoc,
+                ref,
+                Uri.file(cachePath),
+              );
             }
           }
         } on io.IOException {
@@ -565,6 +578,11 @@
     } else if (error is io.TlsException) {
       fail('Got TLS error trying to find package $package at $url.', error,
           stackTrace);
+    } else if (error is FormatException) {
+      throw PackageNotFoundException(
+          'Got badly formatted response trying to find package $package at $url',
+          innerError: error,
+          innerTrace: stackTrace);
     } else {
       // Otherwise re-throw the original exception.
       throw error;
diff --git a/test/hosted/fail_gracefully_on_bad_version_listing_response_test.dart b/test/hosted/fail_gracefully_on_bad_version_listing_response_test.dart
new file mode 100644
index 0000000..5f83baf
--- /dev/null
+++ b/test/hosted/fail_gracefully_on_bad_version_listing_response_test.dart
@@ -0,0 +1,40 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:shelf/shelf.dart';
+import 'package:test/test.dart';
+
+import 'package:pub/src/exit_codes.dart' as exit_codes;
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+void main() {
+  forBothPubGetAndUpgrade((command) {
+    test(
+        'fails gracefully if the package server responds with broken package listings',
+        () async {
+      await servePackages((b) => b..serve('foo', '1.2.3'));
+      globalPackageServer.extraHandlers[RegExp('/api/packages/.*')] =
+          expectAsync1((request) {
+        expect(request.method, 'GET');
+        return Response(200,
+            body: jsonEncode({
+              'notTheRight': {'response': 'type'}
+            }));
+      });
+      await d.appDir({'foo': '1.2.3'}).create();
+
+      await pubCommand(command,
+          error: allOf([
+            contains(
+                'Got badly formatted response trying to find package foo at http://localhost:'),
+            contains('), version solving failed.')
+          ]),
+          exitCode: exit_codes.DATA);
+    });
+  });
+}