From e8bd21d1081c8a42ab3fbc0d2fa972608e1328d2 Mon Sep 17 00:00:00 2001 From: Hartmut Bauer Date: Thu, 12 Feb 2026 23:47:06 +0100 Subject: [PATCH] =?UTF-8?q?Authentication=20aufr=C3=A4umen,=203.=20Schritt?= =?UTF-8?q?=20Error=20Handling=20f.=20auth=20+=20HttpClient?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- books/buildnumber.properties | 4 +- .../kotlin/net/keinekekse/app/App.kt | 54 +++++++++++++---- .../net/keinekekse/app/books/Version.kt | 6 +- .../net/keinekekse/app/service/HttpClient.kt | 59 ++++++++++++++++++- 4 files changed, 107 insertions(+), 16 deletions(-) diff --git a/books/buildnumber.properties b/books/buildnumber.properties index 2232086..920ac1d 100644 --- a/books/buildnumber.properties +++ b/books/buildnumber.properties @@ -1,3 +1,3 @@ #auto generated -#Thu Feb 12 03:10:51 CET 2026 -build.number=238 +#Thu Feb 12 22:52:33 CET 2026 +build.number=251 diff --git a/books/src/commonMain/kotlin/net/keinekekse/app/App.kt b/books/src/commonMain/kotlin/net/keinekekse/app/App.kt index c6cd4e6..3d93bdf 100644 --- a/books/src/commonMain/kotlin/net/keinekekse/app/App.kt +++ b/books/src/commonMain/kotlin/net/keinekekse/app/App.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.tooling.preview.Preview import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.* import io.ktor.client.request.* -import io.ktor.client.statement.* +import io.ktor.http.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -25,6 +25,9 @@ import net.keinekekse.app.auth.AuthResult import net.keinekekse.app.auth.Authenticator import net.keinekekse.app.books.generated.resources.Res import net.keinekekse.app.books.generated.resources.compose_multiplatform +import net.keinekekse.app.books.model.Book +import net.keinekekse.app.service.ApiResponse +import net.keinekekse.app.service.safeRequest import org.jetbrains.compose.resources.painterResource import org.koin.compose.koinInject import org.koin.core.annotation.Single @@ -44,6 +47,9 @@ fun App() { val test = koinInject() val authenticator = koinInject() var showContent by remember { mutableStateOf(false) } + // externer StateFlow + val logonState by remember { authenticator.logonState }.collectAsState() + Column( modifier = Modifier .background(MaterialTheme.colorScheme.primaryContainer) @@ -74,8 +80,7 @@ fun App() { Text("Compose: $greeting") } } - // refresh fkt. nicht! - when (authenticator.logonState.value) { + when (logonState) { is AuthResult.Success -> { Text("Anmeldung erfolgreich") } @@ -99,16 +104,45 @@ fun App() { class TestAuth(val httpClient: HttpClient) { private val srv = "https://kas.keinekekse.net" + suspend fun request(url: String) { - logger.debug { "connecting" } -// val response = httpClient.get(srv + "testdb/books") + logger.debug { "TestAuth: connecting $srv$url" } + + val response: ApiResponse, Any> = httpClient.safeRequest { + url(Url(srv + url)) + method = HttpMethod.Get + } + when (response) { + is ApiResponse.Success -> { + logger.debug { "TestAuth: success" } + val books = response.body + for (book in books) { + logger.debug { book } + } + } + + is ApiResponse.ErrorResponse -> { + when (response) { + is ApiResponse.ErrorResponse.HttpError -> logger.debug { "TestAuth: HTTP error ${response.code} - ${response.errorBody}" } + is ApiResponse.ErrorResponse.NetworkError -> logger.debug { "TestAuth: Network error ${response.exception}" } + is ApiResponse.ErrorResponse.SerializationError -> logger.debug { "TestAuth: Serialization error" } + } + + } + } // val books: List = response.body() - val response = httpClient.get(srv + url) - val body = response.bodyAsText() +// var response: HttpResponse? = null +// var body = "" +// try { +// response = httpClient.get(srv + url) +// body = response.bodyAsText() +// } catch (e: Throwable) { +// val error = e.toString() +// val status = response?.status +// val body = response?.bodyAsText() ?: "" +// logger.error(e) { "TestAuth: error $error ($body) [$status]" } +// } - logger.debug { "call to $url" } - logger.debug { response.status } - logger.debug { body } } } diff --git a/books/src/commonMain/kotlin/net/keinekekse/app/books/Version.kt b/books/src/commonMain/kotlin/net/keinekekse/app/books/Version.kt index 94512de..b8c327a 100644 --- a/books/src/commonMain/kotlin/net/keinekekse/app/books/Version.kt +++ b/books/src/commonMain/kotlin/net/keinekekse/app/books/Version.kt @@ -4,13 +4,13 @@ import net.keinekekse.version.Version object VERSION: Version { override val version = "1.0.0" - override val fullVersion = "1.0.0+build.238" + override val fullVersion = "1.0.0+build.251" override val major = 1 override val minor = 0 override val patch = 0 override val preRelease = "" - override val buildMetadata = "build.238" - override val buildDate = "2026-02-12T02:10:51.539449Z" + override val buildMetadata = "build.251" + override val buildDate = "2026-02-12T21:52:33.708217Z" override val displayName = "Books App" override val copyright = "Copyright (c) 2026 Hartmut Bauer" override val name = "books" diff --git a/shared/src/commonMain/kotlin/net/keinekekse/app/service/HttpClient.kt b/shared/src/commonMain/kotlin/net/keinekekse/app/service/HttpClient.kt index f6b9e89..8a716bb 100644 --- a/shared/src/commonMain/kotlin/net/keinekekse/app/service/HttpClient.kt +++ b/shared/src/commonMain/kotlin/net/keinekekse/app/service/HttpClient.kt @@ -2,12 +2,17 @@ package net.keinekekse.app.service import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.* +import io.ktor.client.call.* import io.ktor.client.plugins.* import io.ktor.client.plugins.auth.* import io.ktor.client.plugins.auth.providers.* import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* import io.ktor.serialization.kotlinx.json.* +import io.ktor.util.network.* +import kotlinx.io.IOException import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import net.keinekekse.app.auth.Authenticator import org.koin.core.annotation.Single @@ -18,6 +23,7 @@ private val logger = KotlinLogging.logger {} @Single fun createHttpClient(authenticator: Authenticator): HttpClient { val client = HttpClient { + expectSuccess = true install(Auth) { bearer { loadTokens { @@ -36,7 +42,6 @@ fun createHttpClient(authenticator: Authenticator): HttpClient { } } } - install(ContentNegotiation) { json(Json { prettyPrint = true @@ -50,6 +55,7 @@ fun createHttpClient(authenticator: Authenticator): HttpClient { requestTimeoutMillis = 10000 connectTimeoutMillis = 5000 } + } client.plugin(HttpSend).intercept { request -> @@ -67,3 +73,54 @@ fun createHttpClient(authenticator: Authenticator): HttpClient { return client } + +// HTTP-Error-Handling -> https://stackoverflow.com/questions/54679592/handling-exception-in-httpclient-ktor + +sealed class ApiResponse { + /** + * Represents successful network responses (2xx). + */ + data class Success(val body: T) : ApiResponse() + + sealed class ErrorResponse : ApiResponse() { + /** + * Represents server (50x) and client (40x) errors. + */ + data class HttpError(val code: Int, val errorBody: E?) : ErrorResponse() + + /** + * Represent IOExceptions and connectivity issues. + */ + data class NetworkError(val exception: String) : ErrorResponse() + + /** + * Represent SerializationExceptions. + */ + object SerializationError : ErrorResponse() + } +} + +suspend inline fun HttpClient.safeRequest( + block: HttpRequestBuilder.() -> Unit, +): ApiResponse = + try { + val response = request { block() } + ApiResponse.Success(response.body()) + } catch (e: ClientRequestException) { + ApiResponse.ErrorResponse.HttpError(e.response.status.value, e.errorBody()) + } catch (e: ServerResponseException) { + ApiResponse.ErrorResponse.HttpError(e.response.status.value, e.errorBody()) + } catch (e: IOException) { + ApiResponse.ErrorResponse.NetworkError(e.toString()) + } catch (e: UnresolvedAddressException) { // no Network + ApiResponse.ErrorResponse.NetworkError(e.toString()) + } catch (_: SerializationException) { + ApiResponse.ErrorResponse.SerializationError + } + +suspend inline fun ResponseException.errorBody(): E? = + try { + response.body() + } catch (_: SerializationException) { + null + }