Skip to content
Open
1 change: 1 addition & 0 deletions cells/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ kotlin {
implementation(libs.turbine)
// ktor test
implementation(libs.ktor.mock)
implementation(libs.ktor.contentNegotiation)
// mocks
implementation(libs.okio.test)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ internal class CellsApiImpl(
}.mapSuccess { }

override suspend fun getAllTags(): NetworkResponse<List<String>> = wrapCellsResponse {
nodeServiceApi.listNamespaceValues(namespace = TAGS_METADATA, operationValues = listOf())
nodeServiceApi.listNamespaceValues(namespace = TAGS_METADATA)
}.mapSuccess { it.propertyValues ?: emptyList() }

private fun networkError(message: String) =
Expand Down
6 changes: 3 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ compose-material3 = "1.0.0-alpha01"
compose-jetbrains = "1.8.2"
fileKit = "0.10.0-beta04"
android-security = "1.1.0-alpha06"
ktor = "2.3.10"
ktor = "3.2.3"
okio = "3.9.0"
ok-http = "4.12.0"
# 3.0.1 with a fix for a bug https://github.com/mockative/mockative/issues/143 uploaded to a temporary repo
Expand Down Expand Up @@ -45,7 +45,7 @@ core-crypto = "8.0.1"
core-crypto-multiplatform = "0.6.0-rc.3-multiplatform-pre1"
completeKotlin = "1.1.0"
desugar-jdk = "2.1.3"
kermit = "2.0.3"
kermit = "2.0.6"
detekt = "1.23.8"
agp = "8.10.1"
dokka = "2.0.0"
Expand All @@ -71,7 +71,7 @@ jmh = "1.37"
jmhReport = "0.9.6"
xerialDriver = "3.48.0.0"
kotlinx-io = "0.5.3"
cells-sdk = "0.1.1-alpha10"
cells-sdk = "0.1.1-alpha15"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cells-sdk = "0.1.1-alpha15"
cells-sdk = "0.1.1-alpha15.02"

This newer version has artifacts for all apple targets we support. So... no more warnings and we can ditch x64 and use simulatorArm64 instead.


[plugins]
# Home-made convention plugins
Expand Down
25 changes: 14 additions & 11 deletions logic/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,21 @@ kotlin {
}

android {
testOptions.unitTests.isIncludeAndroidResources = true
}
testOptions {
unitTests {
isIncludeAndroidResources = true
all { test ->
test.enabled = true

android {
testOptions.unitTests.all { test ->
// only run tests that are different for the android platform, the rest is covered by the jvm tests
file("src/androidUnitTest/kotlin").let { dir ->
if (dir.exists() && dir.isDirectory) {
dir.walk().forEach {
if (it.isFile && it.extension == "kt") {
it.relativeToOrNull(dir)?.let {
test.include(it.path.removeSuffix(".kt").suffixIfNot("*"))
// only run tests that are different for the android platform, the rest is covered by the jvm tests
file("src/androidUnitTest/kotlin").let { dir ->
if (dir.exists() && dir.isDirectory) {
dir.walk().forEach {
if (it.isFile && it.extension == "kt") {
it.relativeToOrNull(dir)?.let {
test.include(it.path.removeSuffix(".kt").suffixIfNot("*"))
}
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions network/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ kotlin {

// mock engine
implementation(libs.ktor.mock)
implementation(libs.ktor.contentNegotiation)

// KTX
implementation(libs.ktxDateTime)
Expand Down
22 changes: 22 additions & 0 deletions network/consumer-proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,25 @@

# protobuf
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }

# Ktor
-dontwarn io.ktor.client.network.sockets.SocketTimeoutException
-dontwarn io.ktor.client.network.sockets.TimeoutExceptionsCommonKt
-dontwarn io.ktor.client.plugins.HttpTimeout$HttpTimeoutCapabilityConfiguration
-dontwarn io.ktor.client.plugins.HttpTimeout$Plugin
-dontwarn io.ktor.client.plugins.HttpTimeout
-dontwarn io.ktor.util.InternalAPI
-dontwarn io.ktor.utils.io.ByteReadChannelJVMKt
-dontwarn io.ktor.utils.io.CoroutinesKt
-dontwarn io.ktor.utils.io.core.ByteBuffersKt
-dontwarn io.ktor.utils.io.core.BytePacketBuilder
-dontwarn io.ktor.utils.io.core.ByteReadPacket$Companion
-dontwarn io.ktor.utils.io.core.ByteReadPacket
-dontwarn io.ktor.utils.io.core.CloseableJVMKt
-dontwarn io.ktor.utils.io.core.Input
-dontwarn io.ktor.utils.io.core.InputArraysKt
-dontwarn io.ktor.utils.io.core.InputPrimitivesKt
-dontwarn io.ktor.utils.io.core.Output
-dontwarn io.ktor.utils.io.core.OutputPrimitivesKt
-dontwarn io.ktor.utils.io.core.PreviewKt
-dontwarn okhttp3.internal.Util
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,24 @@ package com.wire.kalium.network
import com.wire.kalium.logger.KaliumLogger
import com.wire.kalium.network.utils.obfuscatePath
import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.plugins.HttpClientPlugin
import io.ktor.client.plugins.logging.DEFAULT
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.plugins.observer.ResponseHandler
import io.ktor.client.plugins.observer.ResponseObserver
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.HttpSendPipeline
import io.ktor.client.statement.HttpReceivePipeline
import io.ktor.client.statement.HttpResponsePipeline
import io.ktor.client.statement.readRawBytes
import io.ktor.http.Url
import io.ktor.http.content.OutgoingContent
import io.ktor.http.contentType
import io.ktor.util.AttributeKey
import io.ktor.util.InternalAPI
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.charsets.Charset
import io.ktor.utils.io.core.readText
import io.ktor.utils.io.readRemaining
import kotlin.coroutines.cancellation.CancellationException

private val KaliumHttpCustomLogger = AttributeKey<KaliumHttpLogger>("KaliumHttpLogger")
Expand Down Expand Up @@ -120,7 +118,6 @@ class KaliumKtorCustomLogging private constructor(
}
}

@OptIn(InternalAPI::class)
private fun setupResponseLogging(client: HttpClient) {
client.receivePipeline.intercept(HttpReceivePipeline.State) { response ->
if (level == LogLevel.NONE || response.call.attributes.contains(DisableLogging)) return@intercept
Expand Down Expand Up @@ -157,20 +154,20 @@ class KaliumKtorCustomLogging private constructor(

if (!level.body) return

val observer: ResponseHandler = observer@{
if (level == LogLevel.NONE || it.call.attributes.contains(DisableLogging)) {
return@observer
client.receivePipeline.intercept(HttpReceivePipeline.After) { response ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you test with this logger enabled, i remember using intercept will consume the response stream and later stages will not have anything to read

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I can see the response in logs

if (level == LogLevel.NONE || response.call.attributes.contains(DisableLogging)) {
return@intercept
}

val logger = it.call.attributes[KaliumHttpCustomLogger]
val logger = response.call.attributes[KaliumHttpCustomLogger]

try {
logger.logResponseBody(it.contentType(), it.content)
logger.logResponseBody(response.contentType(), ByteReadChannel(response.readRawBytes()))
} catch (_: Throwable) {
} finally {
logger.closeResponseLog()
}
}
ResponseObserver.install(ResponseObserver(observer), client)
}

private fun logRequest(request: HttpRequestBuilder): OutgoingContent? {
Expand Down Expand Up @@ -213,14 +210,6 @@ class KaliumKtorCustomLogging private constructor(
}
}

/**
* Configure and install [Logging] in [HttpClient].
*/
@Suppress("FunctionNaming")
fun HttpClientConfig<*>.Logging(block: Logging.Config.() -> Unit = {}) {
install(Logging, block)
}

@Suppress("TooGenericExceptionCaught")
internal suspend inline fun ByteReadChannel.tryReadText(charset: Charset): String? = try {
readRemaining().readText(charset = charset)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ package com.wire.kalium.network

import io.ktor.http.content.OutgoingContent
import io.ktor.util.copyToBoth
import io.ktor.utils.io.ByteWriteChannel
import io.ktor.utils.io.close
import io.ktor.utils.io.ByteChannel
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.ByteWriteChannel
import io.ktor.utils.io.writeFully
import io.ktor.utils.io.writer
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope

internal suspend fun OutgoingContent.observe(log: ByteWriteChannel): OutgoingContent = when (this) {
is OutgoingContent.ByteArrayContent -> {
log.writeFully(bytes(), 0, bytes().size)
log.close()
log.flushAndClose()
this
}
is OutgoingContent.ReadChannelContent -> {
Expand All @@ -49,7 +49,7 @@ internal suspend fun OutgoingContent.observe(log: ByteWriteChannel): OutgoingCon
KaliumLoggedContent(this, responseChannel)
}
else -> {
log.close()
log.flushAndClose()
this
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import io.ktor.http.Url
import io.ktor.http.appendPathSegments
import io.ktor.serialization.kotlinx.json.json
import io.ktor.websocket.WebSocketSession
import kotlin.time.Duration.Companion.milliseconds
import io.ktor.client.plugins.websocket.pingInterval

/**
* Provides a [HttpClient] that has all the
Expand Down Expand Up @@ -146,7 +148,7 @@ internal class AuthenticatedWebSocketClient(
install(WebSockets) {
// Depending on the Engine (OkHttp for example), we might
// need to set this value there too, as this here won't work
pingInterval = WEBSOCKET_PING_INTERVAL_MILLIS
pingInterval = WEBSOCKET_PING_INTERVAL_MILLIS.milliseconds
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
package com.wire.kalium.network.api.v0.authenticated

import com.wire.kalium.network.AuthenticatedNetworkClient
import com.wire.kalium.network.api.base.authenticated.asset.AssetApi
import com.wire.kalium.network.api.authenticated.asset.AssetMetadataRequest
import com.wire.kalium.network.api.authenticated.asset.AssetResponse
import com.wire.kalium.network.api.base.authenticated.asset.AssetApi
import com.wire.kalium.network.exceptions.KaliumException
import com.wire.kalium.network.kaliumLogger
import com.wire.kalium.network.utils.NetworkResponse
Expand All @@ -47,6 +47,8 @@ import io.ktor.utils.io.close
import io.ktor.utils.io.core.ByteReadPacket
import io.ktor.utils.io.core.isNotEmpty
import io.ktor.utils.io.core.readBytes
import io.ktor.utils.io.readRemaining
import io.ktor.utils.io.writePacket
import io.ktor.utils.io.writeStringUtf8
import okio.Buffer
import okio.Sink
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.http.HttpStatusCode
import io.ktor.http.Url
import io.ktor.utils.io.CancellationException
import io.ktor.websocket.CloseReason
import io.ktor.websocket.Frame
import io.ktor.websocket.WebSocketSession
import io.ktor.websocket.close
Expand Down Expand Up @@ -149,10 +151,15 @@ internal open class NotificationApiV0 internal constructor(

defaultClientWebSocketSession.incoming
.consumeAsFlow()
.onCompletion {
defaultClientWebSocketSession.close()
logger.w("Websocket Closed", it)
emit(WebSocketEvent.Close(it))
.onCompletion { cause ->
val closeReason = if (cause == null || cause is CancellationException) {
CloseReason(CloseReason.Codes.NORMAL, "Normal closure")
} else {
CloseReason(CloseReason.Codes.INTERNAL_ERROR, "Error: ${cause.message}")
}
defaultClientWebSocketSession.close(closeReason)
logger.w("Websocket Closed", cause)
emit(WebSocketEvent.Close(cause))
}
.collect { frame ->
logger.v("Websocket Received Frame: $frame")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import com.wire.kalium.network.utils.setWSSUrl
import com.wire.kalium.network.utils.wrapKaliumResponse
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.utils.io.CancellationException
import io.ktor.websocket.CloseReason
import io.ktor.websocket.Frame
import io.ktor.websocket.WebSocketSession
import io.ktor.websocket.close
Expand Down Expand Up @@ -118,12 +120,17 @@ internal open class NotificationApiV9 internal constructor(

defaultClientWebSocketSession.incoming
.consumeAsFlow()
.onCompletion {
.onCompletion { cause ->
defaultClientWebSocketSession.close()
logger.w("Websocket Closed", it)
session?.close(it)
logger.w("Websocket Closed", cause)
val closeReason = if (cause == null || cause is CancellationException) {
CloseReason(CloseReason.Codes.NORMAL, "Normal closure")
} else {
CloseReason(CloseReason.Codes.INTERNAL_ERROR, "Error: ${cause.message}")
}
session?.close(closeReason)
session = null
emit(WebSocketEvent.Close(it))
emit(WebSocketEvent.Close(cause))
}
.collect { frame ->
logger.v("Websocket Received Frame: $frame")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ internal class AuthenticatedHttpClientProviderImpl(
}
val newSession = sessionManager.updateToken(
accessTokenApi = accessTokenApi(client),
oldRefreshToken = oldTokens!!.refreshToken
oldRefreshToken = oldTokens!!.refreshToken ?: error("Old refresh token is null!")
)
BearerTokens(
accessToken = newSession.accessToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import io.ktor.http.content.OutgoingContent
import io.ktor.serialization.Configuration
import io.ktor.serialization.ContentConverter
import io.ktor.util.reflect.TypeInfo
import io.ktor.util.toByteArray
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.charsets.Charset
import io.ktor.utils.io.toByteArray

/**
* A ContentConverter which does nothing, it simply passes byte arrays through as they are. This is useful
Expand All @@ -38,7 +38,7 @@ class ByteArrayConverter : ContentConverter {
return content.toByteArray()
}

override suspend fun serializeNullable(contentType: ContentType, charset: Charset, typeInfo: TypeInfo, value: Any?): OutgoingContent? {
override suspend fun serialize(contentType: ContentType, charset: Charset, typeInfo: TypeInfo, value: Any?): OutgoingContent? {
return ByteArrayContent(value as ByteArray, contentType)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ import io.ktor.client.utils.buildHeaders
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpProtocolVersion
import io.ktor.http.HttpStatusCode
import io.ktor.util.InternalAPI
import io.ktor.util.date.GMTDate
import io.ktor.utils.io.ByteReadChannel
import io.mockative.Mockable
import kotlin.coroutines.CoroutineContext
import io.ktor.utils.io.InternalAPI
Copy link

Copilot AI Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using @InternalAPI annotation suggests this code relies on internal Ktor APIs that may change in future versions. Consider finding a stable public API alternative or document this dependency risk.

Suggested change
import io.ktor.utils.io.InternalAPI

Copilot uses AI. Check for mistakes.

@Mockable
interface SessionManager {
Expand Down Expand Up @@ -83,7 +83,9 @@ private fun HttpClient.addWWWAuthenticateHeaderIfNeeded() {
override val version: HttpProtocolVersion = response.version
override val requestTime: GMTDate = response.requestTime
override val responseTime: GMTDate = response.responseTime
override val content: ByteReadChannel = response.content

@InternalAPI
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the goal of adding this @InternalAPI annotation?

If it's delicate, we can/should create an annotation of our own.

@RequiresOptIn(
    level = RequiresOptIn.Level.ERROR,
    message = "Some explanation here."
)
@Target(
    AnnotationTarget.CLASS,
    AnnotationTarget.TYPEALIAS,
    AnnotationTarget.FUNCTION,
    AnnotationTarget.PROPERTY,
    AnnotationTarget.FIELD,
    AnnotationTarget.CONSTRUCTOR,
    AnnotationTarget.PROPERTY_SETTER,
    AnnotationTarget.PROPERTY_SETTER
)
public annotation class InternalNetworkAPI

override val rawContent: ByteReadChannel get() = response.rawContent
override val headers get() = headers
override val coroutineContext: CoroutineContext = response.coroutineContext
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import kotlin.coroutines.cancellation.CancellationException
internal fun HttpRequestBuilder.setWSSUrl(baseUrl: Url, vararg path: String) {
url {
host = baseUrl.host
pathSegments = baseUrl.pathSegments + path
pathSegments = baseUrl.rawSegments + path
protocol = URLProtocol.WSS
port = URLProtocol.WSS.defaultPort
}
Expand All @@ -59,7 +59,7 @@ internal fun HttpRequestBuilder.setUrl(baseUrl: String, vararg path: String) {
private fun HttpRequestBuilder.setHttpsUrl(baseUrl: Url, path: List<String>) {
url {
host = baseUrl.host
pathSegments = baseUrl.pathSegments + path
pathSegments = baseUrl.rawSegments + path
protocol = URLProtocol.HTTPS
}
}
Expand Down
Loading
Loading