fix: show specific error diagnostics instead of generic connection failure
Replace null-returning fetchModels/fetchConversations with FetchResult<T> sealed interface. Error banner now shows the exact cause (HTTP status, DNS, SSL, timeout, JSON parsing) instead of "Verbindung zum Server fehlgeschlagen".
This commit is contained in:
parent
827a2f307c
commit
423abc03af
2 changed files with 56 additions and 20 deletions
|
|
@ -57,6 +57,7 @@ import dev.kaizen.app.haptics.rememberHaptics
|
|||
import dev.kaizen.app.net.ChatHttpException
|
||||
import dev.kaizen.app.net.ChatSpeed
|
||||
import dev.kaizen.app.net.ConversationSummary
|
||||
import dev.kaizen.app.net.FetchResult
|
||||
import dev.kaizen.app.net.KaizenApi
|
||||
import dev.kaizen.app.net.KaizenModel
|
||||
import dev.kaizen.app.net.SaveMessage
|
||||
|
|
@ -112,17 +113,21 @@ fun ChatScreen(
|
|||
LaunchedEffect(session.config?.baseUrl, session.config?.token) {
|
||||
val cfg = session.config ?: return@LaunchedEffect
|
||||
loadError = null
|
||||
val fetchedModels = KaizenApi.fetchModels(cfg.baseUrl, cfg.token)
|
||||
if (fetchedModels != null) {
|
||||
models = fetchedModels
|
||||
if (fetchedModels.isNotEmpty()) session.store.saveModelsCache(fetchedModels)
|
||||
val errors = mutableListOf<String>()
|
||||
|
||||
when (val r = KaizenApi.fetchModels(cfg.baseUrl, cfg.token)) {
|
||||
is FetchResult.Ok -> {
|
||||
models = r.data
|
||||
if (r.data.isNotEmpty()) session.store.saveModelsCache(r.data)
|
||||
}
|
||||
val fetchedConvos = KaizenApi.fetchConversations(cfg.baseUrl, cfg.token)
|
||||
if (fetchedConvos != null) {
|
||||
conversations = fetchedConvos
|
||||
is FetchResult.Fail -> errors.add(r.reason)
|
||||
}
|
||||
if (fetchedModels == null || fetchedConvos == null) {
|
||||
loadError = "Verbindung zum Server fehlgeschlagen"
|
||||
when (val r = KaizenApi.fetchConversations(cfg.baseUrl, cfg.token)) {
|
||||
is FetchResult.Ok -> conversations = r.data
|
||||
is FetchResult.Fail -> errors.add(r.reason)
|
||||
}
|
||||
if (errors.isNotEmpty()) {
|
||||
loadError = errors.joinToString(" · ")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +144,8 @@ fun ChatScreen(
|
|||
fun refreshConversations() {
|
||||
val cfg = session.config ?: return
|
||||
scope.launch {
|
||||
KaizenApi.fetchConversations(cfg.baseUrl, cfg.token)?.let { conversations = it }
|
||||
val r = KaizenApi.fetchConversations(cfg.baseUrl, cfg.token)
|
||||
if (r is FetchResult.Ok) conversations = r.data
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,26 @@ sealed interface LoginResult {
|
|||
/** Thrown by [KaizenApi.chat] when the backend rejects the request before streaming. */
|
||||
class ChatHttpException(val code: Int) : IOException("chat failed: HTTP $code")
|
||||
|
||||
/** Result of a fetch that can either succeed or fail with a diagnostic message. */
|
||||
sealed interface FetchResult<out T> {
|
||||
data class Ok<T>(val data: T) : FetchResult<T>
|
||||
data class Fail(val reason: String) : FetchResult<Nothing>
|
||||
}
|
||||
|
||||
private fun diagnoseFetchError(endpoint: String, e: Exception): String {
|
||||
val name = e.javaClass.simpleName
|
||||
return when {
|
||||
e is java.net.UnknownHostException -> "DNS: Host nicht gefunden"
|
||||
e is java.net.ConnectException -> "Verbindung abgelehnt (${e.message})"
|
||||
e is java.net.SocketTimeoutException -> "Timeout ($endpoint)"
|
||||
e is javax.net.ssl.SSLException -> "SSL/TLS: ${e.message}"
|
||||
e is javax.net.ssl.SSLHandshakeException -> "SSL-Zertifikat ungültig"
|
||||
e is java.io.EOFException -> "Verbindung unerwartet geschlossen"
|
||||
e is kotlinx.serialization.SerializationException -> "JSON-Parsing: ${e.message?.take(80)}"
|
||||
else -> "$name: ${e.message?.take(100)}"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thin client over the Kaizen v1 API. One uniform `Authorization: Bearer` contract,
|
||||
* same against the official server or any self-host. The chat response is a
|
||||
|
|
@ -145,8 +165,8 @@ object KaizenApi {
|
|||
}
|
||||
}
|
||||
|
||||
/** GET /api/v1/models — the full model catalog for the picker. Returns null on failure (with logged reason). */
|
||||
suspend fun fetchModels(baseUrl: String, token: String): List<KaizenModel>? =
|
||||
/** GET /api/v1/models — the full model catalog for the picker. */
|
||||
suspend fun fetchModels(baseUrl: String, token: String): FetchResult<List<KaizenModel>> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val req = Request.Builder()
|
||||
.url("$baseUrl/api/v1/models")
|
||||
|
|
@ -155,19 +175,24 @@ object KaizenApi {
|
|||
try {
|
||||
client.newCall(req).execute().use { resp ->
|
||||
if (!resp.isSuccessful) {
|
||||
Log.w(TAG, "fetchModels: HTTP ${resp.code} from $baseUrl")
|
||||
return@use null
|
||||
val reason = "models: HTTP ${resp.code}" + when (resp.code) {
|
||||
401 -> " (Token ungültig)"
|
||||
403 -> " (Zugriff verweigert)"
|
||||
else -> ""
|
||||
}
|
||||
json.decodeFromString<ModelsResponse>(resp.body!!.string()).models
|
||||
Log.w(TAG, "fetchModels: HTTP ${resp.code} from $baseUrl")
|
||||
return@use FetchResult.Fail(reason)
|
||||
}
|
||||
FetchResult.Ok(json.decodeFromString<ModelsResponse>(resp.body!!.string()).models)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "fetchModels: ${e.javaClass.simpleName}: ${e.message}")
|
||||
null
|
||||
FetchResult.Fail(diagnoseFetchError("models", e))
|
||||
}
|
||||
}
|
||||
|
||||
/** GET /api/v1/conversations — the sidebar list. Returns null on failure (with logged reason). */
|
||||
suspend fun fetchConversations(baseUrl: String, token: String): List<ConversationSummary>? =
|
||||
/** GET /api/v1/conversations — the sidebar list. */
|
||||
suspend fun fetchConversations(baseUrl: String, token: String): FetchResult<List<ConversationSummary>> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val req = Request.Builder()
|
||||
.url("$baseUrl/api/v1/conversations")
|
||||
|
|
@ -176,14 +201,19 @@ object KaizenApi {
|
|||
try {
|
||||
client.newCall(req).execute().use { resp ->
|
||||
if (!resp.isSuccessful) {
|
||||
Log.w(TAG, "fetchConversations: HTTP ${resp.code} from $baseUrl")
|
||||
return@use null
|
||||
val reason = "conversations: HTTP ${resp.code}" + when (resp.code) {
|
||||
401 -> " (Token ungültig)"
|
||||
403 -> " (Zugriff verweigert)"
|
||||
else -> ""
|
||||
}
|
||||
json.decodeFromString<ConversationsResponse>(resp.body!!.string()).conversations
|
||||
Log.w(TAG, "fetchConversations: HTTP ${resp.code} from $baseUrl")
|
||||
return@use FetchResult.Fail(reason)
|
||||
}
|
||||
FetchResult.Ok(json.decodeFromString<ConversationsResponse>(resp.body!!.string()).conversations)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "fetchConversations: ${e.javaClass.simpleName}: ${e.message}")
|
||||
null
|
||||
FetchResult.Fail(diagnoseFetchError("conversations", e))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue