diff --git a/lib/src/authentication/client.dart b/lib/src/authentication/client.dart
index c36e52975..ffa7889cd 100644
--- a/lib/src/authentication/client.dart
+++ b/lib/src/authentication/client.dart
@@ -11,6 +11,7 @@ import 'package:http_parser/http_parser.dart';
import '../http.dart';
import '../log.dart' as log;
import '../system_cache.dart';
+import '../utils.dart';
import 'credential.dart';
/// This client authenticates requests by injecting `Authentication` header to
@@ -83,11 +84,7 @@ class _AuthenticatedClient extends http.BaseClient {
}
}
if (serverMessage != null) {
- // Only allow printable ASCII, map anything else to whitespace, take
- // at-most 1024 characters.
- serverMessage = String.fromCharCodes(
- serverMessage.runes.map((r) => 32 <= r && r <= 127 ? r : 32).take(1024),
- );
+ serverMessage = sanitizeForTerminal(serverMessage);
}
throw AuthenticationException(response.statusCode, serverMessage);
}
diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart
index befe15a8d..8b63718f2 100644
--- a/lib/src/command/lish.dart
+++ b/lib/src/command/lish.dart
@@ -179,9 +179,7 @@ class LishCommand extends PubCommand {
} on PubHttpResponseException catch (error) {
var url = error.response.request!.url;
if (url == cloudStorageUrl) {
- // TODO(nweiz): the response may have XML-formatted information about
- // the error. Try to parse that out once we have an easily-accessible
- // XML parser.
+ handleGCSError(error.response);
fail(log.red('Failed to upload the package.'));
} else if (Uri.parse(url.origin) == Uri.parse(host.origin)) {
handleJsonError(error.response);
diff --git a/lib/src/http.dart b/lib/src/http.dart
index 294d567f2..157e3e945 100644
--- a/lib/src/http.dart
+++ b/lib/src/http.dart
@@ -211,6 +211,40 @@ void handleJsonError(http.BaseResponse response) {
fail(log.red(error['message'] as String));
}
+/// Handles an unsuccessful XML-formatted response from google cloud storage.
+///
+/// Assumes messages are of the form in
+/// https://cloud.google.com/storage/docs/xml-api/reference-status.
+///
+/// This is a poor person's XML parsing with regexps, but this should be
+/// sufficient for the specified messages.
+void handleGCSError(http.BaseResponse response) {
+ if (response is http.Response) {
+ final responseBody = response.body;
+ if (responseBody.contains('(.*)$tag>').firstMatch(responseBody)?[1];
+ if (result == null) return null;
+ return sanitizeForTerminal(result);
+ }
+
+ final code = getTagText('Code');
+ final message = getTagText('Message');
+ // `Details` are not specified in the doc above, but have been observed in actual responses.
+ final details = getTagText('Details');
+ if (code != null) {
+ log.error('Server error code: $code');
+ }
+ if (message != null) {
+ log.error('Server message: $message');
+ }
+ if (details != null) {
+ log.error('Server details: $details');
+ }
+ }
+ }
+}
+
/// Parses a response body, assuming it's JSON-formatted.
///
/// Throws a user-friendly error if the response body is invalid JSON, or if
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 31ca1d188..d43780a30 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -767,3 +767,13 @@ extension RetrieveFlags on ArgResults {
String option(String name) => this[name] as String;
String? optionWithoutDefault(String name) => this[name] as String?;
}
+
+/// Limits the range of characters and length.
+///
+/// Useful for displaying externally provided strings.
+///
+/// Only allowd printable ASCII, map anything else to whitespace, take at-most
+/// 1024 characters.
+String sanitizeForTerminal(String input) => String.fromCharCodes(
+ input.runes.map((r) => 32 <= r && r <= 127 ? r : 32).take(1024),
+ );
diff --git a/test/lish/cloud_storage_upload_provides_an_error_test.dart b/test/lish/cloud_storage_upload_provides_an_error_test.dart
index bd14d84d0..a11ba9542 100644
--- a/test/lish/cloud_storage_upload_provides_an_error_test.dart
+++ b/test/lish/cloud_storage_upload_provides_an_error_test.dart
@@ -22,14 +22,30 @@ void main() {
globalServer.expect('POST', '/upload', (request) {
return request.read().drain().then((_) {
return shelf.Response.notFound(
- 'Your request sucked.',
+ // Actual example of an error code we get from GCS
+ "EntityTooLarge
Your proposed upload is larger than the maximum object size specified in your Policy Document.Content-length exceeds upper bound on range ",
headers: {'content-type': 'application/xml'},
);
});
});
- // TODO(nweiz): This should use the server's error message once the client
- // can parse the XML.
+ expect(
+ pub.stderr,
+ emits(
+ 'Server error code: EntityTooLarge',
+ ),
+ );
+ expect(
+ pub.stderr,
+ emits(
+ 'Server message: Your proposed upload is larger than the maximum object size specified in your Policy Document.',
+ ),
+ );
+ expect(
+ pub.stderr,
+ emits('Server details: Content-length exceeds upper bound on range'),
+ );
+
expect(pub.stderr, emits('Failed to upload the package.'));
await pub.shouldExit(1);
});