google-maps-services-java icon indicating copy to clipboard operation
google-maps-services-java copied to clipboard

Using Ktor results in CallNotFoundException due to work on wrong thread

Open michaeltheshah opened this issue 4 years ago • 4 comments

I'm using Ktor, which is based on coroutines. This seems to be conflicting with the thread manager that the Google Maps Platform Java library uses. It keeps giving me an Unexpected exception from com.google.apphosting.api.ApiProxy$CallNotFoundException: Can't make API call urlfetch.Fetch in a thread that is neither the original request thread nor a thread created by ThreadManager error.

Environment details

  1. Specify the API at the beginning of the title (for example, "Places: ...")
  2. OS type and version
  3. Library version and other environment information

Places Autocomplete Kotlin 1.4 0.11.0

Steps to reproduce

  1. Make a request to PlacesApi.placeAutocomplete(context, query, null).await()

Code example

fun Application.placesAutocomplete() {
    routing {
        get("/v1/places/autocomplete") {
            val parameters = call.request.queryParameters
            val query: String? = parameters["query"]
            val type: String? = parameters["type"]

            if (query == null) {
                call.respondParameterRequired("query")
                return@get
            }

            if (query.isBlank()) {
                call.respond(AutocompleteResponse(emptyList()))
                return@get
            }

            val PLACES_KEY = System.getenv("PLACES_KEY")

            val placeAutocompleteTypes = try {
                type?.let { PlaceAutocompleteType.valueOf(type) }
            } catch (e: IllegalArgumentException) {
                call.respond(HttpStatusCode.BadRequest, "Invalid place type")
                return@get
            }

            val context = GeoApiContext.Builder(GaeRequestHandler.Builder()).apiKey(PLACES_KEY).build()

            val placeAutocompleteRequest = PlacesApi.placeAutocomplete(context, query, null)
            placeAutocompleteTypes?.let { types ->
                placeAutocompleteRequest.types(types)
            }

            try {
                val placeAutocompleteResponse = withContext(Dispatchers.IO) { placeAutocompleteRequest.await() }
                val places = placeAutocompleteResponse.map {
                    val formatting = it.structuredFormatting
                    Place(
                        placeId = it.placeId, mainText = formatting.mainText
                            ?: "", secondaryText = formatting.secondaryText
                            ?: ""
                    )
                }

                val autocompleteResponse = AutocompleteResponse(places)
                call.respond(autocompleteResponse)
            } catch (e: Exception) {
                call.respond(HttpStatusCode.InternalServerError, e)
            }
        }
    }
}

Stack trace

com.google.maps.errors.UnknownErrorException: Unexpected exception from com.google.apphosting.api.ApiProxy$CallNotFoundException: Can't make API call urlfetch.Fetch in a thread that is neither the original request thread nor a thread created by ThreadManager
	at com.google.maps.internal.GaePendingResult.await(GaePendingResult.java:121)
	at com.google.maps.PendingResultBase.await(PendingResultBase.java:58)
	at appengine.PlacesAutocompleteKt$placesAutocomplete$1$1$placeAutocompleteResponse$1.invokeSuspend(PlacesAutocomplete.kt:74)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:272)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:79)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:54)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:36)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at appengine.PlacesAutocompleteKt$placesAutocomplete$1$1.invokeSuspend(PlacesAutocomplete.kt:74)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:175)
	at kotlinx.coroutines.DispatchedTaskKt.resumeUnconfined(DispatchedTask.kt:137)
	at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:108)
	at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:307)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:317)
	at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:249)
	at retrofit2.KotlinExtensions$awaitResponse$2$2.onResponse(KotlinExtensions.kt:93)
	at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:129)
	at okhttp3.RealCall$AsyncCall.execute(RealCall.java:174)
	at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)

michaeltheshah avatar Apr 01 '20 18:04 michaeltheshah

Hi @mochat97, based on the stacktrace you shared, you will need to create the full request object (specifically the GeoApiContext which creates a OkHttpRequestHandler internally) inside the coroutine scope where the request is invoked.

So:

try {
    val placeAutocompleteResponse = withContext(Dispatchers.IO) { 
        val context = GeoApiContext.Builder(GaeRequestHandler.Builder()).apiKey(PLACES_KEY).build()
        val placeAutocompleteRequest = PlacesApi.placeAutocomplete(context, query, null)
        placeAutocompleteTypes?.let { types ->
            placeAutocompleteRequest.types(types)
        }
        placeAutocompleteRequest.await() 
    }

Let me know if this resolves your issue.

arriolac avatar Apr 09 '20 16:04 arriolac

Still doesn't work :(

michaeltheshah avatar Apr 10 '20 03:04 michaeltheshah

I'm not very familiar with Ktor but seems like the only solution here would be to create a custom Dispatcher (instead of Dispatchers.IO) that pulls new threads from ThreadManager.

arriolac avatar Apr 16 '20 00:04 arriolac

As a HTTP GET request is synchronous by nature, do you need to spawn a new coroutine at the point you are in the code causing the issue? I believe the solution would be to implement asynchronous behaviour in your client.

LDuncAndroid avatar Apr 10 '22 01:04 LDuncAndroid