feat(gotrue): surface the sign-out reason on the signedOut event#1453
Conversation
…d refresh token When recoverSession restores a persisted session with an invalid or expired refresh token, _executeRefresh already removes the session and emits a signedOut event. The blanket catch in recoverSession then called notifyException a second time, pushing an AuthException onto onAuthStateChange. For listeners without an onError handler this surfaces as an uncaught async error (reported via Sentry). Notification is now explicit per path: the refresh path defers to _callRefreshToken, unexpected parse failures are still notified, and the missing-data and session-expired paths keep their explicit notifications. Closes #930
Returning the _callRefreshToken future without awaiting keeps its error out of recoverSession's catch, so the refresh outcome is owned solely by _executeRefresh (signedOut for an invalid token, an exception for a retryable failure). All other error surfacing in recoverSession (parse errors, signOut failures, the explicit missing-data and session-expired notifications) is preserved, so existing behavior and tests are unchanged.
Calling _callRefreshToken after the try/catch (instead of returning an un-awaited future from inside it) keeps its error out of recoverSession's notification path while satisfying the prefer-return-await lint. Behavior is unchanged: the refresh outcome is owned solely by _executeRefresh.
Add AuthState.exception, populated when the user is signed out involuntarily because the session could not be recovered (an invalid or expired refresh token). It is null for an explicit signOut and for all other events, letting listeners tell the two apart without attaching an onError handler.
Returning the future without awaiting dropped recoverSession from the asynchronous stack trace. Await it (still outside the catch, so the error is not re-notified) so the frame is preserved for debugging.
…resh failure The refresh runs fire-and-forget through a completer, so a failure was rooted at _executeRefresh with no recoverSession or caller frames. Rethrow with the current stack (still outside the catch, so the error is not re-notified) so the call site is visible. Add a test asserting recoverSession appears in the trace.
…ned-out-reason # Conflicts: # packages/gotrue/test/refresh_token_race_test.dart
|
Adding an |
The missing-data and session-expired paths called notifyException explicitly and then threw, and the surrounding catch notified again, so two errors hit onAuthStateChange. Throw without the explicit notify and let the catch be the single notification point. Add a test asserting exactly one error is emitted.
The missing-data and session-expired paths called notifyException explicitly and then threw, and the surrounding catch notified again, so two errors hit onAuthStateChange. Throw without the explicit notify and let the catch be the single notification point. Add a test asserting exactly one error is emitted.
Replaces the `AuthState.exception` field with a typed `SignOutReason` (userInitiated, sessionExpired, sessionMissing) so listeners can tell an explicit sign out apart from an involuntary one without inspecting an exception. The reason is threaded through `notifyAllSubscribers` and set on every local sign-out path; it is still not broadcast across tabs. The involuntary recoverSession/setInitialSession exceptions now also carry stable `session_expired` / `session_missing` codes (added to `ErrorCode`, which is now exported) so the same reason is available on the stream error. Registers the new public symbols in sdk-compliance.yaml.
|
@Vinzent03 I went with an enum anyways, I think it should be enough and it's much cleaner. What do you think? |
# Conflicts: # packages/gotrue/lib/src/gotrue_client.dart # packages/gotrue/test/refresh_token_race_test.dart
|
I like it! Should be much easier to handle for the user and is not too specific to properly maintain for us. |
Re-points the signOutReason symbols to the new canonical auth.session.sign_out_reason capability instead of folding them into subscribe_auth_events.
What
Adds
AuthState.signOutReason, a typedSignOutReasonpopulated on thesignedOutevent so listeners can tell why the user was signed out:userInitiatedfor an explicitsignOut().sessionExpiredwhen the refresh token was rejected (invalid or expired) and the session was removed.sessionMissingwhen a stored session could not be recovered because it was missing required data.It is
nullfor every event other thansignedOut, and alsonullforsignedOutevents received from another tab viaweb.BroadcastChannel.Why
Follow-up to #1450, which stops
recoverSessionfrom double-reporting an expected sign-out as an uncaught stream error. The remaining concern was that a pureonAuthStateChangelistener then seessignedOutwithout knowing why the user was signed out, removing the ability to handle it differently or notify the user.This restores that capability through the event itself, rather than through an error that only reaches listeners with an
onErrorhandler:How
SignOutReasonenum (userInitiated,sessionExpired,sessionMissing), exported fromgotrue.dart.AuthStategains a nullablesignOutReasonfield, threaded throughnotifyAllSubscribers.signOut()now delegates to a private_signOut({scope, reason})so the public API is unchanged while internal callers can pass a reason.recoverSessionandsetInitialSessionsign out with the appropriate reason and now throwAuthExceptions tagged withErrorCode.sessionMissing/ErrorCode.sessionExpired(new error codes).sessionExpired.The reason is intentionally not propagated across tabs via the broadcast channel, so it is always
nullwhenfromBroadcastistrue.Tests
signedOutevent carriesSignOutReason.sessionExpiredon an invalid refresh token.signedOutevent reportsSignOutReason.userInitiatedon an explicitsignOut.