wip
This commit is contained in:
112
ERROR_HANDLING.md
Normal file
112
ERROR_HANDLING.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# Error Handling in MyCrib Apps
|
||||||
|
|
||||||
|
Both iOS and Android apps now display detailed error messages from the API.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Backend (Django)
|
||||||
|
The Django backend returns errors in a standardized JSON format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Error Type",
|
||||||
|
"detail": "Detailed error message",
|
||||||
|
"status_code": 400,
|
||||||
|
"errors": {
|
||||||
|
"field_name": ["Field-specific error messages"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shared Network Layer (Kotlin Multiplatform)
|
||||||
|
|
||||||
|
#### ErrorResponse Model
|
||||||
|
Located at: `composeApp/src/commonMain/kotlin/com/example/mycrib/models/ErrorResponse.kt`
|
||||||
|
|
||||||
|
Defines the structure of error responses from the API.
|
||||||
|
|
||||||
|
#### ErrorParser
|
||||||
|
Located at: `composeApp/src/commonMain/kotlin/com/example/mycrib/network/ErrorParser.kt`
|
||||||
|
|
||||||
|
Parses error responses and extracts detailed messages:
|
||||||
|
- Primary error detail from the `detail` field
|
||||||
|
- Field-specific errors from the `errors` field (if present)
|
||||||
|
- Fallback to plain text or generic message if parsing fails
|
||||||
|
|
||||||
|
#### Updated API Calls
|
||||||
|
All TaskApi methods now use ErrorParser to extract detailed error messages:
|
||||||
|
- `getTasks()`
|
||||||
|
- `getTask()`
|
||||||
|
- `createTask()`
|
||||||
|
- `updateTask()`
|
||||||
|
- `deleteTask()`
|
||||||
|
- `getTasksByResidence()`
|
||||||
|
- `cancelTask()`
|
||||||
|
- `uncancelTask()`
|
||||||
|
- `markInProgress()`
|
||||||
|
- `archiveTask()`
|
||||||
|
- `unarchiveTask()`
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
Android automatically receives parsed error messages through the shared `ApiResult.Error` class. ViewModels and UI components display these messages to users.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```kotlin
|
||||||
|
when (result) {
|
||||||
|
is ApiResult.Error -> {
|
||||||
|
// result.message contains the detailed error from ErrorParser
|
||||||
|
showError(result.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
|
||||||
|
iOS receives parsed error messages through the `ApiResultError` wrapper class from the shared KMM module. ViewModels extract and display these messages.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```swift
|
||||||
|
if let errorResult = result as? ApiResultError {
|
||||||
|
self.errorMessage = errorResult.message // Contains detailed error from ErrorParser
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Message Format
|
||||||
|
|
||||||
|
### Simple Errors
|
||||||
|
```
|
||||||
|
Invalid task status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation Errors with Field Details
|
||||||
|
```
|
||||||
|
Invalid data provided
|
||||||
|
|
||||||
|
Details:
|
||||||
|
• email: This field is required.
|
||||||
|
• password: Password must be at least 8 characters.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
1. **User-Friendly**: Users see specific error messages instead of generic "Failed to..." messages
|
||||||
|
2. **Debugging**: Developers can quickly identify issues from error messages
|
||||||
|
3. **Consistent**: Both platforms display the same detailed errors
|
||||||
|
4. **Maintainable**: Single source of truth for error parsing in shared code
|
||||||
|
5. **Backend-Driven**: Error messages are controlled by the Django backend
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
To test error handling:
|
||||||
|
|
||||||
|
1. Create a task with invalid data
|
||||||
|
2. Update a task with missing required fields
|
||||||
|
3. Try to perform actions without authentication
|
||||||
|
4. Observe the detailed error messages displayed in the UI
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
|
||||||
|
- Add error codes for programmatic handling
|
||||||
|
- Implement retry logic for specific error types
|
||||||
|
- Add localization support for error messages
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.mycrib.shared.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ErrorResponse(
|
||||||
|
val error: String,
|
||||||
|
val detail: String,
|
||||||
|
@SerialName("status_code") val statusCode: Int,
|
||||||
|
val errors: Map<String, List<String>>? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.mycrib.shared.network
|
||||||
|
|
||||||
|
import com.mycrib.shared.models.ErrorResponse
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.statement.HttpResponse
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
object ErrorParser {
|
||||||
|
private val json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
isLenient = true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun parseError(response: HttpResponse): String {
|
||||||
|
return try {
|
||||||
|
val errorResponse = response.body<ErrorResponse>()
|
||||||
|
|
||||||
|
// Build detailed error message
|
||||||
|
val message = StringBuilder()
|
||||||
|
message.append(errorResponse.detail)
|
||||||
|
|
||||||
|
// Add field-specific errors if present
|
||||||
|
errorResponse.errors?.let { fieldErrors ->
|
||||||
|
if (fieldErrors.isNotEmpty()) {
|
||||||
|
message.append("\n\nDetails:")
|
||||||
|
fieldErrors.forEach { (field, errors) ->
|
||||||
|
message.append("\n• $field: ${errors.joinToString(", ")}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message.toString()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Fallback to reading as plain text
|
||||||
|
try {
|
||||||
|
response.body<String>()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
"An error occurred (${response.status.value})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,8 @@ class TaskApi(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("Failed to fetch tasks", response.status.value)
|
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")
|
||||||
@@ -38,7 +39,8 @@ class TaskApi(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("Failed to fetch task", response.status.value)
|
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")
|
||||||
@@ -56,7 +58,8 @@ class TaskApi(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("Failed to create task", response.status.value)
|
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")
|
||||||
@@ -74,7 +77,8 @@ class TaskApi(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("Failed to update task", response.status.value)
|
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")
|
||||||
@@ -90,7 +94,8 @@ class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
|||||||
if (response.status.isSuccess()) {
|
if (response.status.isSuccess()) {
|
||||||
ApiResult.Success(Unit)
|
ApiResult.Success(Unit)
|
||||||
} else {
|
} else {
|
||||||
ApiResult.Error("Failed to delete task", response.status.value)
|
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")
|
||||||
@@ -111,7 +116,8 @@ class TaskApi(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("Failed to fetch tasks by residence", response.status.value)
|
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")
|
||||||
@@ -127,7 +133,8 @@ class TaskApi(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("Failed to cancel task", response.status.value)
|
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")
|
||||||
@@ -143,7 +150,8 @@ class TaskApi(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("Failed to uncancel task", response.status.value)
|
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")
|
||||||
@@ -159,7 +167,8 @@ class TaskApi(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("Failed to mark task as in progress", response.status.value)
|
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")
|
||||||
@@ -175,7 +184,8 @@ class TaskApi(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("Failed to archive task", response.status.value)
|
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")
|
||||||
@@ -191,7 +201,8 @@ class TaskApi(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("Failed to unarchive task", response.status.value)
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user