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.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
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
|
@Serializable
|
||||||
data class ErrorResponse(
|
data class ErrorResponse(
|
||||||
val error: String,
|
val error: String,
|
||||||
val detail: String,
|
val detail: String? = null,
|
||||||
@SerialName("status_code") val statusCode: Int,
|
@SerialName("status_code") val statusCode: Int? = null,
|
||||||
val errors: Map<String, List<String>>? = null
|
val errors: Map<String, List<String>>? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
|||||||
if (response.status.isSuccess()) {
|
if (response.status.isSuccess()) {
|
||||||
ApiResult.Success(response.body())
|
ApiResult.Success(response.body())
|
||||||
} else {
|
} 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) {
|
} catch (e: Exception) {
|
||||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||||
@@ -36,7 +38,9 @@ class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
|||||||
if (response.status.isSuccess()) {
|
if (response.status.isSuccess()) {
|
||||||
ApiResult.Success(response.body())
|
ApiResult.Success(response.body())
|
||||||
} else {
|
} 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) {
|
} catch (e: Exception) {
|
||||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||||
|
|||||||
@@ -11,13 +11,27 @@ object ErrorParser {
|
|||||||
isLenient = true
|
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 {
|
suspend fun parseError(response: HttpResponse): String {
|
||||||
return try {
|
return try {
|
||||||
val errorResponse = response.body<ErrorResponse>()
|
val errorResponse = response.body<ErrorResponse>()
|
||||||
|
|
||||||
// Build detailed error message
|
// Build detailed error message
|
||||||
val message = StringBuilder()
|
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
|
// Add field-specific errors if present
|
||||||
errorResponse.errors?.let { fieldErrors ->
|
errorResponse.errors?.let { fieldErrors ->
|
||||||
@@ -31,11 +45,18 @@ object ErrorParser {
|
|||||||
|
|
||||||
message.toString()
|
message.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Fallback to reading as plain text
|
// Fallback: try to parse as simple {"error": "message"} map
|
||||||
try {
|
try {
|
||||||
response.body<String>()
|
val simpleError = response.body<Map<String, String>>()
|
||||||
} catch (e: Exception) {
|
simpleError["error"] ?: simpleError["message"] ?: simpleError["detail"]
|
||||||
"An error occurred (${response.status.value})"
|
?: "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 {
|
if let code = error.code?.intValue {
|
||||||
switch code {
|
switch code {
|
||||||
case 400:
|
|
||||||
errorMessage = "Invalid Apple Sign In token"
|
|
||||||
case 401:
|
case 401:
|
||||||
errorMessage = "Authentication failed. Please try again."
|
errorMessage = "Authentication failed. Please try again."
|
||||||
case 403:
|
case 403:
|
||||||
errorMessage = "Access denied"
|
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:
|
case 500...599:
|
||||||
errorMessage = "Server error. Please try again later."
|
errorMessage = "Server error. Please try again later."
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -106,12 +106,18 @@ class LoginViewModel: ObservableObject {
|
|||||||
// Check for specific error codes and provide user-friendly messages
|
// Check for specific error codes and provide user-friendly messages
|
||||||
if let code = error.code?.intValue {
|
if let code = error.code?.intValue {
|
||||||
switch code {
|
switch code {
|
||||||
case 400, 401:
|
case 401:
|
||||||
self.errorMessage = "Invalid username or password"
|
self.errorMessage = "Invalid username or password"
|
||||||
case 403:
|
case 403:
|
||||||
self.errorMessage = "Access denied. Please check your credentials."
|
self.errorMessage = "Access denied. Please check your credentials."
|
||||||
case 404:
|
case 404:
|
||||||
self.errorMessage = "Service not found. Please try again later."
|
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:
|
case 500...599:
|
||||||
self.errorMessage = "Server error. Please try again later."
|
self.errorMessage = "Server error. Please try again later."
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -72,7 +72,24 @@ class RegisterViewModel: ObservableObject {
|
|||||||
self.isRegistered = true
|
self.isRegistered = true
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
} else if let error = result as? ApiResultError {
|
} 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
|
self.isLoading = false
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user