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