Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error handling improvements #2067

Merged
merged 2 commits into from
Mar 9, 2022

Conversation

amanda-tarafa
Copy link
Contributor

As usual one commit at a time.

@amanda-tarafa amanda-tarafa requested a review from jskeet March 3, 2022 17:14
@amanda-tarafa amanda-tarafa requested a review from a team as a code owner March 3, 2022 17:14
@amanda-tarafa amanda-tarafa self-assigned this Mar 3, 2022
@amanda-tarafa
Copy link
Contributor Author

amanda-tarafa commented Mar 3, 2022

Things that we can consider:

  • Add more information in GoogleApiException.Message if no message has been specified.
  • Even if a message was specified, attach more information to it. Not sure I like this option, I think we should respect whatever the messages calling code specifies.

Copy link
Collaborator

@jskeet jskeet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally fine - a few nits, but my only real concern is whether the default ToString() is now too big.

}

/// <summary>Creates an API Service exception.</summary>
public GoogleApiException(string serviceName, string message) : this(serviceName, message, null) { }
public GoogleApiException(string serviceName, string message)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the formatting was actually clearer before, to be honest. not overly bothered though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed : this(...) back to the previous line but left the { } on the next line, so it's clearer that the constructor is not doing anything itself.

: this(serviceName, message, null)
{ }

/// <summary>Creates an API Service exception.</summary>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expand the comment to say "with no message" and maybe explain when the Message property will still have useful information?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, done.


private string ServiceNameMessage => $"The service {serviceName} has thrown an exception.";

private string DefaultMessage =>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unwrap to single line?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And maybe rename to "ServiceHttpAndErrorMessage" or something - "DefaultMessage" sounds like "the message you'd get if you didn't override the Message property" which is the opposite of what it means :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unwrapped done and renamed to CombinedMessage.

