square / retrofit

A type-safe HTTP client for Android and the JVM
https://square.github.io/retrofit/
Apache License 2.0
43.01k stars 7.3k forks source link

BehaviorDelegate fails for suspend functions #3148

Closed chris-horner closed 4 years ago

chris-horner commented 5 years ago

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)
rafaeltoledo commented 5 years ago

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

pedrodimoura commented 5 years ago

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...

JakeWharton commented 5 years ago

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

JavierSegoviaCordoba commented 5 years ago

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

JavierSegoviaCordoba commented 5 years ago

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

JakeWharton commented 5 years ago

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

On Sun, Sep 15, 2019, 7:43 PM Javier Segovia Córdoba < notifications@github.com> 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 .

alexfrostdev commented 5 years ago

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
ghost commented 4 years ago

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
vrickey123 commented 4 years ago

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.

rafaeltoledo commented 4 years ago

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?

JakeWharton commented 4 years ago

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 .

vrickey123 commented 4 years ago

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

vrickey123 commented 4 years ago

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.

rafaeltoledo commented 4 years ago

@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.