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:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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})"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user