Highly ambitious ATProtocol AppView service and sdks
1//! Error types for the Slices API.
2//!
3//! All error types follow the pattern: `error-slices-{domain}-{number} {message}`
4//! This provides consistent error tracking and debugging across the application.
5//!
6//! ## Error Hierarchy
7//!
8//! ```text
9//! AppError (HTTP boundary layer)
10//! ├─> DatabaseError (data layer)
11//! ├─> SyncError (sync service)
12//! ├─> JetstreamError (real-time processing)
13//! ├─> ActorResolverError (identity resolution)
14//! └─> BlobUploadError (ATProto extensions)
15//! ```
16
17use axum::{
18 Json,
19 http::StatusCode,
20 response::{IntoResponse, Response},
21};
22use thiserror::Error;
23
24// =============================================================================
25// Database Layer Errors
26// =============================================================================
27
28/// Database operation errors from the data access layer.
29#[derive(Error, Debug)]
30pub enum DatabaseError {
31 #[error("error-slices-database-1 SQL query failed: {0}")]
32 SqlQuery(#[from] sqlx::Error),
33
34 #[error("error-slices-database-2 Record not found: {uri}")]
35 RecordNotFound { uri: String },
36}
37
38// =============================================================================
39// Sync Service Errors
40// =============================================================================
41
42/// Errors from background sync operations with ATProto relay.
43#[derive(Error, Debug)]
44pub enum SyncError {
45 #[error("error-slices-sync-1 HTTP request failed: {0}")]
46 HttpRequest(#[from] reqwest::Error),
47
48 #[error("error-slices-sync-2 Database operation failed: {0}")]
49 Database(#[from] DatabaseError),
50
51 #[error("error-slices-sync-3 JSON parsing failed: {0}")]
52 JsonParse(#[from] serde_json::Error),
53
54 #[error("error-slices-sync-4 Failed to list repos for collection: {status}")]
55 ListRepos { status: u16 },
56
57 #[error("error-slices-sync-5 Failed to list records: {status}")]
58 ListRecords { status: u16 },
59
60 #[error("error-slices-sync-6 Task join failed: {0}")]
61 TaskJoin(#[from] tokio::task::JoinError),
62
63 #[error("error-slices-sync-7 Generic error: {0}")]
64 Generic(String),
65
66 #[error("error-slices-sync-8 Database query failed: {0}")]
67 DatabaseQuery(String),
68
69 #[error("error-slices-sync-9 Job cancelled by user")]
70 Cancelled,
71}
72
73// =============================================================================
74// Jetstream / Real-time Processing Errors
75// =============================================================================
76
77/// Errors from Jetstream event stream processing.
78#[derive(Error, Debug)]
79pub enum JetstreamError {
80 #[error("error-slices-jetstream-1 Connection failed: {message}")]
81 ConnectionFailed { message: String },
82
83 #[error("error-slices-jetstream-2 Database error: {0}")]
84 Database(#[from] DatabaseError),
85}
86
87// =============================================================================
88// ATProto Identity Resolution Errors
89// =============================================================================
90
91/// Errors from DID and handle resolution operations.
92#[derive(Error, Debug)]
93pub enum ActorResolverError {
94 #[error("error-slice-actor-1 Failed to resolve DID: {0}")]
95 ResolveFailed(String),
96
97 #[error("error-slice-actor-2 Failed to parse DID: {0}")]
98 ParseFailed(String),
99
100 #[error("error-slice-actor-3 Subject resolved to handle instead of DID")]
101 InvalidSubject,
102}
103
104// =============================================================================
105// ATProto Extensions Errors
106// =============================================================================
107
108/// Errors from ATProto blob upload operations.
109#[derive(Error, Debug)]
110pub enum BlobUploadError {
111 #[error("error-slice-blob-1 HTTP request failed: {0}")]
112 HttpRequest(#[from] reqwest_middleware::Error),
113
114 #[error("error-slice-blob-2 JSON parsing failed: {0}")]
115 JsonParse(#[from] serde_json::Error),
116
117 #[error("error-slice-blob-3 DPoP proof creation failed: {0}")]
118 DPoPProof(String),
119
120 #[error("error-slice-blob-4 Upload request failed: {status} - {message}")]
121 UploadFailed { status: u16, message: String },
122}
123
124// =============================================================================
125// Core Application Errors (HTTP Boundary Layer)
126// =============================================================================
127
128/// Top-level application errors for HTTP handlers and server operations.
129///
130/// This is the boundary layer that converts domain errors into HTTP responses.
131/// Domain-specific errors are wrapped and converted to appropriate HTTP status codes.
132#[derive(Error, Debug)]
133pub enum AppError {
134 #[error("error-slices-app-1 Database error: {0}")]
135 Database(#[from] DatabaseError),
136
137 #[error("error-slices-app-2 Sync error: {0}")]
138 Sync(#[from] SyncError),
139
140 #[error("error-slices-app-3 Jetstream error: {0}")]
141 Jetstream(#[from] JetstreamError),
142
143 #[error("error-slices-app-4 Actor resolution error: {0}")]
144 ActorResolver(#[from] ActorResolverError),
145
146 #[error("error-slices-app-5 Blob upload error: {0}")]
147 BlobUpload(#[from] BlobUploadError),
148
149 #[error("error-slices-app-6 Database connection failed: {0}")]
150 DatabaseConnection(#[from] sqlx::Error),
151
152 #[error("error-slices-app-7 Database migration failed: {0}")]
153 Migration(#[from] sqlx::migrate::MigrateError),
154
155 #[error("error-slices-app-8 Server bind failed: {0}")]
156 ServerBind(#[from] std::io::Error),
157
158 #[error("error-slices-app-9 Cache error: {0}")]
159 Cache(#[from] anyhow::Error),
160
161 #[error("error-slices-app-10 Bad request: {0}")]
162 BadRequest(String),
163
164 #[error("error-slices-app-11 Resource not found: {0}")]
165 NotFound(String),
166
167 #[error("error-slices-app-12 Authentication required: {0}")]
168 AuthRequired(String),
169
170 #[error("error-slices-app-13 Forbidden: {0}")]
171 Forbidden(String),
172
173 #[error("error-slices-app-14 Internal server error: {0}")]
174 Internal(String),
175}
176
177impl From<StatusCode> for AppError {
178 fn from(status: StatusCode) -> Self {
179 match status {
180 StatusCode::BAD_REQUEST => AppError::BadRequest("Bad request".to_string()),
181 StatusCode::UNAUTHORIZED => {
182 AppError::AuthRequired("Authentication required".to_string())
183 }
184 StatusCode::FORBIDDEN => AppError::Forbidden("Forbidden".to_string()),
185 StatusCode::NOT_FOUND => AppError::NotFound("Not found".to_string()),
186 _ => AppError::Internal(format!("HTTP error: {}", status)),
187 }
188 }
189}
190
191impl IntoResponse for AppError {
192 fn into_response(self) -> Response {
193 let (status, error_name, error_message) = match &self {
194 AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "BadRequest", msg.clone()),
195 AppError::NotFound(msg) => (StatusCode::NOT_FOUND, "NotFound", msg.clone()),
196 AppError::AuthRequired(msg) => (
197 StatusCode::UNAUTHORIZED,
198 "AuthenticationRequired",
199 msg.clone(),
200 ),
201 AppError::Forbidden(msg) => (StatusCode::FORBIDDEN, "Forbidden", msg.clone()),
202 AppError::Internal(msg) => (
203 StatusCode::INTERNAL_SERVER_ERROR,
204 "InternalServerError",
205 msg.clone(),
206 ),
207
208 // Domain errors - all map to internal server error
209 AppError::Database(e) => (
210 StatusCode::INTERNAL_SERVER_ERROR,
211 "InternalServerError",
212 e.to_string(),
213 ),
214 AppError::Sync(e) => (
215 StatusCode::INTERNAL_SERVER_ERROR,
216 "InternalServerError",
217 e.to_string(),
218 ),
219 AppError::Jetstream(e) => (
220 StatusCode::INTERNAL_SERVER_ERROR,
221 "InternalServerError",
222 e.to_string(),
223 ),
224 AppError::ActorResolver(e) => (
225 StatusCode::INTERNAL_SERVER_ERROR,
226 "InternalServerError",
227 e.to_string(),
228 ),
229 AppError::BlobUpload(e) => (
230 StatusCode::INTERNAL_SERVER_ERROR,
231 "InternalServerError",
232 e.to_string(),
233 ),
234
235 // Infrastructure errors
236 AppError::DatabaseConnection(e) => (
237 StatusCode::INTERNAL_SERVER_ERROR,
238 "InternalServerError",
239 e.to_string(),
240 ),
241 AppError::Migration(e) => (
242 StatusCode::INTERNAL_SERVER_ERROR,
243 "InternalServerError",
244 e.to_string(),
245 ),
246 AppError::ServerBind(e) => (
247 StatusCode::INTERNAL_SERVER_ERROR,
248 "InternalServerError",
249 e.to_string(),
250 ),
251 AppError::Cache(e) => (
252 StatusCode::INTERNAL_SERVER_ERROR,
253 "InternalServerError",
254 e.to_string(),
255 ),
256 };
257
258 let body = Json(serde_json::json!({
259 "error": error_name,
260 "message": error_message
261 }));
262
263 (status, body).into_response()
264 }
265}