KMP-NativeCoroutines icon indicating copy to clipboard operation
KMP-NativeCoroutines copied to clipboard

Implicit return types combined with some annotations can cause recursion errors

Open lomakk opened this issue 2 years ago • 7 comments

👋 I facing with iOs building issue after integrating the KMP-Native coroutines library. Library version 0.8.0, kotlin 1.5.31

Build stacktrace:stacktrace.txt NetworkProvider.kt:

class NetworkProvider(val storage: SharedStorage) {
    val serverMode: ServerMode
        get() = getServerModeById(storage.serverMode)

    val client = HttpClient {
        expectSuccess = false

        install(Logging) {
            logger = object : Logger {
                override fun log(message: String) {
                }
            }
            level = LogLevel.ALL
        }
        install(JsonFeature) {
            serializer = KotlinxSerializer(json = kotlinx.serialization.json.Json {
                isLenient = false
                ignoreUnknownKeys = true
                allowSpecialFloatingPointValues = true
                useArrayPolymorphism = false
            })
        }
        HttpResponseValidator {
            handleResponseException { cause ->
                throw cause
            }
            validateResponse { response ->
                val statusCode = response.status.value
                val originCall = response.call
                if (statusCode < 300 || originCall.attributes.contains(ValidateMark)) {
                    return@validateResponse
                }

                val exceptionCall = originCall.save().apply {
                    attributes.put(ValidateMark, Unit)
                }

                val exceptionResponse = exceptionCall.response
                val exceptionResponseText = exceptionResponse.readText()

                when (statusCode) {
                    in 300..399 -> throw RedirectNetworkException(
                        exceptionResponse,
                        exceptionResponseText,
                        statusCode
                    )
                    in 400..499 -> throw ClientNetworkException(
                        exceptionResponse,
                        exceptionResponseText,
                        statusCode
                    )
                    in 500..599 -> throw ServerNetworkException(
                        exceptionResponse,
                        exceptionResponseText,
                        statusCode
                    )
                    else -> throw NetworkException(
                        exceptionResponse,
                        exceptionResponseText,
                        statusCode
                    )
                }
            }
        }
    }

    fun HttpRequestBuilder.applyAppHeaders(): HeadersBuilder {
        return headers.apply {
            append(ApiConstantsShared.Api.CONSUMER_KEY_FIELD, serverMode.consumerKey)
            if ([email protected]()) {
                append(ApiConstantsShared.Api.ACCESS_KEY_FIELD, storage.sessionKey)
                append(
                    ApiConstantsShared.Api.COOKIE,
                    "${ApiConstantsShared.Api.SESSION_ID}=${storage.sessionKey}"
                )
            }
        }
    }

    suspend inline fun <reified ResponseData> delete(
        path: String,
        parameters: List<Pair<String, String>> = emptyList()
    ): Response<ResponseData> {
        return try {
            Logger.DEFAULT.log(this.toString())
            val url = serverMode.baseUrl + "/" + path

            Logger.DEFAULT.log("Path: $url")
            Logger.DEFAULT.log("Parameters: $parameters")

            val response = client.delete<ResponseData>(url) {
                parameters.forEach {
                    this.parameter(it.first, it.second)
                }
                headers { applyAppHeaders() }
            }
            Logger.DEFAULT.log("Response: $response")
            Response.Success(response)
        } catch (ex: Exception) {
            ex.message?.let { Logger.DEFAULT.log(it) }
            Response.Error(ex)
        }
    }

    suspend inline fun <reified ResponseData> getItem(
        path: String,
        parameters: List<Pair<String, String>> = emptyList()
    ): Response<ResponseData> {
        return try {
            Logger.DEFAULT.log(this.toString())
            val url = serverMode.baseUrl + "/" + path

            Logger.DEFAULT.log("Path: $url")
            Logger.DEFAULT.log("Parameters: $parameters")

            val response = client.get<ResponseData>(url) {
                parameters.forEach {
                    this.parameter(it.first, it.second)
                }
                headers { applyAppHeaders() }
            }
            Logger.DEFAULT.log("Response: $response")
            Response.Success(response)
        } catch (ex: Exception) {
            ex.message?.let { Logger.DEFAULT.log(it) }
            Response.Error(ex)
        }
    }

    suspend inline fun <reified ResponseData, Body> postItem(
        path: String,
        parameters: List<Pair<String, String>> = emptyList(),
        body: Body? = null
    ): Response<ResponseData> {
        return try {
            Logger.DEFAULT.log(this.toString())
            val url = serverMode.baseUrl + "/" + path

            Logger.DEFAULT.log("Path: $url")
            Logger.DEFAULT.log("Parameters: $parameters")
            Logger.DEFAULT.log("Body: $body")

            val response = client.post<ResponseData>(url) {
                headers { applyAppHeaders() }
                if (parameters.isNotEmpty()) {
                    this.body = FormDataContent(
                        Parameters.build {
                            parameters.forEach {
                                this.append(it.first, it.second)
                            }
                        }
                    )
                } else if (body != null) {
                    header(HttpHeaders.ContentType, ContentType.Application.Json)
                    this.body = body
                }
            }
            Logger.DEFAULT.log("Response: $response")
            return Response.Success(response)
        } catch (ex: Exception) {
            ex.message?.let { Logger.DEFAULT.log(it) }
            Response.Error(ex)
        }
    }

    @OptIn(InternalAPI::class)
    suspend inline fun <reified ResponseData, reified Body> uploadFiles(
        path: String,
        filesData: Map<String, ByteArray>,
        filesJsonData: Body
    ): Response<ResponseData> {
        return try {
            Logger.DEFAULT.log(this.toString())
            val url = serverMode.baseUrl + "/" + path

            Logger.DEFAULT.log("Path: $url")
            Logger.DEFAULT.log("FilesData: $filesJsonData")

            val response = client.post<ResponseData>(url) {
                headers { applyAppHeaders() }
                body = MultiPartFormDataContent(
                    formData {
                        filesData.entries.map { entry ->
                            this.append(
                                key = entry.key,
                                value = entry.value,
                                headers = Headers.build {
                                    append(
                                        HttpHeaders.ContentDisposition,
                                        ApiConstantsShared.UploadImage.FILENAME +
                                                entry.key +
                                                ApiConstantsShared.UploadImage.IMAGE_EXT
                                    )
                                    append(
                                        HttpHeaders.ContentType,
                                        ApiConstantsShared.UploadImage.IMAGE_CONTENT_TYPE
                                    )
                                }
                            )
                        }
                        this.append(
                            key = ApiConstantsShared.UploadImage.PARAM_UPDATE,
                            value = filesJsonData.convertToJsonString(),
                            headers = Headers.build {
                                append(HttpHeaders.ContentType, ContentType.Application.Json)
                            }
                        )
                    }
                )
            }
            Logger.DEFAULT.log("Response: $response")
            Response.Success(response)
        } catch (ex: Exception) {
            ex.printStackTrace()
            ex.message?.let { Logger.DEFAULT.log(it) }
            Response.Error(ex)
        }
    }

    inline fun <reified DataResponse> performDeleteApiMethod(
        path: String,
        params: List<Pair<String, String>> = listOf()
    ): Flow<Response<DataResponse>> {
        return flow {
            val data = [email protected]<DataResponse>(path, params)
            emit(data)
        }
    }

    inline fun <reified DataResponse> performApiMethod(
        path: String,
        params: List<Pair<String, String>> = listOf()
    ): Flow<Response<DataResponse>> {
        return flow {
            val data = [email protected]<DataResponse>(path, params)
            emit(data)
        }
    }

    inline fun <reified DataResponse, Body> performApiMethod(
        path: String,
        params: List<Pair<String, String>> = listOf(),
        body: Body? = null
    ): Flow<Response<DataResponse>> {
        return flow {
            val data = [email protected]<DataResponse, Body>(path, params, body)
            emit(data)
        }
    }

    inline fun <reified DataResponse, reified Body> performApiMethod(
        path: String,
        filesData: Map<String, ByteArray>,
        filesJsonData: Body
    ): Flow<Response<DataResponse>> {
        return flow {
            val data = [email protected]<DataResponse, Body>(
                path,
                filesData,
                filesJsonData
            )
            emit(data)
        }
    }
}

lomakk avatar Nov 25 '21 13:11 lomakk

Hey thanks for the report! Could you try annotating the client property with @NativeCoroutinesIgnore?

rickclephas avatar Nov 25 '21 17:11 rickclephas

I wasn't able to reproduce the issue, so I will be closing this for now. Let me know if you have a reproduction project.

rickclephas avatar Dec 11 '21 12:12 rickclephas

I'm also getting this error on the descriptor field in this code. The code was generated by OpenAPITools. I tried adding @NativeCoroutinesIgnore to that field but it made no difference.

import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesIgnore
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*

@Serializable
class Base64ByteArray(val value: ByteArray) {
    @Serializer(Base64ByteArray::class)
    companion object : KSerializer<Base64ByteArray> {
        @NativeCoroutinesIgnore
        override val descriptor = PrimitiveSerialDescriptor("Base64ByteArray", PrimitiveKind.STRING)
        override fun serialize(encoder: Encoder, obj: Base64ByteArray) = encoder.encodeString(obj.value.encodeBase64())
        override fun deserialize(decoder: Decoder) = Base64ByteArray(decoder.decodeString().decodeBase64Bytes())
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || this::class != other::class) return false
        other as Base64ByteArray
        return value.contentEquals(other.value)
    }

    override fun hashCode(): Int {
        return value.contentHashCode()
    }

    override fun toString(): String {
        return "Base64ByteArray(${hex(value)})"
    }
}

Error message:

expression.kt
PrimitiveKind.STRING
org.jetbrains.kotlin.utils.KotlinExceptionWithAttachments: Exception while analyzing expression at (13,80) in /Users/graham/Work/Brassik/shared/src/commonMain/kotlin/org/openapitools/client/infrastructure/Base64ByteArray.kt

GrahamBorland avatar Dec 16 '21 10:12 GrahamBorland

Thanks @GrahamBorland! I narrowed that issue down to some kind of conflict between KMP-NativeCoroutines and kotlinx.serialization. Will track this in #27.

rickclephas avatar Dec 16 '21 21:12 rickclephas

@rickclephas the issue is still present in version 0.11.1 of your library. I configured a minimal example from the KMM template here: https://github.com/ln-12/NativeCoroutinesTest

Running the command ./gradlew :shared:compileKotlinIosArm64 in the project root directory leads to the mentioned org.jetbrains.kotlin.utils.KotlinExceptionWithAttachments. As soon as I remove the additional annotation @OptIn(ExperimentalUnsignedTypes::class) (here), everything starts to work again.

Could you please have a look at it? Thanks in advance!

ln-12 avatar Jan 14 '22 12:01 ln-12

One thing I also noticed: if you move the OptIn annotation in my example from method to class level, the error disappears. Maybe this is helpful for @lomakk, but this approach can't be used for any annotation obviously.

ln-12 avatar Jan 14 '22 13:01 ln-12

@ln-12 thanks for the reproduction code. I didn't quite figure out why adding the OptIn annotation triggers this error. Note: you'll see a similar error with the @Throws annotation.

However to workaround this error you should explicitly define the type of the something property:

val something: StateFlow<Unit?> = _something.asStateFlow()

rickclephas avatar Jan 15 '22 15:01 rickclephas