@@ -26,6 +26,7 @@ namespace Google
public class GoogleApiException : Exception
{
private readonly string serviceName;
private readonly string message;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, this can probably be a boolean just to indicate whether a message was supplied.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And let's give it a leading underscore, which won't be inconsistent if we take other suggestions...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, done

@@ -26,6 +26,7 @@ namespace Google
public class GoogleApiException : Exception
{
private readonly string serviceName;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this and make ServiceName a read-only autoprop. That remove the unconventional field name and makes the code feel more modern :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, done.

// Parse the error from the current response.
error = await service.DeserializeError(responseMessage).ConfigureAwait(false);
}
catch(GoogleApiException ex) when (!(ex.Error is null))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: add space. Oh for later versions of C# with "is not null". (Or just use ex.Error is object for now.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

{
internal static async Task<RequestError> DeserializeErrorAsync(this HttpResponseMessage response, string name, ISerializer serializer)
{
if (response?.Content is null)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}
catch (JsonException ex)
{
throw new GoogleApiException(name, null, ex)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use a named argument for the second one (I assume it's message).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, yes it's the message

Error = new RequestError { ErrorResponseContent = responseText }
};
}
if (error == null)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is instead of ==, if you've got the energy to change it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

{
internal static class HttpResponseMessageExtensions
{
internal static async Task<RequestError> DeserializeErrorAsync(this HttpResponseMessage response, string name, ISerializer serializer)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to add a comment to explain the situations in which this throws an exception instead of returning an error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor Author

@amanda-tarafa amanda-tarafa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jon, all comments adressed on the latest commit.

For trying to reduce the size of the ToString() I'm only printing the raw content if we couldn't parse any of it as an ErrorResponse instead of always.

I'm taking a look at BQ and Storage now and see what direct improvements we can do there.

@@ -26,6 +26,7 @@ namespace Google
public class GoogleApiException : Exception
{
private readonly string serviceName;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, done.

@@ -26,6 +26,7 @@ namespace Google
public class GoogleApiException : Exception
{
private readonly string serviceName;
private readonly string message;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, done

}

/// <summary>Creates an API Service exception.</summary>
public GoogleApiException(string serviceName, string message) : this(serviceName, message, null) { }
public GoogleApiException(string serviceName, string message)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed : this(...) back to the previous line but left the { } on the next line, so it's clearer that the constructor is not doing anything itself.

: this(serviceName, message, null)
{ }

/// <summary>Creates an API Service exception.</summary>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, done.


private string ServiceNameMessage => $"The service {serviceName} has thrown an exception.";

private string DefaultMessage =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unwrapped done and renamed to CombinedMessage.

// Parse the error from the current response.
error = await service.DeserializeError(responseMessage).ConfigureAwait(false);
}
catch(GoogleApiException ex) when (!(ex.Error is null))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Error = new RequestError { ErrorResponseContent = responseText }
};
}
if (error == null)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}
catch (JsonException ex)
{
throw new GoogleApiException(name, null, ex)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, yes it's the message

{
internal static class HttpResponseMessageExtensions
{
internal static async Task<RequestError> DeserializeErrorAsync(this HttpResponseMessage response, string name, ISerializer serializer)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

{
internal static async Task<RequestError> DeserializeErrorAsync(this HttpResponseMessage response, string name, ISerializer serializer)
{
if (response?.Content is null)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@jskeet
Copy link
Collaborator

jskeet commented Mar 7, 2022

Will look tomorrow...

Copy link
Collaborator

@jskeet jskeet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few nits, but they could be dealt with separately if you'd prefer

serviceName.ThrowIfNull("serviceName");
this.serviceName = serviceName;
this.message = message;
ServiceName = serviceName.ThrowIfNull("serviceName");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: use nameof.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

this.serviceName = serviceName;
this.message = message;
ServiceName = serviceName.ThrowIfNull("serviceName");
_hasMessage = !(message is null);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

message is object?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

: $"Raw error details are: {Error.ErrorResponseContent}";
: (Error.IsOnlyRawContent
? $"No JSON error details were specified.{Environment.NewLine}" +
(Error.ErrorResponseContent.Trim() == ""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use string.IsNullOrWhitespace?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, yes, of course, done.

string exToString = exception.ToString();
Assert.Contains(ErrorMessageHanlder.ExpectedError, exToString);
Assert.Contains(ErrorMessageHanlder.ErrorContent, exToString);
Assert.Contains(ErrorMessageHanlder.ExpectedError, exception.ToString());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: at some point, maybe not in this PR, let's fix ErrorMessageHanlder to ErrorMessageHandler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm adding a commit for this.

/// </list>
/// Any exception thrown while reading the <paramref name="response"/> <see cref="HttpResponseMessage.Content"/>
/// will be bubbled up.
/// Otherwie this method will returned the deserialized <see cref="RequestError"/>.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Otherwie => Otherwise

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, and also returned-> return

In particular improves GoogleApiExcetion error messages and string representation by including more information.
In cases when then response does not contain a valid JSON body, the error message has changed entirely to be more explicit.
This affects normal, batch, and media upload and download requests.

Closes googleapis#2063.
Towards googleapis/google-cloud-dotnet#8206

BREAKING CHANGE: If code was depending on exact exception message or string representation content.
Copy link
Contributor Author

@amanda-tarafa amanda-tarafa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All latest nits addressed.

I've squashed all the commits into one, and added a new commit fixing the test class name typo.

serviceName.ThrowIfNull("serviceName");
this.serviceName = serviceName;
this.message = message;
ServiceName = serviceName.ThrowIfNull("serviceName");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

this.serviceName = serviceName;
this.message = message;
ServiceName = serviceName.ThrowIfNull("serviceName");
_hasMessage = !(message is null);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

: $"Raw error details are: {Error.ErrorResponseContent}";
: (Error.IsOnlyRawContent
? $"No JSON error details were specified.{Environment.NewLine}" +
(Error.ErrorResponseContent.Trim() == ""
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, yes, of course, done.

string exToString = exception.ToString();
Assert.Contains(ErrorMessageHanlder.ExpectedError, exToString);
Assert.Contains(ErrorMessageHanlder.ErrorContent, exToString);
Assert.Contains(ErrorMessageHanlder.ExpectedError, exception.ToString());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm adding a commit for this.

/// </list>
/// Any exception thrown while reading the <paramref name="response"/> <see cref="HttpResponseMessage.Content"/>
/// will be bubbled up.
/// Otherwie this method will returned the deserialized <see cref="RequestError"/>.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, and also returned-> return

@amanda-tarafa amanda-tarafa merged commit 92c58dd into googleapis:main Mar 9, 2022
@amanda-tarafa amanda-tarafa deleted the error-handling branch March 9, 2022 14:23
amanda-tarafa added a commit to amanda-tarafa/google-api-dotnet-client that referenced this pull request Mar 25, 2022
Features
- [googleapis#2067] BREAKING. Improve error handling for failed requests.
  BREAKING CHANGE: If calling code depended on exact GoogleApiException message or string representation.
- [googleapis#2086] Makes `LocalServerCodeReceiver` partially extendable.
jskeet pushed a commit that referenced this pull request Mar 26, 2022
Features
- [#2067] BREAKING. Improve error handling for failed requests.
  BREAKING CHANGE: If calling code depended on exact GoogleApiException message or string representation.
- [#2086] Makes `LocalServerCodeReceiver` partially extendable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants