Repository navigation
fix: start a malformed link at the start page; harden sign-in failures and the re-sign-in return page - #597
Merged
Conversation
A web page load on an address whose path or query doesn't decode (such as `#/room/%FF/x`) threw inside go_router before any route, redirect or error page ran, so the splash never cleared, and a reload kept the bad hash. `buildRouter` now computes the start location itself, with go_router's own rule (an empty path is `/`; a platform route of exactly `/` means the initial route) plus a decode check, and passes it with `overridePlatformDefaultLocation: true`. An address that doesn't decode starts at `/`, with one warning that carries only the failure's type. A push after boot (a same-tab hash change, an iOS deep link) is already dropped by the framework's own observer, so it needs no guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`_authenticate` saved the pre-auth state and read the inactivity flag before its `try`, and its callers don't await it, so a storage failure or the `ArgumentError` the `PreAuthState` constructor throws for an unsafe `returnTo` (from a link) left the spinner up with no message. Everything that can throw now sits inside the `try`, and a final catch covers errors as well as exceptions: an `Exception` is logged with `error:`, anything else through `describeFailure`, so a link-supplied `returnTo` never reaches the log. The pre-auth state is cleared through a guarded helper, so a failing store can't throw out of a catch block. The storage-failure tests for the guarded clear need a failing `SharedPreferences` platform store; they wait for an injected pre-auth store (follow-up). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The web engine stores `PlatformDispatcher.onError` and never calls it (flutter/flutter#100277), so on web an unhandled async error reaches only the browser console. `installUncaughtErrorLogging`'s doc and `runSoliplexShell`'s say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`PreAuthStateStorage` was a static class over SharedPreferences, so `ConnectFlow` couldn't be given one that fails, and the tests for its guarded pre-auth clear had no seam short of the plugin's platform store. It is an interface now, with `LocalPreAuthStateStorage` keeping the same bodies and key, provided by `preAuthStateStorageProvider` with that as the default (not exported, so forks see no change). `ConnectFlow` takes it in its constructor and `AuthCallbackScreen` reads it from the provider. Restores the storage-failure tests: a failed save, and the clear failing after a failed save, after an IdP rejection, and after a sign-in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A re-sign-in link carries `?url=` and `returnTo`. `HomeScreen` forwarded `returnTo` on every connect, so after the address was edited, or a retry, a sign-in to another server still returned to the first server's page. `ConnectFlow` takes a `ReturnTarget` (a saved server's id and a path) in its constructor, built from the known server, and saves the path only for a sign-in to that server; it lasts across resets and retries. `isSafeReturnTo` is public and also rejects a path or query that doesn't decode, which would throw when the callback navigates to it. An unsafe target is dropped with a warning that carries no value, and a stored one that no longer passes makes `load` clear the state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… check `isDecodable` holds the path-and-query decode check the router's start guard and `isSafeReturnTo` both run. The callback screen's return check goes through `isSafeReturnTo` instead of its own weaker copy, and logs a rejected value without the value, per the logging rule; the diagnostics risks doc no longer lists `returnTo` among logged attributes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The callback screen cleared the saved pre-auth state before adding the
server, unguarded, so a failing store dropped a sign-in the IdP had
already completed ("Something went wrong"). `ConnectFlow` already treated
the same clear as best-effort; both now use one `clearBestEffort()` on
`PreAuthStateStorage`, which logs a failure and never throws.
The callback's catch-all logs through `describeFailure`: a huge
`expires_in` in the link makes the expiry overflow with a `RangeError`
that carries a value derived from it. No call in that `try` does network
I/O, so no host is lost.
`buildRouter`'s doc says why an undecodable push after boot leaves no
record: the framework's first binding observer drops it before the router
runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the whole-branch review: - An undecodable start address starts at the config's initial route, as a platform route of `/` does, instead of a fixed `/`: a signed-in user lands on the lobby, a landing path is honoured, and a fork with no `/` route has no dead end. `platformStartLocation` returns `null` for a rejected route instead of throwing. - `isSafeReturnTo` also rejects `/auth/callback` (with or without a trailing slash or query), so a link can't make a sign-in return into the callback, and parses with `Uri.tryParse`. - `AuthCallbackScreen` navigates to the stored return page directly: the `PreAuthState` constructor already enforces `isSafeReturnTo`, so its re-check could never reject anything. - `LocalPreAuthStateStorage.load` returns `null` when the store can't be read or the state can't be cleared, as its interface says; `now` is an implementation detail, off the interface. - `_authenticate`'s final catch sends an `Error` through `describeFailure` and keeps `error:` for anything else, which also covers a web storage `DOMException`. Tests pin the callback's rejection of tampered state, HomeScreen's target for unknown and differently written saved addresses, and the storage failures of `load`; comments name reasons, not pointers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A web link whose address can't be decoded opens the app's start page
instead of hanging on the splash screen. A storage failure while starting a
sign-in shows an error instead of a stuck spinner. A re-sign-in's return
page applies only to the server it was issued for. The pre-auth storage is
injected, so its failure paths are tested.
What changed
75534599, refined in77091307).buildRoutercomputes the start location itself, with go_router's ownrule plus a decode check, and passes
overridePlatformDefaultLocation: true.#/room/%FF/x)starts at the config's initial route, with one warning that carries no
part of the address. Before, go_router threw before any route ran and
the splash never cleared, and a reload kept the bad hash.
iOS deep link) is dropped by the framework's own observer, so it needs
no guard. The
buildRouterdoc says why.2143a420,f353c110)._authenticatethat can throw sits inside itstry:the pre-auth save, the inactivity-flag read,
serverIdFromUrl.Erroras well. AnErroris logged throughdescribeFailure; anything else keepserror:.clearBestEffort(). A failingstore can't throw out of a catch block, and on web it can't drop a
sign-in the IdP has already completed.
describeFailure: a hugeexpires_inmakes aRangeErrorthat carries a value derived from thelink.
PreAuthStateStorage(3851598a). It is an interface now,with
LocalPreAuthStateStorageas the default throughpreAuthStateStorageProvider(not exported).ConnectFlowtakes it, andAuthCallbackScreenreads it.ReturnTarget(3897587a,0d3c1247,77091307).ConnectFlowtakes a saved server's id and a path, built byHomeScreenfrom the known server. It saves the path only for asign-in to that server, and keeps it across resets and retries.
isSafeReturnTois public, also rejects a path or query that doesn'tdecode, and rejects
/auth/callback. An unsafe target is dropped with awarning that carries no value. A stored one that no longer passes makes
loadclear the state.gone.
0a5ab198).installUncaughtErrorLoggingandrunSoliplexShellsay thatPlatformDispatcher.onErrorrecords nothingon web (Implement PlatformDispatcher.onError for web flutter/flutter#100277).
Library consumers:
buildRouterstarts at the config's initial routewhen the platform's start address can't be decoded.
Behaviour worth knowing
//path after the host still crashes, as onmain. Flutter's webengine builds its first history URL from
pathnamewithout the origin.Recorded as a follow-up, with an
index.htmlworkaround and an upstreamreport.
That happens when Chrome uses its back/forward cache, so no page load
runs. It doesn't loop, and Cancel works.
mainis the same.lands on the lobby, as before.
Tests
Uri.parserejects each start at the initial route, with a record thatcarries no part of the address. A valid deep link, a platform
/, acallback boot and an unknown route behave as before.
isMarked, or clear (alone, aftera failed save, after an IdP rejection, after a sign-in) ends at an error,
or at
Connectedwhen the server was already added. A reset while acheck fails stays reset. The
ErrorandExceptionarms log asintended.
ReturnTarget: the target is saved only for its own server, keptacross a reset and a retry, and dropped when unsafe, undecodable, or
/auth/callback.written differently.
LocalPreAuthStateStorage.loadreturnsnullwhen the storecan't be read or the state can't be cleared.
behaviour removed.
Test Plan
thread_view_state_testdisposed-signal trace.flutter analyzereports no issues,dart formatis clean,markdownlint is clean, and
flutter build websucceeds.test/coreandtest/modules/auth.types, simplification, security): no Critical or High findings. The
approved findings are fixed in
77091307.afsoc-rag backend with Keycloak, with temporary logging and fault
injection:
index.htmlscript;returnTo=/auth/callbackdropped;?url=ignored;isMarkedfailure.server's link auto-connects; an unknown one is ignored.
🤖 Generated with Claude Code