Skip to content

dataconnect: Fixed occasional NullPointerException leading to erroneous UNAUTHENTICATED exceptions #7001

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 30, 2025

Conversation

dconeybe
Copy link
Contributor

@dconeybe dconeybe commented May 30, 2025

This PR fixes a race condition in Data Connect where DataConnectAuth could register a null token listener before its constructor completed, leading to spurious NullPointerException and downstream UNAUTHENTICATED errors. It introduces an explicit initialize() API to control the setup phase, hooks initialize() into FirebaseDataConnectImpl for both Auth and AppCheck, and augments unit tests to cover initialize and close behaviors.

There is, unfortunately, no workaround for the bug. The only mitigation would be to close the FirebaseDataConnect object, re-open it, and try again. This fix will be included in the release of the firebase-android-sdk in about 4 weeks, near the end of June 2025.

The bug was easy to reproduce: simply add a call to Thread.sleep() in a certain place and, boom. This is a race condition in this code:

init {
// Call `whenAvailable()` on a non-main thread because it accesses SharedPreferences, which
// performs disk i/o, violating the StrictMode policy android.os.strictmode.DiskReadViolation.
val coroutineName = CoroutineName("k6rwgqg9gh $instanceId whenAvailable")
coroutineScope.launch(coroutineName + blockingDispatcher) {
deferredProvider.whenAvailable(DeferredProviderHandlerImpl(weakThis))
}
}

The problem occurred when the DeferredProviderHandlerImpl was called back very quickly and the DataConnectAuth constructor had not yet completed, namely, not having yet assigned idTokenListener (link), and, therefore, registering a null listener, resulting in the NullPointerException.

The problematic exception was:

FirebaseDataConnect     [16.0.2] DataConnectAuth[id=lgrbeqq7n984t] uncaught exception from a coroutine named CoroutineName(k6rwgqg9gh DataConnectAuth[id=lgrbeqq7n984t] whenAvailable):
java.lang.NullPointerException: null reference
	at com.google.android.gms.common.internal.Preconditions.checkNotNull(com.google.android.gms:play-services-basement@@18.5.0:1)
	at com.google.firebase.auth.FirebaseAuth.addIdTokenListener(com.google.firebase:firebase-auth@@23.2.1:365)
	at com.google.firebase.dataconnect.core.DataConnectAuth.addTokenListener(DataConnectAuth.kt:45)
	at com.google.firebase.dataconnect.core.DataConnectAuth.addTokenListener(DataConnectAuth.kt:29)
	at com.google.firebase.dataconnect.core.DataConnectCredentialsTokenManager.onProviderAvailable(DataConnectCredentialsTokenManager.kt:351)
	at com.google.firebase.dataconnect.core.DataConnectCredentialsTokenManager.access$onProviderAvailable(DataConnectCredentialsTokenManager.kt:54)
	at com.google.firebase.dataconnect.core.DataConnectCredentialsTokenManager$DeferredProviderHandlerImpl.handle(DataConnectCredentialsTokenManager.kt:393)
	at com.google.firebase.components.OptionalProvider.whenAvailable(OptionalProvider.java:77)
	at com.google.firebase.dataconnect.core.DataConnectCredentialsTokenManager$1.invokeSuspend(DataConnectCredentialsTokenManager.kt:82)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:101)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1156)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:651)
	at com.google.firebase.concurrent.CustomThreadFactory.lambda$newThread$0$com-google-firebase-concurrent-CustomThreadFactory(CustomThreadFactory.java:47)
	at com.google.firebase.concurrent.CustomThreadFactory$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
	at java.lang.Thread.run(Thread.java:1119)

which would result in an exception later on when a query or mutation was executed:

io.grpc.StatusException: UNAUTHENTICATED: unauthenticated: this operation requires a signed-in user
	at io.grpc.Status.asException(Status.java:548)
	at io.grpc.kotlin.ClientCalls$rpcImpl$1$1$1.onClose(ClientCalls.kt:300)
	at io.grpc.internal.DelayedClientCall$DelayedListener$3.run(DelayedClientCall.java:489)
	at io.grpc.internal.DelayedClientCall$DelayedListener.delayOrExecute(DelayedClientCall.java:453)
	at io.grpc.internal.DelayedClientCall$DelayedListener.onClose(DelayedClientCall.java:486)
	at io.grpc.internal.ClientCallImpl.closeObserver(ClientCallImpl.java:574)
	at io.grpc.internal.ClientCallImpl.access$300(ClientCallImpl.java:72)
	at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1StreamClosed.runInternal(ClientCallImpl.java:742)
	at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1StreamClosed.runInContext(ClientCallImpl.java:723)
	at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:37)
	at io.grpc.internal.SerializingExecutor.run(SerializingExecutor.java:133)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1156)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:651)
	at com.google.firebase.concurrent.CustomThreadFactory.lambda$newThread$0$com-google-firebase-concurrent-CustomThreadFactory(CustomThreadFactory.java:47)
	at com.google.firebase.concurrent.CustomThreadFactory$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
	at java.lang.Thread.run(Thread.java:1119)

Googlers see b/419064737 for full details of the bug, as reported by a customer.

