Apollo Kotlin: An unexpected Null Pointer Exception when initializing BatchingHttpInterceptor

Hi All,

While attempting to adopt Query Batching using the Apollo Kotlin version 3.6.2
I’m experiencing a NullPointerException with the following Stacktrace.

pool-21-thread-1
        at com.somrandomapp.android.fd1.w.y0(_Collections.kt:3)
        at com.apollographql.apollo3.network.http.BatchingHttpInterceptor.executePendingRequests(BatchingHttpInterceptor.kt:121)
        at com.apollographql.apollo3.network.http.BatchingHttpInterceptor.access$executePendingRequests(BatchingHttpInterceptor:62)
        at com.apollographql.apollo3.network.http.BatchingHttpInterceptor$1.invokeSuspend(BatchingHttpInterceptor.kt:80)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

My understanding is that this exception is being thrown when attempting to convert the pendingRequests property to an immutable list in line:121 of the BatchingHttpInterceptor.

The property pendingRequests is a non-null property which is initialized during the Initialization of the BatchingHttpInterceptor and it throwing a NPE is bizarre.
So far the only explanation for this behavior could be race condition where in the pendingRequests is being accessed before it is initialized.

Any suggestions that could help explain this behavior would be extremely useful.

Hi @ArjanSM :wave:. This is indeed unexpected :thinking:

I wonder what com.somrandomapp.android.fd1.w.y0 is. Could it be that you ship your own collections?

Hi @mbonnin
I was able to retrace the obfuscated bits from the stacktrace.

at kotlin.collections.CollectionsKt___CollectionsKt.y0(_Collections.kt:3)
        at com.apollographql.apollo3.network.http.BatchingHttpInterceptor.executePendingRequests(BatchingHttpInterceptor.kt:121)
        at com.apollographql.apollo3.network.http.BatchingHttpInterceptor.access$executePendingRequests(BatchingHttpInterceptor:62)
        at com.apollographql.apollo3.network.http.BatchingHttpInterceptor$1.invokeSuspend(BatchingHttpInterceptor.kt:80)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

it appears com.somrandomapp.android.fd1.w.y0 is referring to the kotlin collections package.
Also, the y0 refers to the following:-

// from the mappings.txt
kotlin.collections.CollectionsKt___CollectionsKt -> com.somrandomapp.android.fd1.w:
...
    6:9:java.util.List toList(java.lang.Iterable):1312:1312 -> y0
    10:21:java.util.List toList(java.lang.Iterable):1313:1313 -> y0
    22:26:java.util.List toList(java.lang.Iterable):1316:1316 -> y0
    27:51:java.util.List toList(java.lang.Iterable):1315:1315 -> y0
    52:54:java.util.List toList(java.lang.Iterable):1314:1314 -> y0
    55:63:java.util.List toList(java.lang.Iterable):1319:1319 -> y0

Welp, that’s a mysterious one. If you can send a small reproducer, I’ll look into it, if not, it’s going to be really hard to debug…

Yeah! Reproducing this one is turning out to be a pain.
But if I am able to reproduce it, I’ll surely send it across to you.

1 Like

Hi @mbonnin :wave:
I continue to recreate the exception, in vain, on my Android App.
But I was able to recreate the exception using a sample code I wrote which resembles the BatchingHttpInterceptor.
Here’s the code I wrote:-

fun main(args: Array<String>) {
    Test()
}

class Test{
    private val dispatcher = CloseableSingleThreadDispatcher()
    val scope = CoroutineScope(dispatcher.coroutineDispatcher)

    init {
            val job = scope.launch {
                while(true) {
                    println("In loop!!")
                    delay(10)
                    doSomethingRandom()
                }
            }
        runBlocking {
            job.join()           
        }
        println("Exiting code!!!")
    }

    class BogusInnerClass

    val bogusInnerClasses = mutableListOf<BogusInnerClass>().also {
        println("bogusInnerClassesInitialized!!")
    }

    fun doSomethingRandom(): Int {
        println("# of bogus classes: ${bogusInnerClasses.toList()}") //This is where the exception is thrown
        return -1
    }
}

internal class CloseableSingleThreadDispatcher constructor() : Closeable {
    private var closed = false
    private val _dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()

    val coroutineDispatcher: CoroutineDispatcher
        get() = _dispatcher

    override fun close() {
        if (!closed) {
            _dispatcher.close()
            closed = true
        }
    }
}

This may be a bit of a stretch but shouldn’t the initialization of the bogusInnerClasses take place before the init block?
The thing that confuses me if this were indeed an issue, almost everyone using Query Batching would/should be facing this issue.
But this was the only way of recreating the exception.

Thanks for looking into this :pray:

I disassembled both BatchingHttpInterceptor and Test using javap -v: BatchingHttpInterceptor.dump · GitHub

In the Test case, bogusInnerClasses is initialized (here) after launch {} (here). Especially, since the job is joined, the constructor will pause at this point until bogusInnerClasses is accessed (which crashes because it wasn’t initialized).

In the BatchingHttpInterceptor case, the scenario is different. pendingRequests is initialized in the constructor (here). I can’t figure out how launch {} would be called concurrently with the constructor. Nothing in the constructor calls into intercept()

So I’m not sure what’s going on there. If you’re using R8, might be worth disassembling the dex bytecode to see if it matches the java bytecode. You can do so with apktool

Hi @mbonnin :wave:

We decided instead to bump Apollo Kotlin to v3.8.2.
And so far have not witnessed this issue in it.

1 Like