Skip to content

connectUser() flushes the offline cache on a plain connectivity failure (offlineEnabled=true) #6616

Description

@ktreangen-at-ramsey

Describe the bug

A failed connectUser() (e.g. device is offline) triggers ChatClient.setUser()'s internal .onErrorSuspend { disconnectSuspend(flushPersistence = true) } handler, which wipes the client's offline Room cache via disconnectUserSuspend(flushPersistence = true) -> repositoryFacade.clear() and resets mutableClientState.clearState(). This happens on every failed connectUser() call, including a plain connectivity failure — not just auth errors — so a cold app launch while offline destroys the exact offline cache that ChatClientConfig(offlineEnabled = true) is meant to preserve for a returning, previously-authenticated user.

This directly contradicts the Offline Support guide, which frames flushPersistence = true as something a caller explicitly opts into (e.g. on logout), not a side effect of a failed reconnect — and the Handling User Connection guide, which explicitly says: "Make sure your implementation handles a missing network without recording it as a fatal error" — implying a connectUser() failure due to missing network is an expected, non-fatal occurrence, yet its side effect (wiping the offline cache) isn't documented anywhere.

SDK version

  • 7.6.0 and 7.7.0 (confirmed on both by reading the actual stream-chat-android-client sources for each version; 7.7.0's changelog doesn't mention a related fix, and re-testing on-device against 7.7.0 reproduces the same behavior)

To Reproduce

  1. Sign in successfully with ChatClient.Builder(apiKey, context).config(ChatClientConfig(offlineEnabled = true)).build(), connectUser(...), and browse channels while online so the offline cache is populated.
  2. Fully close the app (kill the process).
  3. Put the device in airplane mode / disconnect all networks.
  4. Cold-launch the app again and call connectUser(...) for the same user with no timeoutMilliseconds (or any value) — it resolves as Result.Failure since the socket can't connect.
  5. Observe: ChannelsScreen/ChannelListViewModel for this client now has an empty/perpetually-loading channel list, getCurrentUser() returns null, and the client's offline database has been cleared — even though step 1 successfully cached channels for this exact user.

Traced root cause (in io.getstream.chat.android.client.ChatClient, 7.6.0/7.7.0 sources):

private suspend fun setUser(...): Result<ConnectionData> {
    ...
    return when {
        ...
        userState is UserState.NotSet -> {
            mutableClientState.setUser(user)
            initializeClientWithUser(user, cacheableTokenProvider, isAnonymous)
            userStateService.onSetUser(user, isAnonymous)
            chatSocket.connectUser(user, isAnonymous)
            mutableClientState.setInitializationState(InitializationState.COMPLETE)
            waitFirstConnection(timeoutMilliseconds) // <- resolves Result.Failure offline, no timeout given
        }
        ...
    }.onErrorSuspend {
        disconnectSuspend(flushPersistence = true) // <- runs on ANY Result.Failure, including pure connectivity failure
    }
}

private suspend fun disconnectUserSuspend(flushPersistence: Boolean) {
    ...
    if (flushPersistence) {
        repositoryFacade.clear()       // wipes the offline Room cache
        userCredentialStorage.clear()
    }
    ...
    mutableClientState.clearState()    // getCurrentUser() becomes null again
    ...
}

Expected behavior

A connectUser() failure caused purely by lack of connectivity should not flush the offline persistence layer or clear the client's local user state — the whole point of offlineEnabled = true is that a previously-authenticated user can browse cached channels/messages without a live connection. At minimum, this destructive side effect should be documented, or ideally scoped to real auth/credential errors rather than every Result.Failure (including plain network-unreachable cases).

Workaround we're using in the meantime

Checking device connectivity (our own ConnectivityManager-backed monitor) before calling connectUser() at all, and skipping the call entirely when known-offline — this avoids triggering the destructive path, at the cost of not restoring the previously-connected user's session (and thus not showing cached channels) on that specific cold-offline launch.

Additional context

  • Related: Switching user cause ChannelScreen goes in an infinite loop and no chats are shown #6009 ("Switching user cause ChannelScreen goes in an infinite loop and no chats are shown") describes the same downstream symptom (ChannelsScreen stuck/empty after a disconnect(flushPersistence = true)), triggered by an explicit switchUser() call rather than an implicit connectivity failure — but the underlying "post-flush, channel list never recovers" behavior looks like the same class of issue.
  • Happy to provide a minimal repro project if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions