Report authentication and connection failures as such, not as a parse error - #2842
Conversation
dbarashev
left a comment
There was a problem hiding this comment.
Looks good overall. A couple of suggestions though
| * different place than a broken file, so they get their own text; anything else keeps | ||
| * the generic message. | ||
| */ | ||
| static String getReadFailureMessage(Throwable failure) { |
There was a problem hiding this comment.
Add SocketTimeoutException, NoRouteToHostException, and SSLException ?
| static String getReadFailureMessage(Throwable failure) { | ||
| for (Throwable cause = failure; cause != null; cause = cause.getCause()) { | ||
| if (cause instanceof NotAuthorizedException) { | ||
| return "Authentication was rejected by the server"; |
ProxyDocument.read() reported every read failure as "Failed to parse document", including a rejected authentication and an unreachable server. That sends the user looking for a fault in their project file instead of in their credentials. Walk the cause chain and name the two cases which lead to a different next step for the user; everything else keeps the existing text. The walk is guarded against a cause which points at itself.
Review of bardsoftware#2842 asked for SocketTimeoutException, NoRouteToHostException and SSLException. They do not all belong to the same case: * NoRouteToHostException joins UnknownHostException and ConnectException. The server cannot be reached and the user has to check the address or the network. * SocketTimeoutException gets its own text. The server may well be reachable and merely slow, so "waiting and trying again may help" is different advice than "this address does not answer". * SSLException gets its own text too. The server did answer, so this is not a reachability problem at all: the user has to look at the certificate or the protocol settings. The test for the certificate case uses SSLHandshakeException on purpose, so that matching the SSLException base class is covered by the subclass which an untrusted certificate actually produces.
Review of bardsoftware#2842 asked for the messages to be internationalized. They now go through RootLocalizer under the document.error.read prefix, which the bundle already uses for document.error.read.unsupportedFormat: document.error.read.authenticationRejected document.error.read.insecureConnection document.error.read.timedOut document.error.read.serverUnreachable document.error.read.parseFailure The keys are not in biz.ganttproject.app.localization yet, so every lookup keeps the English sentence as a fallback, the way Search.kt does for "No results". Without it the user would be shown the bare message key. An empty value falls back as well: the bundle does contain keys with no value at all, and an error dialog with no text in it says less than an untranslated one. The existing tests keep asserting the English sentences rather than the keys, so they still describe what the user reads. Four more tests cover the lookup: that a present key wins, that a missing or empty one leaves the English text, and that the no-argument entry point really asks RootLocalizer.
|
Both done, thanks. The branch is rebased onto current master and carries three The three exception types. I did not put all three into "The server could
Tell me if you would rather have all three folded into the unreachable-server The test for the certificate case deliberately throws The strings. They now go through So the five messages use the English only — the other languages should come from Crowdin, not from me. On the tests. The ten existing tests still compare the English sentences, |
4111faa to
1400e65
Compare
| * The same, with the localizer passed in, so that a test can see which key is asked for. | ||
| */ | ||
| static String getReadFailureMessage(Throwable failure, Localizer i18n) { | ||
| for (Throwable cause = failure; cause != null; cause = cause.getCause()) { |
There was a problem hiding this comment.
This theoretically can be an infinite loop (if there is a loop in the cause chain). There is Throwables::getCausalChain in Guava that probably deals with such cases.
|
I added the keys from this PR into i18n.properties |
Review of bardsoftware#2842 pointed out that the walk over the cause chain can loop forever when the chain contains a loop. The guard which was there only compared an exception with its own cause, which finds A -> A but not A -> B -> A, and not a loop which the chain enters further down. Measured against the previous commit: a two exception loop and an A -> B -> C -> B chain both had to be killed after 30 seconds, while the self caused chain returned in one. The walk now keeps the exceptions it has already seen in an identity set and stops at the first repeat, which ends a loop of any length and still looks at every exception of the chain, so a transport failure inside a loop keeps its own message. Guava's Throwables::getCausalChain was suggested for this. It does find a loop, using two pointers of different speed, but it answers with IllegalArgumentException("Loop in causal chain detected.") rather than a shortened chain -- read in the sources of guava 31.1-jre, which is what "com.google.guava:guava:31.+" resolves to here. This method runs while a read failure is being reported to the user, so an exception thrown here would replace the failure the user is supposed to read about with a failure of the reporting itself. Catching it and falling back would work too, but the set is shorter than the try/catch and cannot fail. The English fallbacks are gone, now that the keys are in i18n.properties (commit 269851a of biz.ganttproject.app.localization, which master already points at). The five keys each carry exactly the sentence the fallback held. A key which is missing all the same is not answered with an empty text: RootLocalizer.formatText returns the key itself, which is measured, so an error dialog is never blank. The tests which say which failure gets which message now hand in a localizer that answers each of the five keys with a marker of its own, instead of asserting the English sentences. Asserting the sentences would have said nothing about the code once the sentences moved into the bundle, and the markers additionally show that the text comes from the localizer. The two tests which covered the fallback are gone with it, as is the one which checked that a present key wins -- every test does that now. Four tests cover the loops, with a timeout on a separate thread: a same thread timeout is only reported once the test method is over, and a test which never ends would take the whole test run with it.
|
Thank you for adding the keys. I checked all five in the bundle before removing You are right about the loop, and more right than the PR text was. It claimed the I did not take Four tests cover this, each with a timeout on a separate thread, because a One more change in the tests: the ones which say which failure gets which message |
Symptom
Opening a document from a WebDAV server with a wrong password, or from a server
which is down, reports:
The file is fine. The user is sent looking for a corrupt project file when the
actual problem is a password or a network.
Cause
ProxyDocument.read()turns every exception into the same message:
java } catch (Exception e) { throw new DocumentException("Failed to parse document", e); } The cause is preserved, so the information is not lost — it just never reaches
the message the user sees.
Fix
A static helper walks the cause chain and picks a message:
NotAuthorizedExceptionanywhere in the chain → "Authentication was rejectedby the server"
UnknownHostExceptionorConnectException→ "The server could not bereached"
The walk is guarded against an exception which reports itself as its own cause.
Nothing else changes: the same exception type is thrown, with the same cause
attached.
Testing
New
ProxyDocumentReadFailureMessageTest: 10 tests. Four cover the newmessages (including a cause nested several levels down and one at the top of the
chain); the rest pin the unchanged behaviour — a genuinely broken file still
says "Failed to parse document", and a self-referencing cause terminates.
Verified the other way round: with the helper returning the generic message
again, exactly those four fail, each with
expected: <Authentication was rejected by the server> but was: <Failed to parse document>or the connection equivalent. The other six stay green.
A question about wording
The two new strings are hardcoded English, matching the existing
"Failed to parse document" right next to them. If you would rather have them go
through
RootLocalizer, say so and I will add the keys — I did not want tointroduce new translation keys without asking.
This is one of four independent WebDAV fixes sent together; the other three are "Do not send a WebDAV document into the local storage pane when saving", "Pass webdav.lockTimeout to the document, and say so when locking is disabled", and "Keep the WebDAV server details visible on a narrow settings page". They touch disjoint sets of files, so they can be reviewed and merged in any order, or individually.