commit 5a50c1764be2f885469db3816853ae0beea7a8f1
parent f65a976c0d4a2bb838e973eecd019ed446a4784d
Author: Iván Ávalos <avalos@disroot.org>
Date: Thu, 16 Jul 2026 16:57:14 +0200
[common] reuse HTTP client across requests
Diffstat:
3 files changed, 48 insertions(+), 15 deletions(-)
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/HttpUtils.kt b/taler-kotlin-android/src/main/java/net/taler/common/HttpUtils.kt
@@ -29,18 +29,33 @@ import io.ktor.client.plugins.logging.Logging
import io.ktor.http.HttpMethod
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
+import okhttp3.ConnectionPool
+import okhttp3.Protocol
+import java.util.concurrent.TimeUnit
+
+private val clientCache = mutableMapOf<String, HttpClient>()
fun getDefaultHttpClient(
withJson: Boolean = true,
- timeoutMs: Long? = null,
followRedirect: Boolean = false,
logging: Boolean = true,
+): HttpClient {
+ val key = "$withJson-$followRedirect-$logging"
+ return clientCache.getOrPut(key) { createClient(withJson, followRedirect, logging) }
+}
+
+private fun createClient(
+ withJson: Boolean,
+ followRedirect: Boolean,
+ logging: Boolean,
): HttpClient = HttpClient(OkHttp) {
expectSuccess = true
followRedirects = followRedirect
engine {
config {
retryOnConnectionFailure(true)
+ connectionPool(ConnectionPool(5, 30, TimeUnit.SECONDS))
+ protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))
}
}
install(ContentNegotiation) {
@@ -52,12 +67,7 @@ fun getDefaultHttpClient(
}
}
install(HttpTimeout) {
- requestTimeoutMillis = if (timeoutMs != null && timeoutMs > 0) {
- timeoutMs
- } else {
- INFINITE_TIMEOUT_MS
- }
-
+ requestTimeoutMillis = INFINITE_TIMEOUT_MS
socketTimeoutMillis = INFINITE_TIMEOUT_MS
connectTimeoutMillis = INFINITE_TIMEOUT_MS
}
@@ -76,6 +86,7 @@ fun String.toHttpMethod(): HttpMethod? = when(this) {
"PUT" -> HttpMethod.Put
"PATCH" -> HttpMethod.Patch
"DELETE" -> HttpMethod.Delete
+ "HEAD" -> HttpMethod.Head
"OPTIONS" -> HttpMethod.Options
else -> null
}
\ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt b/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt
@@ -17,6 +17,11 @@
package net.taler.wallet.backend
import android.util.Log
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import net.taler.qtart.TalerWalletCore
import net.taler.wallet.BuildConfig
@@ -48,6 +53,7 @@ class BackendManager(
private val walletCore = TalerWalletCore()
private val requestManager = RequestManager()
private val networkInterface = NetworkInterface()
+ private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
fun run() {
if (!initialized.getAndSet(true)) {
@@ -62,6 +68,7 @@ class BackendManager(
fun destroy() {
if (initialized.getAndSet(false)) {
+ scope.cancel()
walletCore.destroy()
}
}
@@ -79,7 +86,7 @@ class BackendManager(
}
}
- private fun onMessageReceived(msg: String) {
+ private fun onMessageReceived(msg: String) = scope.launch {
Log.d(TAG, "message received: $msg")
when (val message = json.decodeFromString<ApiMessage>(msg)) {
is ApiMessage.Notification -> {
diff --git a/wallet/src/main/java/net/taler/wallet/backend/NetworkInterface.kt b/wallet/src/main/java/net/taler/wallet/backend/NetworkInterface.kt
@@ -18,6 +18,7 @@ package net.taler.wallet.backend
import android.util.Log
import io.ktor.client.call.body
+import io.ktor.client.plugins.*
import io.ktor.client.plugins.ResponseException
import io.ktor.client.request.header
import io.ktor.client.request.headers
@@ -41,6 +42,14 @@ import java.util.concurrent.ConcurrentHashMap
class NetworkInterface: Networking.RequestHandler {
private val requests: ConcurrentHashMap<Int, Job> = ConcurrentHashMap()
+ private val sharedClient by lazy {
+ getDefaultHttpClient(followRedirect = false, logging = false)
+ }
+
+ private val redirectClient by lazy {
+ getDefaultHttpClient(followRedirect = true, logging = false)
+ }
+
override fun handleRequest(
req: Networking.RequestInfo,
id: Int,
@@ -49,16 +58,15 @@ class NetworkInterface: Networking.RequestHandler {
Log.d(TAG, "HTTP: handleRequest($req, $id")
requests[id] = GlobalScope.launch {
- val client = getDefaultHttpClient(
- timeoutMs = req.timeoutMs,
- followRedirect = req.redirectMode == Networking.RedirectMode.Transparent,
- logging = req.debug,
- )
+ val client = if (req.redirectMode == Networking.RedirectMode.Transparent) {
+ redirectClient
+ } else {
+ sharedClient
+ }
var errorMsg: String? = null
val resp = try {
- // TODO: reuse the same client for every request
client.request {
url(req.url)
@@ -74,6 +82,14 @@ class NetworkInterface: Networking.RequestHandler {
if (req.body != null) {
setBody(req.body)
}
+
+ timeout {
+ val t = req.timeoutMs
+ if (t > 0) {
+ requestTimeoutMillis = t
+ socketTimeoutMillis = t
+ }
+ }
}
} catch (e: ResponseException) {
e.response // send non-200 responses to wallet-core anyway
@@ -87,7 +103,6 @@ class NetworkInterface: Networking.RequestHandler {
null
} finally {
cleanupRequest(id)
- client.close()
}
// HTTP response status code or 0 on error.