-
Notifications
You must be signed in to change notification settings - Fork 318
api: Include stack trace when reporting MalformedServerResponseException #1374
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
base: main
Are you sure you want to change the base?
Conversation
It doesn't look like this new parameter Please test your change end to end in the app, and revise it so it solves the problem described in the issue:
|
ffb0a30
to
4a5a7c9
Compare
Hi @PIG208, whenever you get a chance, could you please take a look at my PR for an initial review? Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this! Left some comments.
test/api/exception_test.dart
Outdated
wrongType['properties']! as Map<String, dynamic>; | ||
fail('Should have thrown'); | ||
} catch (e, stackTrace) { | ||
final exception = MalformedServerResponseException( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A more end-to-end test will be more valuable. MalformedServerResponseException
already has a bunch of tests in test/api/core_test.dart
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should then leave a comment similar to this one above:
// NetworkException.toString: see "API network errors" test in core_test.dart
to indicate that the tests live elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bumping this. I meant that we should test MalformedServerResponseException.toString
in test/api/core_test.dart
, removing the test in test/api/exception_test.dart
, and adding the comment in test/api/exception_test.dart
indicating that the test for MalformedServerResponseException.toString
lives in core_test.dart.
The other comment that you moved in the new revision was meant to be at where it was earlier, since it is a placeholder indicating that a test would be there when it isn't; it's not a description of the test.
lib/api/exception.dart
Outdated
|
||
MalformedServerResponseException({ | ||
required super.routeName, | ||
required super.httpStatus, | ||
required super.data, | ||
this.causeException, | ||
this.causeStackTrace | ||
}) : super(message: causeException == null | ||
? GlobalLocalizations.zulipLocalizations | ||
.errorMalformedResponse(httpStatus) | ||
: GlobalLocalizations.zulipLocalizations | ||
.errorMalformedResponseWithCause( | ||
httpStatus, causeException.toString())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already stringify causeException
and make it a part of message
. Including it again in the toString
implementation will repeat that in the error message. This is what I got when printing out the error from the test:
MalformedServerResponseException: 200 registerQueue: Server gave malformed response; HTTP status 200; type 'String' is not a subtype of type 'Map<String, dynamic>' in type cast
Caused by: type 'String' is not a subtype of type 'Map<String, dynamic>' in type cast
#0 main.<anonymous closure> (file:///home/zixuan/zulip-stuff/zulip-flutter/test/api/exception_test.dart:49:32)
#1 Declarer.test.<anonymous closure>.<anonymous closure> (package:test_api/src/backend/declarer.dart:229:19)
<asynchronous suspension>
#2 Declarer.test.<anonymous closure> (package:test_api/src/backend/declarer.dart:227:7)
<asynchronous suspension>
#3 Invoker._waitForOutstandingCallbacks.<anonymous closure> (package:test_api/src/backend/invoker.dart:258:9)
<asynchronous suspension>
true
lib/api/exception.dart
Outdated
sb.write(" $routeName"); | ||
sb.write(": $message"); | ||
if (causeException != null) { | ||
sb.write('\nCaused by: $causeException'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New strings added should be localized. We already have errorMalformedResponseWithCause
and errorMalformedResponse
, so it might be a good idea to reuse them/add one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I see that you added "Caused by" back. It's an English phrase that should be translated.
lib/api/exception.dart
Outdated
sb.write(": $message"); | ||
if (causeException != null) { | ||
sb.write('\nCaused by: $causeException'); | ||
if (causeStackTrace != null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like it would be a developer error if causeException
is present but causeStackTrace
is not. Let's have an assertion in the constructor to validate that.
Hi @PIG208, thanks for the above reviews. I have pushed the revision. PTAL. |
assets/l10n/app_en.arb
Outdated
@@ -552,6 +552,24 @@ | |||
"details": {"type": "String", "example": "type 'Null' is not a subtype of type 'String' in type cast"} | |||
} | |||
}, | |||
"errorMalformedResponseFormat": "{className}: {httpStatus} {routeName}: {message}", | |||
"@errorMalformedResponseFormat": { | |||
"description": "Format for error messages from malformed server responses. The message parameter is already localized.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the audience of the string description is mainly the translators; so there is nothing actionable for them knowing that message
is localized. Similarly, we can leave out "format" in both the name of the string and its description.
How about naming it "errorMalformedServerResponse"? For rewriting the description, you can find soem examples with:
git grep -hEA2 'error.+\",' assets/l10n/app_en.arb
You can check out git grep --help
for what each of these options mean.
assets/l10n/app_en.arb
Outdated
"errorMalformedServerResponseExceptionAssertMessage": "If causeException is provided, causeStackTrace must also be provided", | ||
"@errorMalformedServerResponseExceptionAssertMessage": { | ||
"description": "Developer error message when creating a MalformedServerResponseException with incomplete error details" | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because assertions are only run in debug builds, messages from them are usually only visible to the developer. We can skip translating those strings.
assets/l10n/app_en.arb
Outdated
"errorMalformedServerResponseExceptionName": "MalformedServerResponseException", | ||
"@errorMalformedServerResponseExceptionName": { | ||
"description": "Name of the error type for malformed server responses" | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, it doesn't seem that a class name that is only meaningful to our codebase can be translated.
lib/api/exception.dart
Outdated
final zulipLocalizations = GlobalLocalizations.zulipLocalizations; | ||
return zulipLocalizations.errorMalformedResponseFormat( | ||
zulipLocalizations.errorMalformedServerResponseExceptionName, | ||
httpStatus, | ||
routeName, | ||
message); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like the stack trace is not used in this revision. We should include it in the error message.
I think the structure we had in the previous revision is good. #1374 (comment) was for the untranslated "Cause by". Since we have removed it in favor of errorMalformedResponseWithCause
, the remaining parts of the error message probably don't need translations.
Thanks for the update! Left some comments. |
Hi @PIG208, thanks for the review. I have pushed the revision. PTAL! |
Hi @PIG208 , whenever you get some time, please take a look at the PR. Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the update! Left some more comments
lib/api/exception.dart
Outdated
}) : assert(causeException == null || causeStackTrace != null, | ||
'If causeException is provided, causeStackTrace must also be provided'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't cover the case when causeException
is missing and causeStackTrace
is given. How about something like (causeException == null && causeStackTrace == null) || (causeException != null && causeStackTrace != null)
?
lib/api/exception.dart
Outdated
sb.write(" $routeName"); | ||
sb.write(": $message"); | ||
if (causeException != null) { | ||
sb.write('\nCaused by: $causeException'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I see that you added "Caused by" back. It's an English phrase that should be translated.
lib/api/exception.dart
Outdated
@override | ||
String toString() { | ||
final StringBuffer sb = StringBuffer('MalformedServerResponseException'); | ||
sb.write(" $routeName"); | ||
sb.write(": $message"); | ||
if (causeException != null) { | ||
sb.write('\nCaused by: $causeException'); | ||
sb.write('\n$causeStackTrace'); | ||
} | ||
return sb.toString(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I see that you added "Caused by" back. It's an English phrase that should be translated. However, maybe we don't need to override toString
at all? ZulipApiException
overrides it because it has multiple checks on fields like httpStatus
, code
, and data
. But for MalformedServerResponseException
, we are only checking causeException
.
The original code seems capable of handling this:
MalformedServerResponseException({
required super.routeName,
required super.httpStatus,
required super.data,
this.causeException,
}) : super(message: causeException == null
? GlobalLocalizations.zulipLocalizations
.errorMalformedResponse(httpStatus)
: GlobalLocalizations.zulipLocalizations
.errorMalformedResponseWithCause(
httpStatus, causeException.toString()));
We just need to add the stack trace to the case where causeException
is non-null, and have the assertions here to ensure that both of these fields are non-null or null at the same time.
test/api/core_test.dart
Outdated
try { | ||
throw FormatException("Expected field 'stream_id' to be an integer, but got string 'abc123'"); | ||
} catch (e, st) { | ||
final exception = MalformedServerResponseException( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, it would be ideal for the MalformedServerResponseException
to be thrown by non-test code, as this is the point of having this test in core_test.dart
.
I think an existing test, "malformed API success responses: exception preserves details", has all the setup needed to test this correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing. So should I update the exiting test only to check for the causeStackTrace
as well or need to add a separate test for it?
Updated test can be:
test('malformed API success responses: exception preserves details', () async {
int distinctivelyNamedFromJson(Map<String, dynamic> json) {
throw DistinctiveError("something is wrong");
}
try {
await tryRequest(json: {}, fromJson: distinctivelyNamedFromJson);
assert(false);
} catch (e, st) {
check(e).isA<MalformedServerResponseException>()
..causeException.isA<DistinctiveError>()
..message.contains("something is wrong")
..has((it) => it.causeStackTrace, "causeStackTrace").isNotNull()
..has((it) => it.causeStackTrace.toString(), "causeStackTrace.toString()")
.contains("distinctivelyNamedFromJson");
check(st.toString()).contains("distinctivelyNamedFromJson");
}
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updating the existing test sounds good to me. I would add causeStackTrace
to the …Checks extension of the error, to avoid inlining the has
' here.
7e205d4
to
56e39b8
Compare
Hi @PIG208, I have pushed the revision PTAL, Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the update! Could you also include a screenshot of what an error message would look like with the next revision? Left some comments.
}) : assert((causeException == null && causeStackTrace == null) || | ||
(causeException != null && causeStackTrace != null), | ||
'causeException and causeStackTrace must either both be null or both be non-null'), | ||
super(message: causeException == null | ||
? GlobalLocalizations.zulipLocalizations | ||
.errorMalformedResponse(httpStatus) | ||
: GlobalLocalizations.zulipLocalizations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this still needs to add the stack trace to the error message below.
lib/api/exception.dart
Outdated
|
||
MalformedServerResponseException({ | ||
required super.routeName, | ||
required super.httpStatus, | ||
required super.data, | ||
this.causeException, | ||
}) : super(message: causeException == null | ||
this.causeStackTrace |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: trailing comma
test/api/core_test.dart
Outdated
@@ -425,7 +425,8 @@ void main() { | |||
} catch (e, st) { | |||
check(e).isA<MalformedServerResponseException>() | |||
..causeException.isA<DistinctiveError>() | |||
..message.contains("something is wrong"); | |||
..message.contains("something is wrong") | |||
..causeStackTrace.toString().contains("distinctivelyNamedFromJson"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should check that the stack trace is a part of the error message, so it is visible in the error shown to the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks better now, one nit is that we want to make sure that the string to match against the stack trace is uniquely present in the stack trace. I think
"distinctivelyNamedFromJson" is a bit dubious identifier since the error message might reasonable contain it.
How about just the first line of the actual stack trace without the path, i.e., "#0 main..distinctivelyNamedFromJson"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling toString()
on a Subject
also causes this to not fail when the expected substring is not found, because we intended to call Subject.contains
, but this actually calls String.contains
, which just returns a bool
and does not throw.
Hi @PIG208, I have pushed the revision as well as updates the PR description with the screenshot of error. PTAL, thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the update! I think the test still needs some tweaks, but it looks a lot closer now.
One other note: I think I wasn't clear in my previous #1374 (review) that I was referring to taking a screenshot of the error the user might see while using the app.
test/api/exception_checks.dart
Outdated
@@ -28,4 +28,5 @@ extension Server5xxExceptionChecks on Subject<Server5xxException> { | |||
|
|||
extension MalformedServerResponseExceptionChecks on Subject<MalformedServerResponseException> { | |||
Subject<Object?> get causeException => has((e) => e.causeException, 'causeException'); | |||
Subject<String> get causeStackTrace => has((e) => e.causeStackTrace.toString(), 'causeStackTrace'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type should match the actual property:
Subject<String> get causeStackTrace => has((e) => e.causeStackTrace.toString(), 'causeStackTrace'); | |
Subject<StackTrace?> get causeStackTrace => has((e) => e.causeStackTrace, 'causeStackTrace'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And then, we can have StackTraceChecks
next to ErrorChecks
in test/stdlib_checks.dart
that offers asString
on Subject<StackTrace>
.
test/api/core_test.dart
Outdated
@@ -425,7 +425,8 @@ void main() { | |||
} catch (e, st) { | |||
check(e).isA<MalformedServerResponseException>() | |||
..causeException.isA<DistinctiveError>() | |||
..message.contains("something is wrong"); | |||
..message.contains("something is wrong") | |||
..causeStackTrace.toString().contains("distinctivelyNamedFromJson"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks better now, one nit is that we want to make sure that the string to match against the stack trace is uniquely present in the stack trace. I think
"distinctivelyNamedFromJson" is a bit dubious identifier since the error message might reasonable contain it.
How about just the first line of the actual stack trace without the path, i.e., "#0 main..distinctivelyNamedFromJson"?
test/api/core_test.dart
Outdated
@@ -425,7 +425,8 @@ void main() { | |||
} catch (e, st) { | |||
check(e).isA<MalformedServerResponseException>() | |||
..causeException.isA<DistinctiveError>() | |||
..message.contains("something is wrong"); | |||
..message.contains("something is wrong") | |||
..causeStackTrace.toString().contains("distinctivelyNamedFromJson"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling toString()
on a Subject
also causes this to not fail when the expected substring is not found, because we intended to call Subject.contains
, but this actually calls String.contains
, which just returns a bool
and does not throw.
Should I add a snackbar or dailogbox in the app to show error to the user on the app screen? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the update! Marking this for Greg's review.
@@ -30,6 +30,10 @@ extension ErrorChecks on Subject<Error> { | |||
Subject<String> get asString => has((x) => x.toString(), 'toString'); // TODO(checks): what's a good convention for this? | |||
} | |||
|
|||
extension StackTraceChecks on Subject<StackTrace> { | |||
Subject<String> get asString => has((x) => x.toString(), 'toString'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
Subject<String> get asString => has((x) => x.toString(), 'toString'); | |
Subject<String> get asString => has((x) => x.toString(), 'toString'); // TODO(checks): what's a good convention for this? |
(removed trailing space and copied the comment from the other asString
check)
As @PIG208 said above, I think we shouldn't need to add any new snack bar or dialog in order for the error to get shown to the user — the existing error dialog should do it. But as the issue #1083 says, and as I emphasized in #1374 (comment) at the top of this thread, the point of the issue is about what information we show to the user. So, does this PR cause the stack trace to get shown to the user when they hit one of these errors? Please post a screenshot of what that looks like. (The PR description currently has a screenshot of something shown in your terminal, as a developer. That isn't informative about what the user sees. If it were relevant information, then copy-paste into a code block would be a clearer way to present it than a screenshot.) |
Fixes: #1083
Changes:
causeStackTrace
field toMalformedServerResponseException
toString()
outputTesting:
Screenshot of error