diff --git a/.github/workflows/compilation-check.yml b/.github/workflows/compilation-check.yml
index 0bb1691..57bc947 100644
--- a/.github/workflows/compilation-check.yml
+++ b/.github/workflows/compilation-check.yml
@@ -8,7 +8,7 @@ on:
jobs:
build:
- runs-on: macOS-latest
+ runs-on: macOS-11
steps:
- uses: actions/checkout@v1
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index e7b5d6b..7dce356 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -11,7 +11,7 @@ on:
jobs:
publish:
name: Publish library at mavenCentral
- runs-on: macOS-latest
+ runs-on: macOS-11
env:
OSSRH_USER: ${{ secrets.OSSRH_USER }}
OSSRH_KEY: ${{ secrets.OSSRH_KEY }}
diff --git a/README.md b/README.md
index c7070f1..e431a42 100755
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ buildscript {
}
dependencies {
- classpath "dev.icerock.moko:network-generator:0.19.0"
+ classpath "dev.icerock.moko:network-generator:0.20.0"
}
}
@@ -53,9 +53,10 @@ project build.gradle
apply plugin: "dev.icerock.mobile.multiplatform-network-generator"
dependencies {
- commonMainApi("dev.icerock.moko:network:0.19.0")
- commonMainApi("dev.icerock.moko:network-bignum:0.19.0") // kbignum serializer
- commonMainApi("dev.icerock.moko:network-errors:0.19.0") // moko-errors integration
+ commonMainApi("dev.icerock.moko:network:0.20.0")
+ commonMainApi("dev.icerock.moko:network-engine:0.20.0") // configured HttpClientEngine
+ commonMainApi("dev.icerock.moko:network-bignum:0.20.0") // kbignum serializer
+ commonMainApi("dev.icerock.moko:network-errors:0.20.0") // moko-errors integration
}
```
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9e40415..8f3f859 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -19,7 +19,7 @@ mokoResourcesVersion = "0.20.1"
mokoMvvmVersion = "0.12.0"
mokoErrorsVersion = "0.6.0"
mokoTestVersion = "0.6.1"
-mokoNetworkVersion = "0.19.0"
+mokoNetworkVersion = "0.20.0"
# tests
espressoCoreVersion = "3.2.0"
@@ -61,9 +61,6 @@ mokoResources = { module = "dev.icerock.moko:resources", version.ref = "mokoReso
mokoMvvmCore = { module = "dev.icerock.moko:mvvm-core", version.ref = "mokoMvvmVersion" }
mokoMvvmLiveData = { module = "dev.icerock.moko:mvvm-livedata", version.ref = "mokoMvvmVersion" }
mokoErrors = { module = "dev.icerock.moko:errors", version.ref = "mokoErrorsVersion" }
-mokoNetwork = { module = "dev.icerock.moko:network", version.ref = "mokoNetworkVersion" }
-mokoNetworkErrors = { module = "dev.icerock.moko:network-errors", version.ref = "mokoNetworkVersion" }
-mokoNetworkBignum = { module = "dev.icerock.moko:network-bignum", version.ref = "mokoNetworkVersion" }
# tests
espressoCore = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCoreVersion" }
diff --git a/network-engine/build.gradle.kts b/network-engine/build.gradle.kts
new file mode 100644
index 0000000..4920077
--- /dev/null
+++ b/network-engine/build.gradle.kts
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+plugins {
+ id("dev.icerock.moko.gradle.multiplatform.mobile")
+ id("dev.icerock.moko.gradle.detekt")
+ id("dev.icerock.moko.gradle.publication")
+ id("dev.icerock.moko.gradle.stub.javadoc")
+ id("dev.icerock.moko.gradle.tests")
+}
+
+kotlin {
+ jvm()
+
+ sourceSets {
+ val commonMain by getting
+
+ val commonJvmAndroid = create("commonJvmAndroid") {
+ dependsOn(commonMain)
+ dependencies {
+ api(libs.ktorClientOkHttp)
+ }
+ }
+
+ val androidMain by getting {
+ dependsOn(commonJvmAndroid)
+ }
+
+ val jvmMain by getting {
+ dependsOn(commonJvmAndroid)
+ }
+ }
+}
+
+dependencies {
+ commonMainImplementation(libs.coroutines)
+ commonMainApi(projects.network)
+ iosMainApi(libs.ktorClientIos)
+}
diff --git a/network-engine/src/androidMain/AndroidManifest.xml b/network-engine/src/androidMain/AndroidManifest.xml
new file mode 100755
index 0000000..53f792a
--- /dev/null
+++ b/network-engine/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/network/src/commonJvmAndroid/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt b/network-engine/src/commonJvmAndroid/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt
similarity index 100%
rename from network/src/commonJvmAndroid/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt
rename to network-engine/src/commonJvmAndroid/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt
diff --git a/network/src/commonMain/kotlin/dev/icerock/moko/network/HttpClientEngineConfig.kt b/network-engine/src/commonMain/kotlin/dev/icerock/moko/network/HttpClientEngineConfig.kt
similarity index 100%
rename from network/src/commonMain/kotlin/dev/icerock/moko/network/HttpClientEngineConfig.kt
rename to network-engine/src/commonMain/kotlin/dev/icerock/moko/network/HttpClientEngineConfig.kt
diff --git a/network/src/iosMain/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt b/network-engine/src/iosMain/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt
similarity index 74%
rename from network/src/iosMain/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt
rename to network-engine/src/iosMain/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt
index 5e2670b..9661043 100644
--- a/network/src/iosMain/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt
+++ b/network-engine/src/iosMain/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt
@@ -6,8 +6,12 @@ package dev.icerock.moko.network
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.darwin.Darwin
+import io.ktor.client.engine.darwin.DarwinHttpRequestException
actual fun createHttpClientEngine(block: HttpClientEngineConfig.() -> Unit): HttpClientEngine {
+ // configure darwin throwable mapper
+ ThrowableToNSErrorMapper.setup { (it as? DarwinHttpRequestException)?.origin }
+ // configure darwin engine
val config = HttpClientEngineConfig().also(block)
return Darwin.create {
this.configureSession {
diff --git a/network/build.gradle.kts b/network/build.gradle.kts
index 4e03c9f..73a8165 100644
--- a/network/build.gradle.kts
+++ b/network/build.gradle.kts
@@ -19,9 +19,6 @@ kotlin {
val commonJvmAndroid = create("commonJvmAndroid") {
dependsOn(commonMain)
- dependencies {
- api(libs.ktorClientOkHttp)
- }
}
val androidMain by getting {
@@ -44,8 +41,6 @@ dependencies {
commonMainImplementation(libs.coroutines)
commonMainApi(libs.kotlinSerialization)
commonMainApi(libs.ktorClient)
- androidMainApi(libs.ktorClientOkHttp)
- iosMainApi(libs.ktorClientIos)
androidMainImplementation(libs.appCompat)
diff --git a/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/RefreshTokenPlugin.kt b/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/RefreshTokenPlugin.kt
index aadc94f..82d6827 100644
--- a/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/RefreshTokenPlugin.kt
+++ b/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/RefreshTokenPlugin.kt
@@ -16,6 +16,7 @@ import io.ktor.client.statement.request
import io.ktor.http.HttpStatusCode
import io.ktor.util.AttributeKey
import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
class RefreshTokenPlugin(
private val updateTokenHandler: suspend () -> Boolean,
@@ -49,30 +50,28 @@ class RefreshTokenPlugin(
return@intercept
}
- refreshTokenHttpPluginMutex.lock()
+ refreshTokenHttpPluginMutex.withLock {
- // If token of the request isn't actual, then token has already been updated and
- // let's just to try repeat request
- if (!plugin.isCredentialsActual(subject.request)) {
- refreshTokenHttpPluginMutex.unlock()
- val requestBuilder = HttpRequestBuilder().takeFrom(subject.request)
- val result: HttpResponse = scope.request(requestBuilder)
- proceedWith(result)
- return@intercept
- }
+ // If token of the request isn't actual, then token has already been updated and
+ // let's just to try repeat request
+ if (!plugin.isCredentialsActual(subject.request)) {
+ val requestBuilder = HttpRequestBuilder().takeFrom(subject.request)
+ val result: HttpResponse = scope.request(requestBuilder)
+ proceedWith(result)
+ return@intercept
+ }
- // Else if token of the request is actual (same as in the storage), then need to send
- // refresh request.
- if (plugin.updateTokenHandler.invoke()) {
- // If the request refresh was successful, then let's just to try repeat request
- refreshTokenHttpPluginMutex.unlock()
- val requestBuilder = HttpRequestBuilder().takeFrom(subject.request)
- val result: HttpResponse = scope.request(requestBuilder)
- proceedWith(result)
- } else {
- // If the request refresh was unsuccessful
- refreshTokenHttpPluginMutex.unlock()
- proceedWith(subject)
+ // Else if token of the request is actual (same as in the storage), then need to send
+ // refresh request.
+ if (plugin.updateTokenHandler.invoke()) {
+ // If the request refresh was successful, then let's just to try repeat request
+ val requestBuilder = HttpRequestBuilder().takeFrom(subject.request)
+ val result: HttpResponse = scope.request(requestBuilder)
+ proceedWith(result)
+ } else {
+ // If the request refresh was unsuccessful
+ proceedWith(subject)
+ }
}
}
}
diff --git a/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/TokenPlugin.kt b/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/TokenPlugin.kt
index 5a5b2b3..9639369 100644
--- a/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/TokenPlugin.kt
+++ b/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/TokenPlugin.kt
@@ -39,7 +39,7 @@ class TokenPlugin private constructor(
}
}
- interface TokenProvider {
+ fun interface TokenProvider {
fun getToken(): String?
}
}
diff --git a/network/src/commonTest/kotlin/RefreshTokenPluginTest.kt b/network/src/commonTest/kotlin/RefreshTokenPluginTest.kt
index 957db2a..6610d7f 100644
--- a/network/src/commonTest/kotlin/RefreshTokenPluginTest.kt
+++ b/network/src/commonTest/kotlin/RefreshTokenPluginTest.kt
@@ -12,8 +12,9 @@ import io.ktor.client.engine.mock.respondOk
import io.ktor.client.request.get
import io.ktor.client.statement.request
import io.ktor.http.HttpStatusCode
+import io.ktor.utils.io.errors.*
+import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.runBlocking
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -48,6 +49,38 @@ class RefreshTokenPluginTest {
val invalidToken = "123"
val validToken = "124"
val tokenHolder = MutableStateFlow(invalidToken)
+ val client = createMockClient(
+ tokenProvider = { tokenHolder.value },
+ pluginConfig = {
+ this.updateTokenHandler = {
+ tokenHolder.value = validToken
+ true
+ }
+ this.isCredentialsActual = { request ->
+ request.headers[AUTH_HEADER_NAME] == tokenHolder.value
+ }
+ }
+ ) { request ->
+ if (request.headers[AUTH_HEADER_NAME] == invalidToken) {
+ respondError(status = HttpStatusCode.Unauthorized)
+ } else respondOk()
+ }
+
+ val result = runBlocking {
+ client.get("localhost")
+ }
+
+ assertEquals(expected = HttpStatusCode.OK, actual = result.status)
+ assertEquals(expected = validToken, actual = result.request.headers[AUTH_HEADER_NAME])
+ }
+
+ @Test
+ fun `mutex not lock permanently when isCredentialsActual fail`() {
+ val invalidToken = "123"
+ val validToken = "124"
+ val tokenHolder = MutableStateFlow(invalidToken)
+ var isFirstTime = true
+
val client = createMockClient(
tokenProvider = object : TokenPlugin.TokenProvider {
override fun getToken(): String? {
@@ -59,6 +92,61 @@ class RefreshTokenPluginTest {
tokenHolder.value = validToken
true
}
+ this.isCredentialsActual = { request ->
+ with(request.headers[AUTH_HEADER_NAME] == tokenHolder.value) {
+ if (isFirstTime) {
+ isFirstTime = false
+ throw IOException("simulate io Error")
+ }
+ this
+ }
+ }
+ },
+ handler = { request ->
+ if (request.headers[AUTH_HEADER_NAME] == invalidToken) {
+ respondError(status = HttpStatusCode.Unauthorized)
+ } else respondOk()
+ }
+ )
+
+ runCatching {
+ runBlocking {
+ client.get("localhost")
+ }
+ }.onFailure {
+ println("simulate first request fail")
+ }
+
+ val result = runBlocking {
+ client.get("localhost")
+ }
+
+ assertEquals(expected = HttpStatusCode.OK, actual = result.status)
+ assertEquals(expected = validToken, actual = result.request.headers[AUTH_HEADER_NAME])
+ }
+
+ @Test
+ fun `mutex not lock permanently when updateTokenHandler fail`() {
+ val invalidToken = "123"
+ val validToken = "124"
+ val tokenHolder = MutableStateFlow(invalidToken)
+ var isFirstTime = true
+
+ val client = createMockClient(
+ tokenProvider = object : TokenPlugin.TokenProvider {
+ override fun getToken(): String? {
+ return tokenHolder.value
+ }
+ },
+ pluginConfig = {
+ this.updateTokenHandler = {
+ if (isFirstTime) {
+ isFirstTime = false
+ throw IOException("simulate io Error")
+ }
+ tokenHolder.value = validToken
+ true
+ }
this.isCredentialsActual = { request ->
request.headers[AUTH_HEADER_NAME] == tokenHolder.value
}
@@ -70,6 +158,14 @@ class RefreshTokenPluginTest {
}
)
+ runCatching {
+ runBlocking {
+ client.get("localhost")
+ }
+ }.onFailure {
+ println("simulate first request fail")
+ }
+
val result = runBlocking {
client.get("localhost")
}
diff --git a/network/src/commonTest/kotlin/TokenFeatureTest.kt b/network/src/commonTest/kotlin/TokenFeatureTest.kt
index 49a1f94..7d2b5fb 100644
--- a/network/src/commonTest/kotlin/TokenFeatureTest.kt
+++ b/network/src/commonTest/kotlin/TokenFeatureTest.kt
@@ -18,16 +18,11 @@ class TokenFeatureTest {
@Test
fun `token added when exist`() {
val client = createMockClient(
- tokenProvider = object : TokenPlugin.TokenProvider {
- override fun getToken(): String {
- return "mytoken"
- }
- },
- handler = { request ->
- if (request.headers[AUTH_HEADER_NAME] == "mytoken") respondOk()
- else respondBadRequest()
- }
- )
+ tokenProvider = { "mytoken" }
+ ) { request ->
+ if (request.headers[AUTH_HEADER_NAME] == "mytoken") respondOk()
+ else respondBadRequest()
+ }
val result = runBlocking {
client.get("localhost")
@@ -39,16 +34,11 @@ class TokenFeatureTest {
@Test
fun `token not added when not exist`() {
val client = createMockClient(
- tokenProvider = object : TokenPlugin.TokenProvider {
- override fun getToken(): String? {
- return null
- }
- },
- handler = { request ->
- if (request.headers.contains(AUTH_HEADER_NAME).not()) respondOk()
- else respondBadRequest()
- }
- )
+ tokenProvider = { null }
+ ) { request ->
+ if (request.headers.contains(AUTH_HEADER_NAME).not()) respondOk()
+ else respondBadRequest()
+ }
val result = runBlocking {
client.get("localhost")
diff --git a/network/src/iosMain/kotlin/dev/icerock/moko/network/NetworkConnectionError.kt b/network/src/iosMain/kotlin/dev/icerock/moko/network/NetworkConnectionError.kt
index d051375..9263cba 100644
--- a/network/src/iosMain/kotlin/dev/icerock/moko/network/NetworkConnectionError.kt
+++ b/network/src/iosMain/kotlin/dev/icerock/moko/network/NetworkConnectionError.kt
@@ -4,11 +4,24 @@
package dev.icerock.moko.network
-import io.ktor.client.engine.darwin.DarwinHttpRequestException
+import platform.Foundation.NSError
+import platform.Foundation.NSURLErrorCannotConnectToHost
+import platform.Foundation.NSURLErrorCannotFindHost
+import platform.Foundation.NSURLErrorCannotLoadFromNetwork
+import platform.Foundation.NSURLErrorDomain
actual fun Throwable.isNetworkConnectionError(): Boolean {
- return when (this) {
- is DarwinHttpRequestException -> isSSLException().not()
+ val nsError: NSError? = ThrowableToNSErrorMapper(this)
+
+ return when {
+ this.isSSLException().not() -> true
+
+ nsError?.domain == NSURLErrorDomain && nsError?.code in listOf(
+ NSURLErrorCannotConnectToHost,
+ NSURLErrorCannotFindHost,
+ NSURLErrorCannotLoadFromNetwork
+ ) -> true
+
else -> false
}
}
diff --git a/network/src/iosMain/kotlin/dev/icerock/moko/network/ThrowableToNSErrorMapper.kt b/network/src/iosMain/kotlin/dev/icerock/moko/network/ThrowableToNSErrorMapper.kt
new file mode 100644
index 0000000..6e00602
--- /dev/null
+++ b/network/src/iosMain/kotlin/dev/icerock/moko/network/ThrowableToNSErrorMapper.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package dev.icerock.moko.network
+
+import platform.Foundation.NSError
+import kotlin.native.concurrent.AtomicReference
+
+object ThrowableToNSErrorMapper : (Throwable) -> NSError? {
+ private val mapperRef: AtomicReference<((Throwable) -> NSError?)?> = AtomicReference(null)
+
+ override fun invoke(throwable: Throwable): NSError? {
+ return requireNotNull(mapperRef.value) { "please setup ThrowableToNSErrorMapper by call ThrowableToNSErrorMapper.setup() in iosMain or use dev.icerock.moko.network.createHttpClientEngine" }
+ .invoke(throwable)
+ }
+
+ fun setup(block: (Throwable) -> NSError?) {
+ mapperRef.value = block
+ }
+}
diff --git a/network/src/iosMain/kotlin/dev/icerock/moko/network/isSSLException.kt b/network/src/iosMain/kotlin/dev/icerock/moko/network/isSSLException.kt
index 087c057..d15b04e 100644
--- a/network/src/iosMain/kotlin/dev/icerock/moko/network/isSSLException.kt
+++ b/network/src/iosMain/kotlin/dev/icerock/moko/network/isSSLException.kt
@@ -4,9 +4,10 @@
package dev.icerock.moko.network
-import io.ktor.client.engine.darwin.DarwinHttpRequestException
+import platform.Foundation.NSError
import platform.Foundation.NSURLErrorCannotLoadFromNetwork
import platform.Foundation.NSURLErrorClientCertificateRequired
+import platform.Foundation.NSURLErrorDomain
import platform.Foundation.NSURLErrorSecureConnectionFailed
import platform.Foundation.NSURLErrorServerCertificateHasBadDate
import platform.Foundation.NSURLErrorServerCertificateHasUnknownRoot
@@ -24,13 +25,14 @@ private val sslKeys = mapOf(
)
actual fun Throwable.isSSLException(): Boolean {
- val iosHttpException = this as? DarwinHttpRequestException ?: return false
- return sslKeys.keys.contains(
- iosHttpException.origin.code
- )
+ val nsError: NSError = ThrowableToNSErrorMapper(this) ?: return false
+
+ return nsError.domain == NSURLErrorDomain && sslKeys.keys.contains(nsError.code)
}
actual fun Throwable.getSSLExceptionType(): SSLExceptionType? {
- val iosHttpException = this as? DarwinHttpRequestException ?: return null
- return sslKeys[iosHttpException.origin.code]
+ val nsError: NSError = ThrowableToNSErrorMapper(this) ?: return null
+ if (nsError.domain != NSURLErrorDomain) return null
+
+ return sslKeys[nsError.code]
}
diff --git a/sample/mpp-library/build.gradle.kts b/sample/mpp-library/build.gradle.kts
index 0c7de12..370be2d 100644
--- a/sample/mpp-library/build.gradle.kts
+++ b/sample/mpp-library/build.gradle.kts
@@ -25,11 +25,9 @@ dependencies {
commonMainApi(libs.mokoMvvmCore)
commonMainApi(libs.mokoMvvmLiveData)
- commonMainApi(libs.mokoNetwork)
- commonMainApi(libs.mokoNetworkErrors)
- commonMainApi(libs.mokoNetworkBignum)
commonMainApi(projects.network)
+ commonMainApi(projects.networkEngine)
commonMainApi(projects.networkBignum)
commonMainApi(projects.networkErrors)
diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/TestViewModel.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/TestViewModel.kt
index cb0b7e2..64f9454 100644
--- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/TestViewModel.kt
+++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/TestViewModel.kt
@@ -53,8 +53,8 @@ class TestViewModel : ViewModel() {
install(TokenPlugin) {
tokenHeaderName = "Authorization"
- tokenProvider = object : TokenPlugin.TokenProvider {
- override fun getToken(): String = "ed155d0a445e4b4fbd878fe1f3bc1b7f"
+ tokenProvider = TokenPlugin.TokenProvider {
+ "ed155d0a445e4b4fbd878fe1f3bc1b7f"
}
}
}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 06cb6c1..4867c11 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -17,6 +17,7 @@ includeBuild("network-generator")
include(":network")
include(":network-errors")
include(":network-bignum")
+include(":network-engine")
include(":sample:android-app")
include(":sample:websocket-echo-server")