Error System Documentation¶
This document describes the comprehensive error handling system implemented in readur for consistent, maintainable, and user-friendly error responses.
Overview¶
The error system provides structured, typed error handling across all API endpoints with automatic integration into the existing error management and monitoring infrastructure.
Architecture¶
Core Components¶
src/errors/mod.rs
- Central error module with shared traits and utilities- Entity-specific error modules - Dedicated error types for each domain:
src/errors/user.rs
- User management errorssrc/errors/source.rs
- File source operation errorssrc/errors/label.rs
- Label management errorssrc/errors/settings.rs
- Settings configuration errorssrc/errors/search.rs
- Search operation errors
AppError Trait¶
All custom errors implement the AppError
trait which provides:
pub trait AppError: std::error::Error + Send + Sync + 'static {
fn status_code(&self) -> StatusCode; // HTTP status code
fn user_message(&self) -> String; // User-friendly message
fn error_code(&self) -> &'static str; // Frontend error code
fn error_category(&self) -> ErrorCategory; // Error categorization
fn error_severity(&self) -> ErrorSeverity; // Severity level
fn suppression_key(&self) -> Option<String>; // For repeated error handling
fn suggested_action(&self) -> Option<String>; // Recovery suggestions
}
Error Type Reference¶
The following table provides a comprehensive overview of all error types, when to use them, and their characteristics:
Error Type | Module | When to Use | Common Scenarios | Example Error Codes | HTTP Status |
---|---|---|---|---|---|
ApiError | errors::mod | Generic API errors when specific types don't apply | Rate limiting, payload validation, generic server errors | BAD_REQUEST , NOT_FOUND , UNAUTHORIZED , INTERNAL_SERVER_ERROR | 400, 401, 404, 500 |
UserError | errors::user | User management and authentication operations | Registration, login, profile updates, permissions | USER_NOT_FOUND , USER_DUPLICATE_USERNAME , USER_PERMISSION_DENIED | 400, 401, 403, 404, 409 |
SourceError | errors::source | File source operations (WebDAV, S3, Local Folder) | Source configuration, sync operations, connection issues | SOURCE_CONNECTION_FAILED , SOURCE_AUTH_FAILED , SOURCE_SYNC_IN_PROGRESS | 400, 401, 404, 409, 503 |
LabelError | errors::label | Document labeling and label management | Creating/editing labels, label assignment, system labels | LABEL_DUPLICATE_NAME , LABEL_IN_USE , LABEL_SYSTEM_MODIFICATION | 400, 403, 404, 409 |
SettingsError | errors::settings | Application and user settings validation | OCR configuration, preferences, validation | SETTINGS_INVALID_LANGUAGE , SETTINGS_VALUE_OUT_OF_RANGE , SETTINGS_READ_ONLY | 400, 403, 404 |
SearchError | errors::search | Document search and indexing operations | Search queries, index operations, result processing | SEARCH_QUERY_TOO_SHORT , SEARCH_INDEX_UNAVAILABLE , SEARCH_TOO_MANY_RESULTS | 400, 503, 429 |
OcrError | ocr::error | OCR processing operations | Tesseract operations, image processing, language detection | OCR_NOT_INSTALLED , OCR_LANG_MISSING , OCR_TIMEOUT | 400, 500, 503 |
DocumentError | routes::documents::crud | Document CRUD operations | File upload, download, metadata operations | DOCUMENT_NOT_FOUND , DOCUMENT_TOO_LARGE | 400, 404, 413, 500 |
Error Type Usage Guidelines¶
ApiError - Generic API Errors¶
- Use when: No specific error type applies
- Best for: Cross-cutting concerns, middleware errors, generic validation
- Avoid when: Domain-specific operations have dedicated error types
- Example: Request rate limiting, malformed JSON, generic server failures
UserError - User Management¶
- Use when: Operations involve user accounts, authentication, or authorization
- Best for: Registration, login, profile management, role/permission checks
- Covers: Account creation, credential validation, access control
- Example: Duplicate usernames, invalid passwords, insufficient permissions
SourceError - File Sources¶
- Use when: Operations involve external file sources (WebDAV, S3, etc.)
- Best for: Source configuration, sync operations, connection management
- Covers: Authentication to external systems, network connectivity, sync status
- Example: WebDAV connection failures, S3 credential issues, sync conflicts
LabelError - Label Management¶
- Use when: Operations involve document labels or label management
- Best for: Label CRUD operations, label assignment to documents
- Covers: Label validation, system label protection, usage tracking
- Example: Duplicate label names, modifying system labels, deleting used labels
SettingsError - Configuration & Settings¶
- Use when: Operations involve application or user settings
- Best for: Configuration validation, preference management, OCR settings
- Covers: Value validation, constraint checking, read-only setting protection
- Example: Invalid OCR languages, out-of-range values, conflicting settings
SearchError - Search Operations¶
- Use when: Operations involve document search or search index management
- Best for: Query validation, index operations, result processing
- Covers: Query syntax, performance limits, index availability
- Example: Short queries, syntax errors, index rebuilding, too many results
OcrError - OCR Processing¶
- Use when: Operations involve OCR processing with Tesseract
- Best for: OCR-specific failures, Tesseract configuration issues
- Covers: Installation checks, language data, processing errors
- Example: Missing Tesseract, language data not found, processing timeouts
DocumentError - Document Operations¶
- Use when: Operations involve document CRUD operations
- Best for: File upload/download, document metadata, storage operations
- Covers: File validation, storage limits, document lifecycle
- Example: File too large, unsupported format, document not found
Error Severity Mapping¶
Error Type | Typical Severity | Reasoning |
---|---|---|
ApiError | Minor to Critical | Depends on specific error - server errors are Critical, validation is Minor |
UserError | Minor to Important | Auth failures are Important, validation errors are Minor |
SourceError | Minor to Important | Connection issues are Important, config errors are Minor |
LabelError | Minor | Usually user input validation, rarely system-critical |
SettingsError | Minor | Configuration errors, typically user-correctable |
SearchError | Minor to Important | Index unavailable is Important, query errors are Minor |
OcrError | Minor to Critical | Missing installation is Critical, processing errors are Minor |
DocumentError | Minor to Important | Storage failures are Important, validation is Minor |
Error Type Decision Tree¶
Use this decision tree to choose the appropriate error type:
Is this a user account/authentication operation?
├─ YES → UserError
└─ NO → Is this a file source operation (WebDAV, S3, Local)?
├─ YES → SourceError
└─ NO → Is this a search/indexing operation?
├─ YES → SearchError
└─ NO → Is this an OCR/Tesseract operation?
├─ YES → OcrError
└─ NO → Is this a document upload/download/CRUD operation?
├─ YES → DocumentError
└─ NO → Is this a label management operation?
├─ YES → LabelError
└─ NO → Is this a settings/configuration operation?
├─ YES → SettingsError
└─ NO → Use ApiError for generic cases
Quick Reference Checklist¶
Before choosing an error type, ask:
- Does the operation primarily involve user accounts, roles, or authentication? → UserError
- Does the operation connect to external file sources? → SourceError
- Does the operation search documents or manage search index? → SearchError
- Does the operation use Tesseract for OCR processing? → OcrError
- Does the operation upload, download, or manage document files? → DocumentError
- Does the operation create, modify, or assign labels? → LabelError
- Does the operation validate or modify application settings? → SettingsError
- None of the above apply? → ApiError
Common Error Mapping Patterns¶
Operation Type | Recommended Error Type | Example Operations |
---|---|---|
User registration/login | UserError | /api/auth/register , /api/auth/login |
File source sync | SourceError | /api/sources/{id}/sync , /api/sources/{id}/test |
Document search | SearchError | /api/search , /api/documents/search |
OCR processing | OcrError | /api/documents/{id}/ocr , /api/ocr/languages |
Document management | DocumentError | /api/documents , /api/documents/{id} |
Label operations | LabelError | /api/labels , /api/documents/{id}/labels |
Settings management | SettingsError | /api/settings , /api/users/{id}/settings |
Generic API operations | ApiError | Rate limiting, payload validation, CORS |
Error Types¶
UserError¶
Handles user management operations:
// Examples
UserError::NotFound
UserError::DuplicateUsername { username: "john_doe" }
UserError::PermissionDenied { reason: "Admin access required" }
UserError::InvalidCredentials
UserError::DeleteRestricted { id, reason: "Cannot delete your own account" }
Error Codes: USER_NOT_FOUND
, USER_DUPLICATE_USERNAME
, USER_PERMISSION_DENIED
, etc.
SourceError¶
Handles file source operations (WebDAV, Local Folder, S3):
// Examples
SourceError::ConnectionFailed { details: "Network timeout" }
SourceError::InvalidPath { path: "/invalid/path" }
SourceError::AuthenticationFailed { name: "my-webdav", reason: "Invalid credentials" }
SourceError::SyncInProgress { name: "backup-source" }
SourceError::ConfigurationInvalid { details: "Missing server URL" }
Error Codes: SOURCE_CONNECTION_FAILED
, SOURCE_AUTH_FAILED
, SOURCE_CONFIG_INVALID
, etc.
LabelError¶
Handles label management:
// Examples
LabelError::DuplicateName { name: "Important" }
LabelError::SystemLabelModification { name: "system-label" }
LabelError::InvalidColor { color: "#gggggg" }
LabelError::LabelInUse { document_count: 42 }
LabelError::MaxLabelsReached { max_labels: 100 }
Error Codes: LABEL_DUPLICATE_NAME
, LABEL_SYSTEM_MODIFICATION
, LABEL_IN_USE
, etc.
SettingsError¶
Handles settings validation and management:
// Examples
SettingsError::InvalidLanguage { language: "xx", available_languages: "en,es,fr" }
SettingsError::ValueOutOfRange { setting_name: "timeout", value: 3600, min: 1, max: 300 }
SettingsError::InvalidOcrConfiguration { details: "DPI too high" }
SettingsError::ConflictingSettings { setting1: "auto_detect", setting2: "fixed_language" }
Error Codes: SETTINGS_INVALID_LANGUAGE
, SETTINGS_VALUE_OUT_OF_RANGE
, etc.
SearchError¶
Handles search operations:
// Examples
SearchError::QueryTooShort { length: 1, min_length: 2 }
SearchError::TooManyResults { result_count: 15000, max_results: 10000 }
SearchError::IndexUnavailable { reason: "Rebuilding index" }
SearchError::InvalidPagination { offset: -1, limit: 0 }
Error Codes: SEARCH_QUERY_TOO_SHORT
, SEARCH_TOO_MANY_RESULTS
, etc.
Integration Features¶
Error Management System¶
All errors automatically integrate with the sophisticated error management system in src/monitoring/error_management.rs
:
- Categorization: Errors are categorized (Auth, Database, Network, etc.)
- Severity Levels: Critical, Important, Minor, Expected
- Intelligent Suppression: Prevents spam from repeated errors
- Structured Logging: Consistent log format with context
HTTP Response Format¶
All errors return consistent JSON responses:
Frontend Error Codes¶
Each error provides a stable error code for frontend handling:
// Frontend can handle specific errors
switch (error.code) {
case 'USER_DUPLICATE_USERNAME':
showUsernameAlreadyExistsMessage();
break;
case 'SOURCE_CONNECTION_FAILED':
showNetworkErrorDialog();
break;
case 'SEARCH_QUERY_TOO_SHORT':
highlightSearchInput();
break;
}
Usage Examples¶
In Route Handlers¶
use crate::errors::user::UserError;
async fn create_user(
auth_user: AuthUser,
State(state): State<Arc<AppState>>,
Json(user_data): Json<CreateUser>,
) -> Result<Json<UserResponse>, UserError> {
require_admin(&auth_user)?;
let user = state
.db
.create_user(user_data)
.await
.map_err(|e| {
let error_msg = e.to_string();
if error_msg.contains("username") && error_msg.contains("unique") {
UserError::duplicate_username(&user_data.username)
} else if error_msg.contains("email") && error_msg.contains("unique") {
UserError::duplicate_email(&user_data.email)
} else {
UserError::internal_server_error(format!("Database error: {}", e))
}
})?;
Ok(Json(user.into()))
}
Convenience Methods¶
All error types provide convenience constructors:
// Instead of verbose enum construction
UserError::DuplicateUsername { username: username.clone() }
// Use convenience methods
UserError::duplicate_username(&username)
UserError::permission_denied("Admin access required")
UserError::not_found_by_id(user_id)
Error Context and Suggestions¶
Errors include contextual information and recovery suggestions:
LabelError::invalid_color("#gggggg")
// Returns: "Invalid color format - use hex format like #0969da"
// Suggestion: "Use a valid hex color format like #0969da or #ff5722"
SourceError::rate_limit_exceeded("my-source", 300)
// Returns: "Rate limit exceeded, try again in 300 seconds"
// Suggestion: "Wait 300 seconds before retrying"
Best Practices¶
1. Use Specific Error Types¶
// Good
return Err(UserError::duplicate_username(&username));
// Avoid
return Err(UserError::BadRequest { message: "Username exists".to_string() });
2. Provide Context¶
// Good
SourceError::connection_failed(format!("Failed to connect to {}: {}", url, e))
// Less helpful
SourceError::connection_failed("Connection failed")
3. Handle Database Errors Thoughtfully¶
.map_err(|e| {
let error_msg = e.to_string();
if error_msg.contains("unique constraint") {
UserError::duplicate_username(&username)
} else if error_msg.contains("not found") {
UserError::not_found()
} else {
UserError::internal_server_error(format!("Database error: {}", e))
}
})?
4. Use Suppression Keys Wisely¶
impl AppError for SearchError {
fn suppression_key(&self) -> Option<String> {
match self {
// Suppress repeated "no results" errors
SearchError::NoResults => Some("search_no_results".to_string()),
// Don't suppress validation errors - users need to see them
SearchError::QueryTooShort { .. } => None,
// Suppress by specific source for connection errors
SearchError::IndexUnavailable { .. } => Some("search_index_unavailable".to_string()),
_ => None,
}
}
}
Migration from Generic Errors¶
When updating existing endpoints:
-
Add error type import:
-
Update function signature:
Replace generic error mapping: Replace basic HTTP status codes with structured error responses.
// Before
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
// After
.map_err(|e| UserError::internal_server_error(format!("Operation failed: {}", e)))?
Update tests: Modify existing tests to expect the new structured error format instead of simple status codes.
// Before
assert_eq!(response.status(), StatusCode::CONFLICT);
// After - check JSON response
let error: serde_json::Value = response.json().await;
assert_eq!(error["code"], "USER_DELETE_RESTRICTED");
Testing¶
The error system includes comprehensive testing to ensure:
- Correct HTTP status codes
- Proper JSON response format
- Error code consistency
- Integration with error management
- Suppression behavior
Run error-specific tests:
Error Code Conventions¶
All error types follow consistent naming conventions for error codes:
Naming Pattern¶
Examples by Type¶
Error Type | Prefix | Example Codes |
---|---|---|
ApiError | None | BAD_REQUEST , NOT_FOUND , UNAUTHORIZED |
UserError | USER_ | USER_NOT_FOUND , USER_DUPLICATE_USERNAME , USER_PERMISSION_DENIED |
SourceError | SOURCE_ | SOURCE_CONNECTION_FAILED , SOURCE_AUTH_FAILED , SOURCE_CONFIG_INVALID |
LabelError | LABEL_ | LABEL_DUPLICATE_NAME , LABEL_IN_USE , LABEL_SYSTEM_MODIFICATION |
SettingsError | SETTINGS_ | SETTINGS_INVALID_LANGUAGE , SETTINGS_VALUE_OUT_OF_RANGE |
SearchError | SEARCH_ | SEARCH_QUERY_TOO_SHORT , SEARCH_INDEX_UNAVAILABLE |
OcrError | OCR_ | OCR_NOT_INSTALLED , OCR_LANG_MISSING , OCR_TIMEOUT |
DocumentError | DOCUMENT_ | DOCUMENT_NOT_FOUND , DOCUMENT_TOO_LARGE |
Code Style Guidelines¶
- Use
SCREAMING_SNAKE_CASE
for all error codes - Start with error type prefix (except ApiError)
- Be descriptive but concise
- Avoid abbreviations unless commonly understood
- Group related errors with consistent sub-prefixes
Practical Examples¶
Complete Implementation Examples¶
User Registration Endpoint¶
use crate::errors::user::UserError;
async fn register_user(
State(state): State<Arc<AppState>>,
Json(user_data): Json<CreateUser>,
) -> Result<Json<UserResponse>, UserError> {
// Validate input
if user_data.username.len() < 3 {
return Err(UserError::invalid_username(
&user_data.username,
"Username must be at least 3 characters"
));
}
// Create user
let user = state.db.create_user(user_data).await
.map_err(|e| {
let error_msg = e.to_string();
if error_msg.contains("username") && error_msg.contains("unique") {
UserError::duplicate_username(&user_data.username)
} else if error_msg.contains("email") && error_msg.contains("unique") {
UserError::duplicate_email(&user_data.email)
} else {
UserError::internal_server_error(format!("Database error: {}", e))
}
})?;
Ok(Json(user.into()))
}
WebDAV Source Test Endpoint¶
use crate::errors::source::SourceError;
async fn test_webdav_connection(
State(state): State<Arc<AppState>>,
Path(source_id): Path<Uuid>,
auth_user: AuthUser,
) -> Result<Json<ConnectionTestResponse>, SourceError> {
let source = state.db.get_source(source_id).await
.map_err(|_| SourceError::not_found_by_id(source_id))?;
// Check ownership
if source.user_id != auth_user.user.id {
return Err(SourceError::permission_denied(
"You can only test your own sources"
));
}
// Test connection
match test_webdav_connection_internal(&source).await {
Ok(()) => Ok(Json(ConnectionTestResponse { success: true })),
Err(e) if e.to_string().contains("authentication") => {
Err(SourceError::authentication_failed(&source.name, &e.to_string()))
},
Err(e) if e.to_string().contains("timeout") => {
Err(SourceError::network_timeout(&source.url, 30))
},
Err(e) => {
Err(SourceError::connection_failed(format!("Connection test failed: {}", e)))
}
}
}
Search Query Endpoint¶
use crate::errors::search::SearchError;
async fn search_documents(
State(state): State<Arc<AppState>>,
Query(params): Query<SearchParams>,
auth_user: AuthUser,
) -> Result<Json<SearchResponse>, SearchError> {
// Validate query length
if params.query.len() < 2 {
return Err(SearchError::query_too_short(params.query.len(), 2));
}
if params.query.len() > 500 {
return Err(SearchError::query_too_long(params.query.len(), 500));
}
// Check pagination
if params.limit > 1000 {
return Err(SearchError::invalid_pagination(params.offset, params.limit));
}
// Perform search
let results = state.search_service.search(¶ms, auth_user.user.id).await
.map_err(|e| match e {
SearchServiceError::IndexUnavailable => {
SearchError::index_unavailable("Search index is being rebuilt")
},
SearchServiceError::TooManyResults(count) => {
SearchError::too_many_results(count, 10000)
},
SearchServiceError::InvalidSyntax(details) => {
SearchError::invalid_syntax(details)
},
_ => SearchError::internal_error(format!("Search failed: {}", e)),
})?;
Ok(Json(results))
}
Monitoring¶
Error metrics are automatically tracked:
- Error rates by type and endpoint
- Suppression statistics for repeated errors
- Severity distribution across the application
- Recovery suggestions utilization
View error dashboards in Grafana or check Prometheus metrics at /metrics
.
Future Enhancements¶
Planned improvements to the error system:
- Internationalization - Multi-language error messages
- Error Analytics - Advanced error pattern detection
- Auto-Recovery - Suggested API retry strategies
- Enhanced Suppression - Time-based and pattern-based suppression
- Error Documentation - Auto-generated API error documentation