Reputation: 402
The App performs a simple sign up (using FirebaseAuth, FirebaseUI & Google Sign In). When authenticated successfully, I take firebaseUser.userId
and use it to fetch user profile from Realtime Database (example location /users/{userId}/someUserDataIsHere
).
In case Realtime Database returns null
object for that userId, it means the user with that userId does not exist in realtime DB, and is signing in for the first time (using Sign up with Google Account), hence a profile should be created (or in other words, user is about to register). In other case, if Firebase db returns user object, the app moves forward to the home screen.
The user profile contains some mandatory data like userId, email, and a name. But also contains some optional data like age, country etc, that could be empty.
The problem is that from time to time, when a user starts the app and the whole authentication process starts, after successful authentication, RealtimeDatabase tries to fetch user profile (for userId provided by FirebaseAuth), but error java.lang.Exception: Client is offline
occurs, returns an empty object so the app "thinks" the user is new and must be inserted in the Realtime Database, and does that (even if it said "Client is offline" like 300ms before)
How it is offline when it authenticated user a few milliseconds before, failed to fetch data for that user from the realtime database (because it is offline??), and managed to write a new profile to the realtime database few ms after?
It does not make a huge problem, because it inserts data to the same userId key (it performs update technically), but remember that I have some optional fields, and those will be reset when this case happens. It is strange from the user's perspective because the user entered some optional fields (for example age) and it disappeared after some time.
I must point out the most usual use case for this:
Some of the dependencies that I use in the app ->
implementation platform('com.google.firebase:firebase-bom:26.4.0')
implementation 'com.google.firebase:firebase-analytics-ktx'
implementation 'com.google.firebase:firebase-auth-ktx'
implementation 'com.google.firebase:firebase-messaging-ktx'
implementation 'com.google.firebase:firebase-database-ktx'
implementation 'com.firebaseui:firebase-ui-auth:6.2.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.1.1'
implementation 'com.google.android.gms:play-services-auth:19.0.0'
Also, using this on app start:
FirebaseDatabase.getInstance().setPersistenceEnabled(false)
And this is the error I get (UPDATED with logs of some other GET request):
2021-01-30 16:12:12.210 9157-9599/com.fourexample.oab D/PersistentConnection: pc_0 - Connection interrupted for: connection_idle
2021-01-30 16:12:12.221 9157-9599/com.fourexample.oab D/Connection: conn_0 - closing realtime connection
2021-01-30 16:12:12.221 9157-9599/com.fourexample.oab D/WebSocket: ws_0 - websocket is being closed
2021-01-30 16:12:12.224 9157-9599/com.fourexample.oab D/PersistentConnection: pc_0 - Got on disconnect due to OTHER
2021-01-30 16:12:12.372 9157-9599/com.fourexample.oab D/WebSocket: ws_0 - closed
2021-01-30 16:13:07.094 9157-9166/com.fourexample.oab I/zygote64: Debugger is no longer active
2021-01-30 16:13:08.682 9157-9599/com.fourexample.oab D/Persistence: Starting transaction.
2021-01-30 16:13:08.687 9157-9599/com.fourexample.oab D/Persistence: Saved new tracked query in 3ms
2021-01-30 16:13:08.705 9157-9599/com.fourexample.oab D/Persistence: Transaction completed. Elapsed: 22ms
2021-01-30 16:13:11.708 9157-9599/com.fourexample.oab D/PersistentConnection: pc_0 - get 1 timed out waiting for connection
2021-01-30 16:13:11.713 9157-9157/com.fourexample.oab I/RepoOperation: get for query /requests/rs falling back to cache after error: Client is offline
2021-01-30 16:13:11.715 9157-9157/com.fourexample.oab D/Persistence: Starting transaction.
2021-01-30 16:13:11.718 9157-9157/com.fourexample.oab D/Persistence: Saved new tracked query in 2ms
2021-01-30 16:13:11.726 9157-9157/com.fourexample.oab D/Persistence: Transaction completed. Elapsed: 9ms
2021-01-30 16:13:11.741 9157-9157/com.fourexample.oab E/RequestService: java.lang.Exception: Client is offline
at com.google.firebase.database.connection.PersistentConnectionImpl$2.run(PersistentConnectionImpl.java:432)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:457)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
at java.lang.Thread.run(Thread.java:764)
Upvotes: 7
Views: 6699
Reputation: 41
I've been running into this same issue when using a coroutine with the IO dispatcher:
private fun performFirstReadOfEntities() {
viewModelScope.launch(ioDispatcher) {
mLoadingState.value = true
try {
Log.d(logTag, "Performing first read of $firebaseDBPath")
dbEntityRef.get()
.addOnSuccessListener { snapshot ->
val snapMap = snapshot!!.value as Map<*, *>
val tempEntityList = mutableListOf<FirebaseEntity>()
for (entry in snapMap) {
Log.d(logTag, "Adding entry with \n\tkey: ${entry.key}\n\tval: ${entry.value.toString()}")
tempEntityList.add(
entityFactory.createFirebaseEntity(
uid = entry.key as String,
map = entry.value as Map<String,Any>))
}
mEntities.value = tempEntityList.sortedBy { it.uid } // So that list and expected list match for emulation testing
Log.d(logTag, "Got ${tempEntityList.size} entities on initial read of database")
dbEntityRef.addChildEventListener(childEventListener) // Add child event listener after
dbListenersInitialized = true
mLoadingState.value = false
clearErrorState()
}
.addOnFailureListener {
setErrorState("Failed to get response from firebase", it)
}
I'm not sure if this is considered "proper" use, but it was working for me, until I got:
error: client is offline
The solution for me was uninstalling the app launcher from the emulated android device, cleaning/rebuilding, and then re-running the test.
Uninstalling the application was what finally did it. Even force closing it didn't do the trick. Maybe there's a firebase issue persisting in the cache and clearing it would have also done the trick? In any case, I'm hoping this is of use to anyone facing the same issue.
Upvotes: 0
Reputation: 11
The error is still occurring randomly (no logical explanation as to why) , it seems when I build the app (mobile flutter) and use the firebase emulator on the first launch the error would occur .
The Solution :
I had to manually uninstall the app and then build it again
Upvotes: 0
Reputation: 37
It wasn't working for me because I didn't specify server Url when getting database Instance. Besides this error I was getting this in console :
Firebase Database connection was forcefully killed by the server. Will not attempt reconnect. Reason: Database lives in a different region. Please change your database URL to
To fix it I just specified server like that:
FirebaseDatabase.getInstance("YOUR_URL_HERE")
"YOUR_URL_HERE" - you can find this in your firebase console, here: screenshot
Upvotes: 0
Reputation: 3348
What worked for me is re-downloading the google-services.json
file, then cleaning and re-building my Android Studio project
Upvotes: 2
Reputation: 874
For me, it ended up being because my google-services.json
was out of date after I had updated some things in Firebase. Particularly it was because my code ended up referencing the default (US-East) region, when I had updated in Firebase to a different one. Try downloading the latest google-services.json
, then making sure you clean
and rebuild
your app!
Upvotes: 3
Reputation: 318
I had an issue with my rules. The message sometime comes up as "Permission denied" and sometimes as "Client is offline" when there is an issue with rules. I fixed my rules and things started working.
Message is misleading at times.
Upvotes: 0
Reputation: 3928
I was facing the same issue and replaced the get().addOnCompleteListener
with addValueEventListener
method. Below is completed code.
Code which throws client is offline issue:
databaseReference.child("pages").child("Help").get().addOnCompleteListener { task: Task<DataSnapshot> ->
if (!task.isSuccessful) {
Log.e("firebase", "Error getting data", task.exception)
} else {
Log.d("firebase",task.result.value.toString())
}
}
Code which solved the above issue:
databaseReference.child("pages").child("Help").addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
Log.e("firebase", "onDataChange ${snapshot.value.toString()}" )
}
override fun onCancelled(error: DatabaseError) {
Log.e("firebase", "onCancelled ${error.message}" )
}
})
Upvotes: 2
Reputation: 402
Okay, there is a bug in Firebase SDK.
Reported/opened issue on GitHub, and they are about to resolve it. Check more on this link
The main problem was usage of suspending functions with get().await() in the following query:
val dataSnapshot = firebaseRoutes.getRequestsReference(countryCode)
.orderByChild("isActive").equalTo(true)
.limitToFirst(20)
.get()
.await()
This would randomly close connection with Realtime Database. I came up with a workaround using extensions until they solve it on their end.
So if you want to use queries and suspending functions, check this extension:
suspend inline fun <reified T> Query.awaitSingleValueEventList(): Flow<FlowDataState<List<T>>> =
callbackFlow {
val valueEventListener = object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
try {
val entityList = mutableListOf<T>()
snapshot.children.forEach { dataSnapshot ->
dataSnapshot.getValue<T>()?.let {
entityList.add(it)
}
}
offer(FlowDataState.Success(entityList))
} catch (e: DatabaseException) {
offer(FlowDataState.Error(e))
}
}
override fun onCancelled(error: DatabaseError) {
offer(FlowDataState.Error(error.toException()))
}
}
addListenerForSingleValueEvent(valueEventListener)
awaitClose { removeEventListener(valueEventListener) }
}
Usage:
suspend fun getActiveRequests(countryCode: String): Flow<FlowDataState<List<RequestEntity>>> {
return firebaseRoutes.getRequestsReference(countryCode)
.orderByChild("isActive").equalTo(true)
.limitToFirst(20)
.awaitSingleValueEventList()
}
FlowDataState is nothing but a wrapper that could be Data or Error
sealed class FlowDataState<out R> {
data class Success<out T>(val data: T) : FlowDataState<T>()
data class Error(val throwable: Throwable) : FlowDataState<Nothing>()
}
Calling this:
service.getActiveRequests(countryCode).collect {
when (it) {
is FlowDataState.Success -> {
// map from entity list to domain model list
// and emit to ViewModel
}
is FlowDataState.Error -> {
// emit error to viewModel
}
}
}
Upvotes: 2