Last active
August 19, 2025 14:41
-
-
Save tschuchortdev/29239cbb399bf9b7c4fda2a797cc39fe to your computer and use it in GitHub Desktop.
How to create a retrofit2.Call from an okhttp3.Call
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* 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