From 9457b2c3356d4d393b33f1c7dfcfbdd1bff49fa5 Mon Sep 17 00:00:00 2001 From: Sigurd Meldgaard Date: Thu, 14 Sep 2023 14:35:06 +0000 Subject: [PATCH] Display error messages from GCS --- lib/src/authentication/client.dart | 7 ++-- lib/src/command/lish.dart | 4 +-- lib/src/http.dart | 34 +++++++++++++++++++ lib/src/utils.dart | 10 ++++++ ...storage_upload_provides_an_error_test.dart | 22 ++++++++++-- 5 files changed, 66 insertions(+), 11 deletions(-) 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('(.*)').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 + "EntityTooLargeYour 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); });