Skip to content

If Task<AppCheckToken> throws an exception without a message set, Firebase errors internally #7624

@davwheat

Description

@davwheat

[READ] Step 1: Are you in the right place?

Issues filed here should be about bugs in the code in this repository. If you have a general
question, need help debugging, or fall into some other category use one of these other channels:

  • For general technical questions, post a question on StackOverflow
    with the firebase tag.
  • For general Firebase discussion, use the
    firebase-talk google group.
  • For help troubleshooting your application that does not fall under one of the above categories,
    reach out to the personalized Firebase support channel.

[REQUIRED] Step 2: Describe your environment

  • Android Studio version: Otter Feature Drop 2025.2.2 Patch 1
  • Firebase Component: App Check
  • Component version: BOM 34.6.0

[REQUIRED] Step 3: Describe the problem

Steps to reproduce:

  1. Register your own AppCheckProvider
  2. Override getToken() to return a task which throws an Exception with no message
  3. Call FirebaseAppCheck.getInstance().getToken()
  4. Observe that Task throws an exception rather than returning an AppCheckTokenResult with getError() which returns the thrown error

Relevant Code:

@OptIn(ExperimentalCoroutinesApi::class)
fun <T> Deferred<T>.asTask(): Task<T> {
    val cancellation = CancellationTokenSource()
    val source = TaskCompletionSource<T>(cancellation.token)

    invokeOnCompletion callback@{
        if (it is CancellationException) {
            cancellation.cancel()
            return@callback
        }

        val t = getCompletionExceptionOrNull()
        if (t == null) {
            source.setResult(getCompleted())
        } else {
            source.setException(t as? Exception ?: RuntimeExecutionException(t))
        }
    }

    return source.task
}

class TestACP() : AppCheckProvider {
    val scope = CoroutineScope(Dispatchers.IO)

    override fun getToken(): Task<AppCheckToken> {
        return scope.launch {
            // some logic

            throw MyOwnCustomExceptionWhichIsDetailedEnoughThatIDontNeedADetailMessage()
        }.asTask()
    }
}


class TestACPF() : AppCheckProviderFactory {

    override fun create(firebaseApp: FirebaseApp): AppCheckProvider {
        return TestACP()
    }
}

// ...
firebaseAppCheck.installAppCheckProviderFactory(TestACPF())
@Throws(Exception::class)
suspend fun <T> Task<T>.await(): T = suspendCancellableCoroutine { continuation ->
    this.addOnSuccessListener { continuation.resume(it) }
        .addOnFailureListener { continuation.resumeWithException(it) }
        .addOnCanceledListener { continuation.cancel() }
}

try {
  val result = FirebaseAppCheck.getInstance().getToken().await()
} catch (e: Exception) {
  // Exception thrown:
  // java.lang.IllegalArgumentException: Detail message must not be empty
}

This is caused by this code in Firebase:

// If the token exchange failed, return a dummy token for integrators to attach
// in their headers.
return Tasks.forResult(
DefaultAppCheckTokenResult.constructFromError(
new FirebaseException(
appCheckTokenTask.getException().getMessage(),
appCheckTokenTask.getException())));

// If the token exchange failed, return a dummy token for integrators to attach
// in their headers.
return Tasks.forResult(
DefaultAppCheckTokenResult.constructFromError(
new FirebaseException(
appCheckTokenTask.getException().getMessage(),
appCheckTokenTask.getException())));

FirebaseException requires a detail message to be provided, so when this is not provided by the implementation the prerequisite check fails:

    public FirebaseException(@NonNull String detailMessage, @NonNull Throwable cause) {
        Preconditions.checkNotEmpty(detailMessage, "Detail message must not be empty");
        super(detailMessage, cause);
    }

The internal code should fall back to a default message when one is not provided rather than throwing an exception. Of particular note is the method's JavaDoc:

Requests an AppCheckTokenResult from the installed AppCheckFactory. This will always return a successful task, with an AppCheckTokenResult that contains either a valid token, or a dummy token and an error string.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions