Skip to content

Instantly share code, notes, and snippets.

@tschuchortdev
Last active August 19, 2025 14:41
Show Gist options
  • Select an option

  • Save tschuchortdev/29239cbb399bf9b7c4fda2a797cc39fe to your computer and use it in GitHub Desktop.

Select an option

Save tschuchortdev/29239cbb399bf9b7c4fda2a797cc39fe to your computer and use it in GitHub Desktop.
How to create a retrofit2.Call from an okhttp3.Call
/* Example:
val okhttpCall = retrofit.callFactory().newCall(Request.Builder().url(url).build())
@Suppress("UNCHECKED_CAST")
val call = okhttpCall.toRetrofitCall<List<T>>(retrofit, newParameterizedType(List::class.java, itemType))
Alternatively, one can use an ad-hoc Retrofit service to make arbitrary HTTP calls:
private interface RetrofitUrlCaller {
@GET
fun callUrl(@Url url: String): Call<Any>
}
val retrofitUrlCaller = retrofit.create(RetrofitUrlCaller::class.java)
val converter = retrofit.responseBodyConverter<List<T>>(
newParameterizedType(List::class.java, itemType),
emptyArray<Annotation>()
)
@Suppress("UNCHECKED_CAST")
val call = retrofitUrlCaller.callUrl(url.toString()).map { converter.convert(it) as List<T> }
*/
fun newParameterizedType(rawType: Class<*>, vararg typeArguments: Type) =
object : ParameterizedType {
override fun getRawType() = rawType
override fun getActualTypeArguments() = typeArguments
override fun getOwnerType(): Type? = null
}
/** Tests, recursively, whether any of the type parameters associated with type are bound to variables.
* Same implementation is included in Apache commons lang3 TypeUtils.containsTypeVariables
*/
fun containsTypeVariables(type: Type): Boolean = when (type) {
is TypeVariable<*> -> true
is ParameterizedType ->
type.actualTypeArguments.any { containsTypeVariables(it) }
is GenericArrayType -> {
// Check the component type of the array
containsTypeVariables(type.genericComponentType)
}
is WildcardType ->
type.upperBounds.any { containsTypeVariables(it) } ||
type.lowerBounds.any { containsTypeVariables(it) }
is Class<*> -> type.typeParameters.isNotEmpty()
else -> false // Class<*> or other concrete types
}
/**
* @param runtimeType Java runtime type corresponding to the type parameter [T]. This runtime type can be created with
* `typeOf<Foo>().javaType`, but be careful: the runtime type must not contain any non-concrete type variables!
*/
@OptIn(ExperimentalStdlibApi::class)
fun <T> okhttp3.Call.toRetrofitCall(retrofit: Retrofit, runtimeType: Type): retrofit2.Call<T> {
if (containsTypeVariables(runtimeType))
throw IllegalArgumentException(
"The runtimeType argument to Call.toRetrofitCall may not contain any non-concrete type variables due " +
"to a limitation in Kotlin reflect (see https://youtrack.jetbrains.com/issue/KT-39661). " +
"Do not try to replace type variables with Any. It will probably lead to ClassCastExceptions.")
@Suppress("UNCHECKED_CAST")
val callAdapter = retrofit.callAdapter(
newParameterizedType(retrofit2.Call::class.java, runtimeType),
emptyArray<Annotation>()
) as CallAdapter<Any, Call<T>>
return callAdapter.adapt(
RetrofitCallFromOkHttpCall<Any>(retrofit, this, callAdapter.responseType())
)
}
class RetrofitCallFromOkHttpCall<T>(
private val retrofit: Retrofit,
private val okhttpCall: okhttp3.Call,
private val type: Type
) : retrofit2.Call<T?> {
/* Implementation of this class is mostly adapted from retrofit2.HttpServiceMethod and retrofit2.OkHttpCall */
private val converter = retrofit.responseBodyConverter<T>(type, emptyArray<Annotation>())
companion object {
@Throws(IOException::class)
private fun okhttp3.ResponseBody.eager(): okhttp3.ResponseBody {
val buffer = Buffer()
this.source().readAll(buffer)
return buffer.asResponseBody(this.contentType(), this.contentLength())
}
}
private class ExceptionCatchingResponseBody(private val delegate: ResponseBody) : ResponseBody() {
var thrownException: IOException? = null
private val delegateSource: BufferedSource = object : ForwardingSource(delegate.source()) {
@Throws(IOException::class)
override fun read(sink: Buffer, byteCount: Long): Long {
try {
return super.read(sink, byteCount)
} catch (e: IOException) {
thrownException = e
throw e
}
}
}.buffer()
override fun contentType(): MediaType = delegate.contentType()!!
override fun contentLength(): Long = delegate.contentLength()
override fun source(): BufferedSource = delegateSource
override fun close() = delegate.close()
@Throws(IOException::class)
fun throwIfCaught() = thrownException?.let { throw it }
}
private fun parseRawResponse(rawResponse: okhttp3.Response): retrofit2.Response<T?> {
// Remove the body's source (the only stateful object) so we can pass the response along.
val rawResponseWithoutBody = rawResponse
.newBuilder()
.body(object : ResponseBody() {
override fun contentLength(): Long = rawResponse.body!!.contentLength()
override fun contentType(): MediaType? = rawResponse.body!!.contentType()
override fun source(): BufferedSource {
throw IllegalStateException("Cannot read raw response body of a converted body.")
}
})
.build()
if (rawResponse.code !in 200..<300) {
try {
// Buffer the entire body to avoid future I/O.
val bufferedBody = rawResponse.body!!.eager()
return Response.error(bufferedBody, rawResponseWithoutBody)
} finally {
rawResponse.body?.close()
}
}
if (rawResponse.code == 204 || rawResponse.code == 205) {
rawResponse.body?.close()
return Response.success<T?>(null, rawResponseWithoutBody)
}
val catchingBody = ExceptionCatchingResponseBody(rawResponse.body!!)
try {
val body: T? = converter.convert(catchingBody)
return Response.success<T?>(body, rawResponseWithoutBody)
} catch (e: RuntimeException) {
// If the underlying source threw an exception, propagate that rather than indicating it was
// a runtime exception.
catchingBody.throwIfCaught()
throw e
}
}
override fun enqueue(callback: retrofit2.Callback<T?>) {
okhttpCall.enqueue(object : okhttp3.Callback {
override fun onFailure(call: okhttp3.Call, e: IOException) {
callback.onFailure(this@RetrofitCallFromOkHttpCall, e)
}
override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
callback.onResponse(this@RetrofitCallFromOkHttpCall, parseRawResponse(response))
}
})
}
override fun execute(): retrofit2.Response<T?> = parseRawResponse(okhttpCall.execute())
override fun clone() = RetrofitCallFromOkHttpCall<T>(retrofit, okhttpCall.clone(), type)
override fun isExecuted(): Boolean = okhttpCall.isExecuted()
override fun cancel() = okhttpCall.cancel()
override fun isCanceled(): Boolean = okhttpCall.isCanceled()
override fun request(): okhttp3.Request = okhttpCall.request()
override fun timeout(): okio.Timeout = okhttpCall.timeout()
}
fun <I, O> Call<I>.map(mapper: (I) -> O): Call<O> = MappedCall(this, mapper)
class MappedCall<I, O>(private val original: Call<I>, private val map: (I) -> O) : Call<O> {
override fun execute(): Response<O?> {
val responseI = original.execute()
if (!responseI.isSuccessful) {
// Forward failure status
return Response.error(
responseI.errorBody()!!,
responseI.raw()
)
}
val bodyI = responseI.body()
val mappedBody = bodyI?.let { map(it) }
return Response.success(mappedBody, responseI.raw())
}
override fun enqueue(callback: Callback<O>) {
original.enqueue(object : Callback<I> {
override fun onResponse(call: Call<I>, response: Response<I>) {
val bodyI = response.body()
val mappedBody: O? = try {
bodyI?.let { map(it) }
} catch (e: Throwable) {
// Mapping threw an exception—pass it as failure
callback.onFailure(this@MappedCall, e)
return
}
callback.onResponse(this@MappedCall, Response.success(mappedBody, response.raw()))
}
override fun onFailure(call: Call<I>, t: Throwable) {
callback.onFailure(this@MappedCall, t)
}
})
}
override fun isExecuted(): Boolean = original.isExecuted
override fun clone(): Call<O> = MappedCall(original.clone(), map)
override fun isCanceled(): Boolean = original.isCanceled
override fun cancel() = original.cancel()
override fun request() = original.request()
override fun timeout() = original.timeout()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment