Improve error handling for Echo backend error format

- 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 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-16 14:01:33 -06:00
parent 2517435551
commit 96ea1f4686
6 changed files with 70 additions and 13 deletions

View File

@@ -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<String, List<String>>? = null
)

View File

@@ -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")

View File

@@ -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<ErrorResponse>()
// 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<String>()
} catch (e: Exception) {
"An error occurred (${response.status.value})"
val simpleError = response.body<Map<String, String>>()
simpleError["error"] ?: simpleError["message"] ?: simpleError["detail"]
?: "An error occurred (${response.status.value})"
} catch (e2: Exception) {
// Last resort: read as plain text
try {
response.body<String>()
} catch (e3: Exception) {
"An error occurred (${response.status.value})"
}
}
}
}

View File

@@ -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:

View File

@@ -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:

View File

@@ -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 {