forked from
slices.network/slices
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
67// =============================================================================
68// Jetstream / Real-time Processing Errors
69// =============================================================================
70
71/// Errors from Jetstream event stream processing.
72#[derive(Error, Debug)]
73pub enum JetstreamError {
74 #[error("error-slices-jetstream-1 Connection failed: {message}")]
75 ConnectionFailed { message: String },
76
77 #[error("error-slices-jetstream-2 Database error: {0}")]
78 Database(#[from] DatabaseError),
79}
80
81// =============================================================================
82// ATProto Identity Resolution Errors
83// =============================================================================
84
85/// Errors from DID and handle resolution operations.
86#[derive(Error, Debug)]
87pub enum ActorResolverError {
88 #[error("error-slice-actor-1 Failed to resolve DID: {0}")]
89 ResolveFailed(String),
90
91 #[error("error-slice-actor-2 Failed to parse DID: {0}")]
92 ParseFailed(String),
93
94 #[error("error-slice-actor-3 Subject resolved to handle instead of DID")]
95 InvalidSubject,
96}
97
98// =============================================================================
99// ATProto Extensions Errors
100// =============================================================================
101
102/// Errors from ATProto blob upload operations.
103#[derive(Error, Debug)]
104pub enum BlobUploadError {
105 #[error("error-slice-blob-1 HTTP request failed: {0}")]
106 HttpRequest(#[from] reqwest_middleware::Error),
107
108 #[error("error-slice-blob-2 JSON parsing failed: {0}")]
109 JsonParse(#[from] serde_json::Error),
110
111 #[error("error-slice-blob-3 DPoP proof creation failed: {0}")]
112 DPoPProof(String),
113
114 #[error("error-slice-blob-4 Upload request failed: {status} - {message}")]
115 UploadFailed { status: u16, message: String },
116}
117
118// =============================================================================
119// Core Application Errors (HTTP Boundary Layer)
120// =============================================================================
121
122/// Top-level application errors for HTTP handlers and server operations.
123///
124/// This is the boundary layer that converts domain errors into HTTP responses.
125/// Domain-specific errors are wrapped and converted to appropriate HTTP status codes.
126#[derive(Error, Debug)]
127pub enum AppError {
128 #[error("error-slices-app-1 Database error: {0}")]
129 Database(#[from] DatabaseError),
130
131 #[error("error-slices-app-2 Sync error: {0}")]
132 Sync(#[from] SyncError),
133
134 #[error("error-slices-app-3 Jetstream error: {0}")]
135 Jetstream(#[from] JetstreamError),
136
137 #[error("error-slices-app-4 Actor resolution error: {0}")]
138 ActorResolver(#[from] ActorResolverError),
139
140 #[error("error-slices-app-5 Blob upload error: {0}")]
141 BlobUpload(#[from] BlobUploadError),
142
143 #[error("error-slices-app-6 Database connection failed: {0}")]
144 DatabaseConnection(#[from] sqlx::Error),
145
146 #[error("error-slices-app-7 Database migration failed: {0}")]
147 Migration(#[from] sqlx::migrate::MigrateError),
148
149 #[error("error-slices-app-8 Server bind failed: {0}")]
150 ServerBind(#[from] std::io::Error),
151
152 #[error("error-slices-app-9 Cache error: {0}")]
153 Cache(#[from] anyhow::Error),
154
155 #[error("error-slices-app-10 Bad request: {0}")]
156 BadRequest(String),
157
158 #[error("error-slices-app-11 Resource not found: {0}")]
159 NotFound(String),
160
161 #[error("error-slices-app-12 Authentication required: {0}")]
162 AuthRequired(String),
163
164 #[error("error-slices-app-13 Forbidden: {0}")]
165 Forbidden(String),
166
167 #[error("error-slices-app-14 Internal server error: {0}")]
168 Internal(String),
169}
170
171impl From<StatusCode> for AppError {
172 fn from(status: StatusCode) -> Self {
173 match status {
174 StatusCode::BAD_REQUEST => AppError::BadRequest("Bad request".to_string()),
175 StatusCode::UNAUTHORIZED => {
176 AppError::AuthRequired("Authentication required".to_string())
177 }
178 StatusCode::FORBIDDEN => AppError::Forbidden("Forbidden".to_string()),
179 StatusCode::NOT_FOUND => AppError::NotFound("Not found".to_string()),
180 _ => AppError::Internal(format!("HTTP error: {}", status)),
181 }
182 }
183}
184
185impl IntoResponse for AppError {
186 fn into_response(self) -> Response {
187 let (status, error_name, error_message) = match &self {
188 AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "BadRequest", msg.clone()),
189 AppError::NotFound(msg) => (StatusCode::NOT_FOUND, "NotFound", msg.clone()),
190 AppError::AuthRequired(msg) => (
191 StatusCode::UNAUTHORIZED,
192 "AuthenticationRequired",
193 msg.clone(),
194 ),
195 AppError::Forbidden(msg) => (StatusCode::FORBIDDEN, "Forbidden", msg.clone()),
196 AppError::Internal(msg) => (
197 StatusCode::INTERNAL_SERVER_ERROR,
198 "InternalServerError",
199 msg.clone(),
200 ),
201
202 // Domain errors - all map to internal server error
203 AppError::Database(e) => (
204 StatusCode::INTERNAL_SERVER_ERROR,
205 "InternalServerError",
206 e.to_string(),
207 ),
208 AppError::Sync(e) => (
209 StatusCode::INTERNAL_SERVER_ERROR,
210 "InternalServerError",
211 e.to_string(),
212 ),
213 AppError::Jetstream(e) => (
214 StatusCode::INTERNAL_SERVER_ERROR,
215 "InternalServerError",
216 e.to_string(),
217 ),
218 AppError::ActorResolver(e) => (
219 StatusCode::INTERNAL_SERVER_ERROR,
220 "InternalServerError",
221 e.to_string(),
222 ),
223 AppError::BlobUpload(e) => (
224 StatusCode::INTERNAL_SERVER_ERROR,
225 "InternalServerError",
226 e.to_string(),
227 ),
228
229 // Infrastructure errors
230 AppError::DatabaseConnection(e) => (
231 StatusCode::INTERNAL_SERVER_ERROR,
232 "InternalServerError",
233 e.to_string(),
234 ),
235 AppError::Migration(e) => (
236 StatusCode::INTERNAL_SERVER_ERROR,
237 "InternalServerError",
238 e.to_string(),
239 ),
240 AppError::ServerBind(e) => (
241 StatusCode::INTERNAL_SERVER_ERROR,
242 "InternalServerError",
243 e.to_string(),
244 ),
245 AppError::Cache(e) => (
246 StatusCode::INTERNAL_SERVER_ERROR,
247 "InternalServerError",
248 e.to_string(),
249 ),
250 };
251
252 let body = Json(serde_json::json!({
253 "error": error_name,
254 "message": error_message
255 }));
256
257 (status, body).into_response()
258 }
259}