…initialize` function to fix race condition (b/419064737)
Copy link
Contributor

github-actions bot commented May 30, 2025

📝 PRs merging into main branch

Our main branch should always be in a releasable state. If you are working on a larger change, or if you don't want this change to see the light of the day just yet, consider using a feature branch first, and only merge into the main branch when the code complete and ready to be released.

@dconeybe dconeybe requested a review from Copilot May 30, 2025 07:39
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes a race condition in Data Connect where DataConnectAuth could register a null token listener before its constructor completed, leading to spurious NullPointerException and downstream UNAUTHENTICATED errors. It introduces an explicit initialize() API to control the setup phase, hooks initialize() into FirebaseDataConnectImpl for both Auth and AppCheck, and augments unit tests to cover initialize and close behaviors.

  • Extract initialization logic from constructor init block into an explicit initialize() method and update the internal state machine.
  • Automatically invoke initialize() in FirebaseDataConnectImpl for Auth and AppCheck to ensure proper startup order.
  • Add comprehensive tests for initialize(), idempotency, ordering with close(), and integrate initialize() calls into existing tests.

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectAuthUnitTest.kt Added new tests for initialize(), updated existing tests to call initialize() before other methods
firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/FirebaseDataConnectImpl.kt Chained .apply { initialize() } to Auth and AppCheck instances
firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectCredentialsTokenManager.kt Moved init-block logic into initialize(), introduced State.New and State.Initialized enum values and updated state transitions
firebase-dataconnect/CHANGELOG.md Documented the fixed race condition
Comments suppressed due to low confidence (3)

firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectCredentialsTokenManager.kt:84

  • [nitpick] The state name New is quite generic and could be confused with other states. Consider renaming it to Uninitialized to clearly indicate that initialization has not yet occurred.
object New : State<Nothing>

firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectAuthUnitTest.kt:98

  • Consider adding a test to verify that calling getToken() (or other public methods) before initialize() throws the expected IllegalStateException, as described in the initialize() KDoc.
fun `initialize() should succeed if called on a brand new instance`() = runTest {

firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/FirebaseDataConnectImpl.kt:126

  • [nitpick] Automatically calling initialize() during construction changes the lifecycle and startup ordering guarantees for DataConnectAuth. Ensure this side effect is intentional and document the eager initialization behavior.
.apply { initialize() }

@google-oss-bot
Copy link
Contributor

google-oss-bot commented May 30, 2025

Coverage Report 1

Affected Products

  • firebase-dataconnect

    Overall coverage changed from 13.43% (af24598) to 13.69% (19d448d) by +0.26%.

    FilenameBase (af24598)Merge (19d448d)Diff
    DataConnectCredentialsTokenManager.kt30.77%31.41%+0.64%
    FirebaseDataConnectImpl.kt33.59%34.11%+0.51%

Test Logs

  1. https://storage.googleapis.com/firebase-sdk-metric-reports/1c44uQMvVD.html

Copy link
Contributor

github-actions bot commented May 30, 2025

Test Results

   66 files  + 42     66 suites  +42   1m 19s ⏱️ +45s
  559 tests +457    558 ✅ +456  1 💤 +1  0 ❌ ±0 
1 118 runs  +914  1 116 ✅ +912  2 💤 +2  0 ❌ ±0 

Results for commit 4df2df4. ± Comparison against base commit af24598.

This pull request removes 102 and adds 559 tests. Note that renamed tests count towards both.
com.google.firebase.ai.DevAPIStreamingSnapshotTests ‑ citation parsed correctly
com.google.firebase.ai.DevAPIStreamingSnapshotTests ‑ image rejected
com.google.firebase.ai.DevAPIStreamingSnapshotTests ‑ long reply
com.google.firebase.ai.DevAPIStreamingSnapshotTests ‑ prompt blocked for safety
com.google.firebase.ai.DevAPIStreamingSnapshotTests ‑ short reply
com.google.firebase.ai.DevAPIStreamingSnapshotTests ‑ stopped for recitation
com.google.firebase.ai.DevAPIUnarySnapshotTests ‑ citation returns correctly
com.google.firebase.ai.DevAPIUnarySnapshotTests ‑ invalid api key
com.google.firebase.ai.DevAPIUnarySnapshotTests ‑ long reply
com.google.firebase.ai.DevAPIUnarySnapshotTests ‑ response blocked for safety
…
com.google.firebase.dataconnect.AnyValueSerializerUnitTest ‑ descriptor should have expected values
com.google.firebase.dataconnect.AnyValueSerializerUnitTest ‑ deserialize() should throw UnsupportedOperationException
com.google.firebase.dataconnect.AnyValueSerializerUnitTest ‑ serialize() should throw UnsupportedOperationException
com.google.firebase.dataconnect.AnyValueUnitTest ‑ constructor(Boolean) creates an object with the expected value
com.google.firebase.dataconnect.AnyValueUnitTest ‑ constructor(Double) creates an object with the expected value (edge cases)
com.google.firebase.dataconnect.AnyValueUnitTest ‑ constructor(Double) creates an object with the expected value (normal cases)
com.google.firebase.dataconnect.AnyValueUnitTest ‑ constructor(List) creates an object with the expected value (edge cases)
com.google.firebase.dataconnect.AnyValueUnitTest ‑ constructor(List) creates an object with the expected value (normal cases)
com.google.firebase.dataconnect.AnyValueUnitTest ‑ constructor(Map) creates an object with the expected value (edge cases)
com.google.firebase.dataconnect.AnyValueUnitTest ‑ constructor(Map) creates an object with the expected value (normal cases)
…

♻️ This comment has been updated with latest results.

@google-oss-bot
Copy link
Contributor

Size Report 1

Affected Products

  • firebase-dataconnect

    TypeBase (af24598)Merge (19d448d)Diff
    aar703 kB704 kB+1.41 kB (+0.2%)
    apk (release)10.0 MB10.0 MB+1.24 kB (+0.0%)

Test Logs

  1. https://storage.googleapis.com/firebase-sdk-metric-reports/8IGq3TWqvY.html

@dconeybe dconeybe requested a review from aashishpatil-g May 30, 2025 14:33
@dconeybe dconeybe merged commit b2fe881 into main May 30, 2025
47 of 50 checks passed
@dconeybe dconeybe deleted the dconeybe/dataconnect/whenAvailableRaceConditionFix branch May 30, 2025 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants