Retrofit: BehaviorDelegate fails for suspend functions

Created on 5 Jul 2019  Â·  14Comments  Â·  Source: square/retrofit

retrofit-mock's BehaviorDelegate throws an exception when attempting to return for a suspend function.

For example

interface SomeApi {
  @GET("foo")
  suspend fun getFoo(): Foo
}

will throw when attempting to use:

val delegate: BehaviorDelegate<SomeApi> = mockRetrofit.create(SomeApi::class.java)
val foo: Foo = delegate.returningResponse(Foo()).getFoo()

I'd submit a failing test for this, but it would involve me pulling in Kotlin into the retrofit-mock module which I'm not sure is desirable?

Exception thrown looks like:

java.lang.IllegalArgumentException: Could not locate call adapter for class java.lang.Object.
 Tried:
  * retrofit2.CompletableFutureCallAdapterFactory
  * retrofit2.DefaultCallAdapterFactory
   at retrofit2.Retrofit.nextCallAdapter(Retrofit.java:241)
   at retrofit2.Retrofit.callAdapter(Retrofit.java:205)
   at retrofit2.mock.BehaviorDelegate$1.invoke(BehaviorDelegate.java:64)
   at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
   at $Proxy0.getGames(Unknown Source)
   at au.com.gridstone.debugdrawer.sampleapp.MockGamesApi.getGames(MockGamesApi.kt:17)
   at au.com.gridstone.debugdrawer.sampleapp.GamesViewModel$refresh$1.invokeSuspend(GamesViewModel.kt:43)
   at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
   at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
   at android.os.Handler.handleCallback(Handler.java:883)
   at android.os.Handler.dispatchMessage(Handler.java:100)
   at android.os.Looper.loop(Looper.java:214)
   at android.app.ActivityThread.main(ActivityThread.java:7319)
   at java.lang.reflect.Method.invoke(Native Method)
   at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:934)
Bug

Most helpful comment

Yep this needs to be special-cased the same way it was inside regular Retrofit.

All 14 comments

I'm facing the same issue and created a test for it in my fork. I'm still investigating it to figure out what is happening

I'm facing the same issue here and can not mock anything with coroutines.
I think MockRetrofit is not updated to work with coroutines like Retrofit...

Yep this needs to be special-cased the same way it was inside regular Retrofit.

Should be great to add the Retrofit.create() extension to MockRetrofit too.

@JakeWharton is there any place to check the advances in this issue? Maybe some sonatype snapshot?

Updates will occur on this issue. For now, it remains a bug.

On Sun, Sep 15, 2019, 7:43 PM Javier Segovia Córdoba <
[email protected]> wrote:

@JakeWharton https://github.com/JakeWharton is there any place to check
the advances in this issue? Maybe some sonatype snapshot?

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/square/retrofit/issues/3148?email_source=notifications&email_token=AAAQIEIO3ATGAU6HQKMSPTDQJ3CDXA5CNFSM4H6KDRIKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD6X3QSA#issuecomment-531609672,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAAQIEJZF3X6KQUMWVTQSGTQJ3CDXANCNFSM4H6KDRIA
.

Have same issue for regular retrofit without any retrofit-mock's

    @GET("url")
    suspend fun getClient(@Header("email") email: String): ClientPayload
    for method Api.getClient
        at retrofit2.Utils.methodError(Utils.java:52)
        at retrofit2.HttpServiceMethod.createCallAdapter(HttpServiceMethod.java:105)
        at retrofit2.HttpServiceMethod.parseAnnotations(HttpServiceMethod.java:66)
        at retrofit2.ServiceMethod.parseAnnotations(ServiceMethod.java:37)
        at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:170)
        at retrofit2.Retrofit$1.invoke(Retrofit.java:149)
        at java.lang.reflect.Proxy.invoke(Proxy.java:397)
        at $Proxy0.getClient(Unknown Source)
        at com..android.data.ClientRegistrationRepository.getClient(ClientRegistrationRepository.kt:23)
        at com..android.domain.client.ClientNameUseCase$updateClientNameSettings$2.invokeSuspend(ClientNameUseCase.kt:21)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:740)
     Caused by: java.lang.IllegalArgumentException: Could not locate call adapter for class java.lang.Object.
      Tried:
       * retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
       * retrofit2.DefaultCallAdapterFactory
        at retrofit2.Retrofit.nextCallAdapter(Retrofit.java:241)
        at retrofit2.Retrofit.callAdapter(Retrofit.java:205)
        at retrofit2.HttpServiceMethod.createCallAdapter(HttpServiceMethod.java:103)
            ... 13 more

