Skip to content

java.lang.ClassCastException: class kotlin.Result cannot be cast to class java.lang.String (kotlin.Result is in unnamed module of loader 'app'; java.lang.String is in module java.base of loader 'bootstrap') #3403

@papo2608

Description

@papo2608

With following sample code I get above error when kotlin.Result is used with a suspend function and a runtime exception is thrown in the Retrofit call factory - this works though with e.g. sealed or data classes.

class CrashingResultCallAdapterTest {

  @get:Rule val server = MockWebServer()

  @Test
  fun crash(): Unit = runBlocking {
    val retrofit = Retrofit.Builder()
      .baseUrl(server.url("/"))
      .addConverterFactory(StringConverterFactory())
      .addCallAdapterFactory(SuspendResultCallAdapterFactory())
      .callFactory { throw IllegalStateException("some error") }
      .build()
    val service = retrofit.create(ResultService::class.java)
    server.enqueue(
      MockResponse()
        .setResponseCode(200)
        .setHeader("content-type", "text/plain")
        .setBody("bar")
    )

    assertThat(service.fooBarResult().getOrThrow()).isEqualTo("bar")
  }

  @Test
  fun success(): Unit = runBlocking {
    val retrofit = Retrofit.Builder()
      .baseUrl(server.url("/"))
      .addConverterFactory(StringConverterFactory())
      .addCallAdapterFactory(SuspendResultCallAdapterFactory())
      .build()
    val service = retrofit.create(ResultService::class.java)
    server.enqueue(
      MockResponse()
        .setResponseCode(200)
        .setHeader("content-type", "text/plain")
        .setBody("bar")
    )

    assertThat(service.fooBarResult().getOrThrow()).isEqualTo("bar")

    server.enqueue(
      MockResponse()
        .setResponseCode(404)
        .setHeader("content-type", "text/plain")
        .setBody("barerror")
    )

    assertThat(service.fooBarResult().exceptionOrNull()).isInstanceOf(HttpException::class.java)

    server.enqueue(MockResponse().setSocketPolicy(DISCONNECT_AFTER_REQUEST))

    assertThat(service.fooBarResult().exceptionOrNull()).isInstanceOf(SocketTimeoutException::class.java)
  }

  private interface ResultService {

    @GET("/")
    suspend fun fooBarResult(): Result<String>

  }

  private class StringConverterFactory : Converter.Factory() {

    override fun requestBodyConverter(
      type: Type, parameterAnnotations: Array<out Annotation>, methodAnnotations: Array<out Annotation>,
      retrofit: Retrofit
    ): Converter<*, RequestBody> {
      return Converter<String, RequestBody> { value -> value.toRequestBody("text/plain".toMediaType()) }
    }

    override fun responseBodyConverter(
      type: Type, annotations: Array<out Annotation>, retrofit: Retrofit
    ): Converter<ResponseBody, *> {
      return Converter<ResponseBody, String> { value -> value.string() }
    }

  }

  private class SuspendResultCallAdapterFactory : CallAdapter.Factory() {

    override fun get(
      returnType: Type,
      annotations: Array<out Annotation>,
      retrofit: Retrofit
    ): CallAdapter<*, *>? {

      if (getRawType(returnType) != Call::class.java) return null

      if (returnType !is ParameterizedType) return null

      val resultType: Type = getParameterUpperBound(0, returnType)
      if (getRawType(resultType) != Result::class.java || resultType !is ParameterizedType
      ) return null

      val delegate: CallAdapter<*, *> = retrofit.nextCallAdapter(this, returnType, annotations)

      return CatchingCallAdapter(delegate)
    }

    private class CatchingCallAdapter(
      private val delegate: CallAdapter<*, *>,
    ) : CallAdapter<Any, Call<Result<*>>> {

      override fun responseType(): Type = delegate.responseType()

      override fun adapt(call: Call<Any>): Call<Result<*>> = CatchingCall(call)

    }

    private class CatchingCall(
      private val delegate: Call<Any>,
    ) : Call<Result<*>> {

      override fun enqueue(callback: Callback<Result<*>>) = delegate.enqueue(object : Callback<Any> {
        override fun onResponse(call: Call<Any>, response: Response<Any>) {
          if (response.isSuccessful) {
            val body = response.body()
            callback.onResponse(this@CatchingCall, Response.success(Result.success(body)))
          } else {
            val throwable = HttpException(response)
            callback.onResponse(this@CatchingCall, Response.success(Result.failure<Any>(throwable)))
          }
        }

        override fun onFailure(call: Call<Any>, t: Throwable) {
          callback.onResponse(this@CatchingCall, Response.success(Result.failure<Any>(t)))
        }
      })

      override fun clone(): Call<Result<*>> = CatchingCall(delegate)

      override fun execute(): Response<Result<*>> =
        throw UnsupportedOperationException("Suspend function should not be blocking.")

      override fun isExecuted(): Boolean = delegate.isExecuted

      override fun cancel(): Unit = delegate.cancel()

      override fun isCanceled(): Boolean = delegate.isCanceled

      override fun request(): Request = delegate.request()

      override fun timeout(): Timeout = delegate.timeout()

    }
  }

}

I'm not sure what the root cause here is.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions