Authentication aufräumen, 3. Schritt Error Handling f. auth + HttpClient
This commit is contained in:
1 parent
9b573243ef
commit
e8bd21d108
4 files changed
+107
-16
No files matched your search
@@ -1,3 +1,3 @@
|
|||||||
#auto generated
|
#auto generated
|
||||||
#Thu Feb 12 03:10:51 CET 2026
|
#Thu Feb 12 22:52:33 CET 2026
|
||||||
build.number=238
|
build.number=251
|
||||||
@@ -17,7 +17,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.http.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -25,6 +25,9 @@ import net.keinekekse.app.auth.AuthResult
|
|||||||
import net.keinekekse.app.auth.Authenticator
|
import net.keinekekse.app.auth.Authenticator
|
||||||
import net.keinekekse.app.books.generated.resources.Res
|
import net.keinekekse.app.books.generated.resources.Res
|
||||||
import net.keinekekse.app.books.generated.resources.compose_multiplatform
|
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.jetbrains.compose.resources.painterResource
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.core.annotation.Single
|
import org.koin.core.annotation.Single
|
||||||
@@ -44,6 +47,9 @@ fun App() {
|
|||||||
val test = koinInject<TestAuth>()
|
val test = koinInject<TestAuth>()
|
||||||
val authenticator = koinInject<Authenticator>()
|
val authenticator = koinInject<Authenticator>()
|
||||||
var showContent by remember { mutableStateOf(false) }
|
var showContent by remember { mutableStateOf(false) }
|
||||||
|
// externer StateFlow
|
||||||
|
val logonState by remember { authenticator.logonState }.collectAsState()
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||||
@@ -74,8 +80,7 @@ fun App() {
|
|||||||
Text("Compose: $greeting")
|
Text("Compose: $greeting")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// refresh fkt. nicht!
|
when (logonState) {
|
||||||
when (authenticator.logonState.value) {
|
|
||||||
is AuthResult.Success -> {
|
is AuthResult.Success -> {
|
||||||
Text("Anmeldung erfolgreich")
|
Text("Anmeldung erfolgreich")
|
||||||
}
|
}
|
||||||
@@ -99,16 +104,45 @@ fun App() {
|
|||||||
class TestAuth(val httpClient: HttpClient) {
|
class TestAuth(val httpClient: HttpClient) {
|
||||||
|
|
||||||
private val srv = "https://kas.keinekekse.net"
|
private val srv = "https://kas.keinekekse.net"
|
||||||
|
|
||||||
suspend fun request(url: String) {
|
suspend fun request(url: String) {
|
||||||
logger.debug { "connecting" }
|
logger.debug { "TestAuth: connecting $srv$url" }
|
||||||
// val response = httpClient.get(srv + "testdb/books")
|
|
||||||
|
val response: ApiResponse<List<Book>, 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<Book> = response.body()
|
// val books: List<Book> = response.body()
|
||||||
|
|
||||||
val response = httpClient.get(srv + url)
|
// var response: HttpResponse? = null
|
||||||
val body = response.bodyAsText()
|
// 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 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,13 +4,13 @@ import net.keinekekse.version.Version
|
|||||||
|
|
||||||
object VERSION: Version {
|
object VERSION: Version {
|
||||||
override val version = "1.0.0"
|
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 major = 1
|
||||||
override val minor = 0
|
override val minor = 0
|
||||||
override val patch = 0
|
override val patch = 0
|
||||||
override val preRelease = ""
|
override val preRelease = ""
|
||||||
override val buildMetadata = "build.238"
|
override val buildMetadata = "build.251"
|
||||||
override val buildDate = "2026-02-12T02:10:51.539449Z"
|
override val buildDate = "2026-02-12T21:52:33.708217Z"
|
||||||
override val displayName = "Books App"
|
override val displayName = "Books App"
|
||||||
override val copyright = "Copyright (c) 2026 Hartmut Bauer"
|
override val copyright = "Copyright (c) 2026 Hartmut Bauer"
|
||||||
override val name = "books"
|
override val name = "books"
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ package net.keinekekse.app.service
|
|||||||
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.plugins.*
|
import io.ktor.client.plugins.*
|
||||||
import io.ktor.client.plugins.auth.*
|
import io.ktor.client.plugins.auth.*
|
||||||
import io.ktor.client.plugins.auth.providers.*
|
import io.ktor.client.plugins.auth.providers.*
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import io.ktor.util.network.*
|
||||||
|
import kotlinx.io.IOException
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import net.keinekekse.app.auth.Authenticator
|
import net.keinekekse.app.auth.Authenticator
|
||||||
import org.koin.core.annotation.Single
|
import org.koin.core.annotation.Single
|
||||||
@@ -18,6 +23,7 @@ private val logger = KotlinLogging.logger {}
|
|||||||
@Single
|
@Single
|
||||||
fun createHttpClient(authenticator: Authenticator): HttpClient {
|
fun createHttpClient(authenticator: Authenticator): HttpClient {
|
||||||
val client = HttpClient {
|
val client = HttpClient {
|
||||||
|
expectSuccess = true
|
||||||
install(Auth) {
|
install(Auth) {
|
||||||
bearer {
|
bearer {
|
||||||
loadTokens {
|
loadTokens {
|
||||||
@@ -36,7 +42,6 @@ fun createHttpClient(authenticator: Authenticator): HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(Json {
|
json(Json {
|
||||||
prettyPrint = true
|
prettyPrint = true
|
||||||
@@ -50,6 +55,7 @@ fun createHttpClient(authenticator: Authenticator): HttpClient {
|
|||||||
requestTimeoutMillis = 10000
|
requestTimeoutMillis = 10000
|
||||||
connectTimeoutMillis = 5000
|
connectTimeoutMillis = 5000
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client.plugin(HttpSend).intercept { request ->
|
client.plugin(HttpSend).intercept { request ->
|
||||||
@@ -67,3 +73,54 @@ fun createHttpClient(authenticator: Authenticator): HttpClient {
|
|||||||
|
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTP-Error-Handling -> https://stackoverflow.com/questions/54679592/handling-exception-in-httpclient-ktor
|
||||||
|
|
||||||
|
sealed class ApiResponse<out T, out E> {
|
||||||
|
/**
|
||||||
|
* Represents successful network responses (2xx).
|
||||||
|
*/
|
||||||
|
data class Success<T>(val body: T) : ApiResponse<T, Nothing>()
|
||||||
|
|
||||||
|
sealed class ErrorResponse<E> : ApiResponse<Nothing, E>() {
|
||||||
|
/**
|
||||||
|
* Represents server (50x) and client (40x) errors.
|
||||||
|
*/
|
||||||
|
data class HttpError<E>(val code: Int, val errorBody: E?) : ErrorResponse<E>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represent IOExceptions and connectivity issues.
|
||||||
|
*/
|
||||||
|
data class NetworkError(val exception: String) : ErrorResponse<Nothing>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represent SerializationExceptions.
|
||||||
|
*/
|
||||||
|
object SerializationError : ErrorResponse<Nothing>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun <reified T, reified E> HttpClient.safeRequest(
|
||||||
|
block: HttpRequestBuilder.() -> Unit,
|
||||||
|
): ApiResponse<T, E> =
|
||||||
|
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 <reified E> ResponseException.errorBody(): E? =
|
||||||
|
try {
|
||||||
|
response.body()
|
||||||
|
} catch (_: SerializationException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
Reference in new issue
Block a user