I am having the same problem as well without retrofit-mock.

    @GET("url")
    suspend fun getItem(): SomeResponse
   java.lang.IllegalArgumentException: Unable to create call adapter for class java.lang.Object
        for method SomeService.getItem
        at retrofit2.ServiceMethod$Builder.methodError(ServiceMethod.java:755)
        at retrofit2.ServiceMethod$Builder.createCallAdapter(ServiceMethod.java:240)
        at retrofit2.ServiceMethod$Builder.build(ServiceMethod.java:165)
        at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:170)
        at retrofit2.Retrofit$1.invoke(Retrofit.java:147)
        at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
        at $Proxy3.getItem(Unknown Source)
        at ...
        at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:91)
        at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:186)
        at ...
        at ...
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.ResumeModeKt.resumeUninterceptedMode(ResumeMode.kt:45)
        at kotlinx.coroutines.internal.ScopeCoroutine.onCompletionInternal$kotlinx_coroutines_core(Scopes.kt:28)
        at kotlinx.coroutines.JobSupport.completeStateFinalization(JobSupport.kt:305)
        at kotlinx.coroutines.JobSupport.tryFinalizeFinishingState(JobSupport.kt:230)
        at kotlinx.coroutines.JobSupport.tryMakeCompleting(JobSupport.kt:799)
        at kotlinx.coroutines.JobSupport.makeCompletingOnce$kotlinx_coroutines_core(JobSupport.kt:742)
        at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:117)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
        at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:233)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
     Caused by: java.lang.IllegalArgumentException: Could not locate call adapter for class java.lang.Object.
      Tried:
       * com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
       * retrofit2.ExecutorCallAdapterFactory
        at retrofit2.Retrofit.nextCallAdapter(Retrofit.java:241)
        at retrofit2.Retrofit.callAdapter(Retrofit.java:205)
        at retrofit2.ServiceMethod$Builder.createCallAdapter(ServiceMethod.java:238)
            ... 28 more

I was able to reproduce this bug on the latest version of Retrofit (v2.6.2) while I was testing the latest support for coroutines. It would be great to have mock responses working, too.

I'm playing a bit with Retrofit code to implement a fix, but some code related to suspend functions support is package private in Retrofit. @JakeWharton is someone working on a fix? What would be an acceptable solution for this?

No one is working on it. Feel free to add support and send a PR!

On Sat, Oct 26, 2019, 2:31 PM Rafael Toledo notifications@github.com
wrote:

I'm playing a bit with Retrofit code to implement a fix, but some code
related to suspend functions support is package private in Retrofit.
@JakeWharton https://github.com/JakeWharton is someone working on a
fix? What would be an acceptable solution for this?

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/square/retrofit/issues/3148?email_source=notifications&email_token=AAAQIELQAXFG5WGD4RBESE3QQSEJBA5CNFSM4H6KDRIKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOECKOEIQ#issuecomment-546628130,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAAQIEJ4HUEDQGIBDNZK6RTQQSEJBANCNFSM4H6KDRIA
.

@rafaeltoledo We have the blessing. It's up to us. Show me what you've got!

I took a look at the logs and I believe I have found the root cause and special casing that Jake mentioned earlier.

It looks like in HttpServiceMethod we check for requestFactory.isKotlinSuspensionFunction to determine which adapterType to use; i.e. the coroutine Response Type or a Generic Return Type.

The adapterType is then used by a private method called createCallAdapter() to return an adapter appropriate for a couroutine Response or a Generic Response.

https://github.com/square/retrofit/blob/master/retrofit/src/main/java/retrofit2/HttpServiceMethod.java#L43-L66

In the Mock Retrofit BehaviorDelegate, we naively use the Generic Response Type to create a Call Adapter which is why everyone sees java.lang.IllegalArgumentException: Could not locate call adapter for class java.lang.Object in their logs.

https://github.com/square/retrofit/blob/master/retrofit-mock/src/main/java/retrofit2/mock/BehaviorDelegate.java#L57-L61

Rather than expose the private method for createCallAdapter, can we think of a reason why the requestFactory.isKotlinSuspensionFunction is not being hit or cannot be used by Mock Retrofit? This is where someone else's knowledge of the library will come in handy.

@vrickey123 I'm not too familiar with Retrofit-Mock internals too. But I did some experiments, and now my current state is the test failing with java.lang.ClassCastException: class retrofit2.mock.BehaviorCall cannot be cast to class java.lang.String (retrofit2.mock.BehaviorCall is in unnamed module of loader 'app'; java.lang.String is in module java.base of loader 'bootstrap'). You can check the master branch of my fork.

Was this page helpful?
0 / 5 - 0 ratings