From 96ea1f4686d97482f7ab2a050df38757409b1c8e Mon Sep 17 00:00:00 2001 From: Trey t Date: Tue, 16 Dec 2025 14:01:33 -0600 Subject: [PATCH] Improve error handling for Echo backend error format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update ErrorResponse model to make detail and statusCode optional since backend now returns simple {"error": "message"} format - Update AuthApi to parse actual backend error messages instead of generic "Registration failed"/"Login failed" strings - Update ErrorParser to prioritize the "error" field and add fallback for simple error map responses - Update iOS ViewModels (Login, Register, AppleSignIn) to properly handle 400 and 409 status codes by displaying backend error messages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../example/casera/models/ErrorResponse.kt | 9 ++++-- .../com/example/casera/network/AuthApi.kt | 8 +++-- .../com/example/casera/network/ErrorParser.kt | 31 ++++++++++++++++--- .../iosApp/Login/AppleSignInViewModel.swift | 8 +++-- iosApp/iosApp/Login/LoginViewModel.swift | 8 ++++- .../iosApp/Register/RegisterViewModel.swift | 19 +++++++++++- 6 files changed, 70 insertions(+), 13 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/models/ErrorResponse.kt b/composeApp/src/commonMain/kotlin/com/example/casera/models/ErrorResponse.kt index 36b31ba..a924ef8 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/models/ErrorResponse.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/models/ErrorResponse.kt @@ -3,10 +3,15 @@ package com.example.casera.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +/** + * Error response model that handles the backend's error format. + * The backend returns: {"error": "message"} + * All fields except 'error' are optional for backwards compatibility. + */ @Serializable data class ErrorResponse( val error: String, - val detail: String, - @SerialName("status_code") val statusCode: Int, + val detail: String? = null, + @SerialName("status_code") val statusCode: Int? = null, val errors: Map>? = null ) diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/network/AuthApi.kt b/composeApp/src/commonMain/kotlin/com/example/casera/network/AuthApi.kt index 9e97d4b..6a17e5d 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/network/AuthApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/network/AuthApi.kt @@ -19,7 +19,9 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) { if (response.status.isSuccess()) { ApiResult.Success(response.body()) } else { - ApiResult.Error("Registration failed", response.status.value) + // Parse actual error message from backend + val errorMessage = ErrorParser.parseError(response) + ApiResult.Error(errorMessage, response.status.value) } } catch (e: Exception) { ApiResult.Error(e.message ?: "Unknown error occurred") @@ -36,7 +38,9 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) { if (response.status.isSuccess()) { ApiResult.Success(response.body()) } else { - ApiResult.Error("Login failed", response.status.value) + // Parse actual error message from backend + val errorMessage = ErrorParser.parseError(response) + ApiResult.Error(errorMessage, response.status.value) } } catch (e: Exception) { ApiResult.Error(e.message ?: "Unknown error occurred") diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/network/ErrorParser.kt b/composeApp/src/commonMain/kotlin/com/example/casera/network/ErrorParser.kt index aa86722..95587ca 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/network/ErrorParser.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/network/ErrorParser.kt @@ -11,13 +11,27 @@ object ErrorParser { isLenient = true } + /** + * Parses error response from the backend. + * The backend returns: {"error": "message"} + * Falls back to detail field or field-specific errors if present. + */ suspend fun parseError(response: HttpResponse): String { return try { val errorResponse = response.body() // Build detailed error message val message = StringBuilder() - message.append(errorResponse.detail) + + // Primary: use the error field (main error message from backend) + message.append(errorResponse.error) + + // Secondary: append detail if present and different from error + errorResponse.detail?.let { detail -> + if (detail.isNotBlank() && detail != errorResponse.error) { + message.append(": $detail") + } + } // Add field-specific errors if present errorResponse.errors?.let { fieldErrors -> @@ -31,11 +45,18 @@ object ErrorParser { message.toString() } catch (e: Exception) { - // Fallback to reading as plain text + // Fallback: try to parse as simple {"error": "message"} map try { - response.body() - } catch (e: Exception) { - "An error occurred (${response.status.value})" + val simpleError = response.body>() + simpleError["error"] ?: simpleError["message"] ?: simpleError["detail"] + ?: "An error occurred (${response.status.value})" + } catch (e2: Exception) { + // Last resort: read as plain text + try { + response.body() + } catch (e3: Exception) { + "An error occurred (${response.status.value})" + } } } } diff --git a/iosApp/iosApp/Login/AppleSignInViewModel.swift b/iosApp/iosApp/Login/AppleSignInViewModel.swift index bf818e4..8e83564 100644 --- a/iosApp/iosApp/Login/AppleSignInViewModel.swift +++ b/iosApp/iosApp/Login/AppleSignInViewModel.swift @@ -125,12 +125,16 @@ class AppleSignInViewModel: ObservableObject { if let code = error.code?.intValue { switch code { - case 400: - errorMessage = "Invalid Apple Sign In token" case 401: errorMessage = "Authentication failed. Please try again." case 403: errorMessage = "Access denied" + case 409: + // Conflict - let backend message explain the issue + errorMessage = ErrorMessageParser.parse(error.message) + case 400: + // Bad request - validation errors from backend + errorMessage = ErrorMessageParser.parse(error.message) case 500...599: errorMessage = "Server error. Please try again later." default: diff --git a/iosApp/iosApp/Login/LoginViewModel.swift b/iosApp/iosApp/Login/LoginViewModel.swift index 021ea8b..114932a 100644 --- a/iosApp/iosApp/Login/LoginViewModel.swift +++ b/iosApp/iosApp/Login/LoginViewModel.swift @@ -106,12 +106,18 @@ class LoginViewModel: ObservableObject { // Check for specific error codes and provide user-friendly messages if let code = error.code?.intValue { switch code { - case 400, 401: + case 401: self.errorMessage = "Invalid username or password" case 403: self.errorMessage = "Access denied. Please check your credentials." case 404: self.errorMessage = "Service not found. Please try again later." + case 409: + // Conflict - let backend message explain the issue + self.errorMessage = ErrorMessageParser.parse(error.message) + case 400: + // Bad request - validation errors from backend + self.errorMessage = ErrorMessageParser.parse(error.message) case 500...599: self.errorMessage = "Server error. Please try again later." default: diff --git a/iosApp/iosApp/Register/RegisterViewModel.swift b/iosApp/iosApp/Register/RegisterViewModel.swift index 4346e1a..f23aa7b 100644 --- a/iosApp/iosApp/Register/RegisterViewModel.swift +++ b/iosApp/iosApp/Register/RegisterViewModel.swift @@ -72,7 +72,24 @@ class RegisterViewModel: ObservableObject { self.isRegistered = true self.isLoading = false } else if let error = result as? ApiResultError { - self.errorMessage = ErrorMessageParser.parse(error.message) + // Handle specific HTTP status codes + if let code = error.code?.intValue { + switch code { + case 409: + // Conflict - duplicate username or email + // The backend error message already contains the specific conflict + self.errorMessage = ErrorMessageParser.parse(error.message) + case 400: + // Bad request - validation errors + self.errorMessage = ErrorMessageParser.parse(error.message) + case 500...599: + self.errorMessage = "Server error. Please try again later." + default: + self.errorMessage = ErrorMessageParser.parse(error.message) + } + } else { + self.errorMessage = ErrorMessageParser.parse(error.message) + } self.isLoading = false } } catch {