KMP-NativeCoroutines
KMP-NativeCoroutines copied to clipboard
Implicit return types combined with some annotations can cause recursion errors
👋 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)
}
}
}
Hey thanks for the report!
Could you try annotating the client
property with @NativeCoroutinesIgnore
?
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.
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
Thanks @GrahamBorland! I narrowed that issue down to some kind of conflict between KMP-NativeCoroutines and kotlinx.serialization. Will track this in #27.
@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!
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 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()