Search code examples
kotlin-multiplatformktorkmm

Ktor httpclient auth feature not working on IOS


I am developing a KMM project and the authentication works well on the Android app. However when I added the Auth feature in the httpclient (located in the shared.commonMain) the ios app failled at runtime with the following message

Function doesn't have or inherit @Throws annotation and thus exception isn't propagated from Kotlin to Objective-C/Swift as NSError. It is considered unexpected and unhandled instead. Program will be terminated.

This is how I create the httpclient

private val httpclient = HttpClient() {
    engine {
        pipelining = true
        threadsCount = 4
    }
    install(Logging) {
        level = LogLevel.HEADERS
        logger = object : Logger {
            override fun log(message: String) {
                Napier.v(tag = "HTTP Client", message = message)
            }
        }
    }
    install(JsonFeature) {
        val json =  Json { ignoreUnknownKeys = true }
        serializer = KotlinxSerializer(json)
    }
    install(Auth) {
        basic {
            credentials {
                BasicAuthCredentials(username = emailUser, password = passwordUser)
            }
        }
    }
}.also {
    initLogger()
}

Here is the complete code of the Greeting class:

class Greeting {
private var emailUser: String = ""
private var passwordUser: String = ""

private val httpclient = HttpClient() {
    engine {
        pipelining = true
        threadsCount = 4
    }
    install(Logging) {
        level = LogLevel.HEADERS
        logger = object : Logger {
            override fun log(message: String) {
                Napier.v(tag = "HTTP Client", message = message)
            }
        }
    }
    install(JsonFeature) {
        val json =  Json { ignoreUnknownKeys = true }
        serializer = KotlinxSerializer(json)
    }
    install(Auth) {
        basic {
            credentials {
                BasicAuthCredentials(username = emailUser, password = passwordUser)
            }
        }
    }
}.also {
    initLogger()
}

@Throws(Exception::class)
suspend fun getVaccines(): List<Vaccine> {
    return httpclient.get(endpointBase + Vaccine.path)
}

@Throws(Exception::class)
suspend fun loginUser(email: String, password: String): String? {
    emailUser = email
    passwordUser = password
    return httpclient.get(endpointBase + User.path + "/userPage")
}

}

Exception full stacktrace

    Function doesn't have or inherit @Throws annotation and thus exception isn't propagated from Kotlin to Objective-C/Swift as NSError.
It is considered unexpected and unhandled instead. Program will be terminated.
Uncaught Kotlin exception: kotlin.native.concurrent.InvalidMutabilityException: mutation attempt of frozen com.example.vaccinationcertificate_mobileapp.Greeting@3963788
    at 0   iosApp                              0x000000010c1f728f kfun:kotlin.Throwable#<init>(kotlin.String?){} + 95
    at 1   iosApp                              0x000000010c1efbbd kfun:kotlin.Exception#<init>(kotlin.String?){} + 93
    at 2   iosApp                              0x000000010c1efe2d kfun:kotlin.RuntimeException#<init>(kotlin.String?){} + 93
    at 3   iosApp                              0x000000010c2272fd kfun:kotlin.native.concurrent.InvalidMutabilityException#<init>(kotlin.String){} + 93
    at 4   iosApp                              0x000000010c228aff ThrowInvalidMutabilityException + 431
    at 5   iosApp                              0x000000010c3db2c0 MutationCheck + 128
    at 6   iosApp                              0x000000010c151165 kfun:com.example.vaccinationcertificate_mobileapp.Greeting#<init>(){} + 437
    at 7   iosApp                              0x000000010c17c9de objc2kotlin.883 + 142
    at 8   iosApp                              0x000000010c150fa3 $sSo14SharedGreetingCABycfcTO + 19
    at 9   iosApp                              0x000000010c14f0af $sSo14SharedGreetingCABycfC + 31
    at 10  iosApp                              0x000000010c150596 $s6iosApp11ContentViewVACycfC + 38 (/Users/oprisvlad2/projects/VaccinationCertificate/VaccinationCertificate-mobileapp/iosApp/iosApp/ContentView.swift:4:0)
    at 11  iosApp                              0x000000010c14ebf3 $s6iosApp6iOSAppV4bodyQrvgAA11ContentViewVyXEfU_ + 35 (/Users/oprisvlad2/projects/VaccinationCertificate/VaccinationCertificate-mobileapp/iosApp/iosApp/iOSApp.swift:7:4)
    at 12  iosApp                              0x000000010c14eda0 $s6iosApp11ContentViewVIgo_ACIegr_TR + 16
    at 13  iosApp                              0x000000010c14edd1 $s6iosApp11ContentViewVIgo_ACIegr_TRTA + 17
    at 14  SwiftUI                             0x00000001173612cf $s7SwiftUI11WindowGroupV7contentACyxGxyXE_tcfC + 63
    at 15  iosApp                              0x000000010c14eac5 $s6iosApp6iOSAppV4bodyQrvg + 181 (/Users/oprisvlad2/projects/VaccinationCertificate/VaccinationCertificate-mobileapp/iosApp/iosApp/iOSApp.swift:6:3)
    at 16  iosApp                              0x000000010c14ef79 $s6iosApp6iOSAppV7SwiftUI0B0AadEP4body4BodyQzvgTW + 9
    at 17  SwiftUI                             0x0000000116dce845 $s7SwiftUI15AppBodyAccessor33_A363922CEBDF47986D9772B903C8737ALLV06updateD02of7changedyx_SbtF0D0QzyXEfU_TA + 22
    at 18  SwiftUI                             0x0000000117357449 $s7SwiftUI12BodyAccessorPAAE03setC0yy0C0QzyXEFAFyXEfU_ + 34
    at 19  SwiftUI                             0x0000000116dce174 $s7SwiftUI15AppBodyAccessor33_A363922CEBDF47986D9772B903C8737ALLV06updateD02of7changedyx_SbtF + 1310
    at 20  SwiftUI                             0x00000001173575ac $s7SwiftUI10StaticBody33_49D2A32E637CD497C6DE29B8E060A506LLV11updateValueyyF + 161
    at 21  SwiftUI                             0x000000011754055c $s14AttributeGraph0A0VyACyxGqd__c5ValueQyd__RszAA12StatefulRuleRd__lufcADSPyqd__GXEfU_ySv_So11AGAttributeatcyXEfU_ySv_AJtcqd__mcfu_ySv_AJtcfu0_TA + 26
    at 22  AttributeGraph                      0x0000000110585e9b _ZN2AG5Graph11UpdateStack6updateEv + 553
    at 23  AttributeGraph                      0x0000000110586491 _ZN2AG5Graph16update_attributeENS_4data3ptrINS_4NodeEEEj + 411
    at 24  AttributeGraph                      0x000000011058c491 _ZN2AG5Graph20input_value_ref_slowENS_4data3ptrINS_4NodeEEENS_11AttributeIDEjPK15AGSwiftMetadataRhl + 299
    at 25  AttributeGraph                      0x00000001105a2889 AGGraphGetValue + 210
    at 26  SwiftUI                             0x00000001173574d5 $s7SwiftUI10StaticBody33_49D2A32E637CD497C6DE29B8E060A506LLV9container9ContainerQzvg + 67
    at 27  SwiftUI                             0x0000000117357599 $s7SwiftUI10StaticBody33_49D2A32E637CD497C6DE29B8E060A506LLV11updateValueyyF + 142
    at 28  SwiftUI                             0x000000011754055c $s14AttributeGraph0A0VyACyxGqd__c5ValueQyd__RszAA12StatefulRuleRd__lufcADSPyqd__GXEfU_ySv_So11AGAttributeatcyXEfU_ySv_AJtcqd__mcfu_ySv_AJtcfu0_TA + 26
    at 29  AttributeGraph                      0x0000000110585e9b _ZN2AG5Graph11UpdateStack6updateEv + 553

Exact solution:

  1. moved emailUser and passwordUser in commonMain.Platform
expect var emailUser: String  
expect var passwordUser: String
  1. androidMain.Platform
actual var emailUser = "" 
actual var passwordUser = ""
  1. iosMain.Platform
actual var emailUser: String = AtomicReference("").value
actual var passwordUser: String = AtomicReference("").value

Solution

  • You need to check out how kotlin-native concurrent-mutability works

    In short, you can't use any var in your shared code, that may be accessed from different threads. You had to wrap those values with Atomic containers. Replace both emailUser and passwordUser with something like this:

    private val emailUser = Atomic("")
    private val passwordUser = Atomic("")
    

    Also you can use delegated-properties so you don't need to write .value each time

    There's no Atomic declarations for common code, so you have to do you by yourself. In actual for iOS you can use native atomics and for android just make a simple wrapped.

    Good news is that this won't stay for long, as JetBrains is planning to change concurrency model soon before KMP release. But for now we had to deal with it.