A Rust application to showcase badge awards in the AT Protocol ecosystem.
1use axum::response::{IntoResponse, Response};
2use reqwest::StatusCode;
3use thiserror::Error;
4
5/// Main error type for the showcase application
6///
7/// All errors follow the format: error-showcase-<domain>-<number> <message>: <details>
8/// Domains are organized alphabetically: config, consumer, http, process, storage
9#[derive(Error, Debug)]
10pub enum ShowcaseError {
11 // Config domain - Configuration-related errors
12 #[error("error-showcase-config-1 Failed to parse HTTP client timeout: {details}")]
13 /// Failed to parse HTTP client timeout from environment variable.
14 ConfigHttpTimeoutInvalid {
15 /// Details about the parsing error.
16 details: String,
17 },
18
19 #[error("error-showcase-config-2 Failed to parse HTTP port: {port}")]
20 /// Failed to parse HTTP port from environment variable.
21 ConfigHttpPortInvalid {
22 /// The invalid port value.
23 port: String,
24 },
25
26 #[error("error-showcase-config-3 Invalid S3 URL format: {details}")]
27 /// Failed to parse S3 URL format from environment variable.
28 ConfigS3UrlInvalid {
29 /// Details about the S3 URL parsing error.
30 details: String,
31 },
32
33 #[error("error-showcase-config-4 Required feature not enabled: {feature}")]
34 /// Required feature is not enabled in the build.
35 ConfigFeatureNotEnabled {
36 /// The feature that is required but not enabled.
37 feature: String,
38 },
39
40 // Consumer domain - Jetstream consumer errors
41 #[error("error-showcase-consumer-1 Failed to send badge event to queue: {details}")]
42 /// Failed to send badge event to the processing queue.
43 ConsumerQueueSendFailed {
44 /// Details about the send failure.
45 details: String,
46 },
47
48 #[error("error-showcase-consumer-2 Jetstream disconnected: {details}")]
49 /// Jetstream consumer disconnected.
50 ConsumerDisconnected {
51 /// Details about the disconnection.
52 details: String,
53 },
54
55 #[error(
56 "error-showcase-consumer-3 Jetstream disconnect rate exceeded: {disconnect_count} disconnects in {duration_mins} minutes"
57 )]
58 /// Jetstream consumer disconnect rate exceeded the allowable limit.
59 ConsumerDisconnectRateExceeded {
60 /// Number of disconnects in the time period.
61 disconnect_count: usize,
62 /// Duration in minutes for the disconnect rate calculation.
63 duration_mins: u64,
64 },
65
66 // HTTP domain - HTTP server and template errors
67 #[error("error-showcase-http-1 Template rendering failed: {template}")]
68 /// Template rendering failed in HTTP response.
69 HttpTemplateRenderFailed {
70 /// The template that failed to render.
71 template: String,
72 },
73
74 #[error("error-showcase-http-2 Internal server error")]
75 /// Generic internal server error for HTTP responses.
76 HttpInternalServerError,
77
78 // Process domain - Badge processing errors
79 #[error("error-showcase-process-1 Invalid AT-URI format: {uri}")]
80 /// Invalid AT-URI format encountered during processing.
81 ProcessInvalidAturi {
82 /// The invalid URI string.
83 uri: String,
84 },
85
86 #[error("error-showcase-process-2 Failed to resolve identity for {did}: {details}")]
87 /// Identity resolution failed with detailed error information.
88 ProcessIdentityResolutionFailed {
89 /// The DID that could not be resolved.
90 did: String,
91 /// Detailed error information.
92 details: String,
93 },
94
95 #[error("error-showcase-process-3 Failed to fetch badge record: {uri}")]
96 /// Failed to fetch badge record from AT Protocol.
97 ProcessBadgeFetchFailed {
98 /// The badge URI that could not be fetched.
99 uri: String,
100 },
101
102 #[error("error-showcase-process-4 Failed to fetch badge {uri}: {details}")]
103 /// Badge record fetch failed with detailed error information.
104 ProcessBadgeRecordFetchFailed {
105 /// The badge URI that could not be fetched.
106 uri: String,
107 /// Detailed error information.
108 details: String,
109 },
110
111 #[error("error-showcase-process-5 Failed to download badge image: {image_ref}")]
112 /// Failed to download badge image.
113 ProcessImageDownloadFailed {
114 /// Reference to the image that failed to download.
115 image_ref: String,
116 },
117
118 #[error("error-showcase-process-6 Failed to process badge event: {event_type}")]
119 /// Failed to process a badge event from Jetstream.
120 ProcessEventHandlingFailed {
121 /// Type of event that failed to process.
122 event_type: String,
123 },
124
125 #[error("error-showcase-process-7 Image file too large: {size} bytes exceeds 3MB limit")]
126 /// Badge image file exceeds maximum allowed size.
127 ProcessImageTooLarge {
128 /// The actual size of the image in bytes.
129 size: usize,
130 },
131
132 #[error("error-showcase-process-8 Failed to decode image: {details}")]
133 /// Failed to decode badge image data.
134 ProcessImageDecodeFailed {
135 /// Details about the decode error.
136 details: String,
137 },
138
139 #[error("error-showcase-process-9 Unsupported image format: {format}")]
140 /// Badge image is in an unsupported format.
141 ProcessUnsupportedImageFormat {
142 /// The unsupported image format.
143 format: String,
144 },
145
146 #[error(
147 "error-showcase-process-10 Image dimensions too small: {width}x{height}, minimum is 512x512"
148 )]
149 /// Badge image dimensions are below minimum requirements.
150 ProcessImageTooSmall {
151 /// The actual width of the image.
152 width: u32,
153 /// The actual height of the image.
154 height: u32,
155 },
156
157 #[error(
158 "error-showcase-process-11 Image width too small after resize: {width}, minimum is 512"
159 )]
160 /// Badge image width is below minimum after resize.
161 ProcessImageWidthTooSmall {
162 /// The actual width after resize.
163 width: u32,
164 },
165
166 #[error("error-showcase-process-12 No signatures field found in record")]
167 /// Badge record is missing required signatures field.
168 ProcessNoSignaturesField,
169
170 #[error("error-showcase-process-13 Missing issuer field in signature")]
171 /// Signature is missing required issuer field.
172 ProcessMissingIssuerField,
173
174 #[error("error-showcase-process-14 Missing signature field in signature")]
175 /// Signature object is missing required signature field.
176 ProcessMissingSignatureField,
177
178 #[error("error-showcase-process-15 Record serialization failed: {details}")]
179 /// Failed to serialize record for signature verification.
180 ProcessRecordSerializationFailed {
181 /// Details about the serialization error.
182 details: String,
183 },
184
185 #[error("error-showcase-process-16 Signature decoding failed: {details}")]
186 /// Failed to decode signature bytes.
187 ProcessSignatureDecodingFailed {
188 /// Details about the decoding error.
189 details: String,
190 },
191
192 #[error(
193 "error-showcase-process-17 Cryptographic validation failed for issuer {issuer}: {details}"
194 )]
195 /// Cryptographic signature validation failed.
196 ProcessCryptographicValidationFailed {
197 /// The issuer DID whose signature validation failed.
198 issuer: String,
199 /// Detailed error information.
200 details: String,
201 },
202
203 // Storage domain - Database and storage errors
204 #[error("error-showcase-storage-1 Database operation failed: {operation}")]
205 /// Database operation failed.
206 StorageDatabaseFailed {
207 /// Description of the failed operation.
208 operation: String,
209 },
210
211 #[error("error-showcase-storage-2 File storage operation failed: {operation}")]
212 /// File storage operation failed.
213 StorageFileOperationFailed {
214 /// Description of the failed file operation.
215 operation: String,
216 },
217}
218
219/// Result type alias for convenience
220pub type Result<T> = std::result::Result<T, ShowcaseError>;
221
222impl From<sqlx::Error> for ShowcaseError {
223 fn from(err: sqlx::Error) -> Self {
224 ShowcaseError::StorageDatabaseFailed {
225 operation: err.to_string(),
226 }
227 }
228}
229
230impl From<serde_json::Error> for ShowcaseError {
231 fn from(err: serde_json::Error) -> Self {
232 ShowcaseError::ProcessEventHandlingFailed {
233 event_type: err.to_string(),
234 }
235 }
236}
237
238impl From<reqwest::Error> for ShowcaseError {
239 fn from(err: reqwest::Error) -> Self {
240 ShowcaseError::ProcessBadgeFetchFailed {
241 uri: err.to_string(),
242 }
243 }
244}
245
246impl From<image::ImageError> for ShowcaseError {
247 fn from(err: image::ImageError) -> Self {
248 ShowcaseError::ProcessImageDownloadFailed {
249 image_ref: err.to_string(),
250 }
251 }
252}
253
254impl From<tokio::sync::mpsc::error::SendError<crate::consumer::AwardEvent>> for ShowcaseError {
255 fn from(err: tokio::sync::mpsc::error::SendError<crate::consumer::AwardEvent>) -> Self {
256 ShowcaseError::ConsumerQueueSendFailed {
257 details: err.to_string(),
258 }
259 }
260}
261
262impl From<minijinja::Error> for ShowcaseError {
263 fn from(err: minijinja::Error) -> Self {
264 ShowcaseError::HttpTemplateRenderFailed {
265 template: err.to_string(),
266 }
267 }
268}
269
270impl From<std::io::Error> for ShowcaseError {
271 fn from(err: std::io::Error) -> Self {
272 ShowcaseError::ProcessImageDownloadFailed {
273 image_ref: err.to_string(),
274 }
275 }
276}
277
278impl From<std::num::ParseIntError> for ShowcaseError {
279 fn from(err: std::num::ParseIntError) -> Self {
280 ShowcaseError::ConfigHttpPortInvalid {
281 port: err.to_string(),
282 }
283 }
284}
285
286impl From<anyhow::Error> for ShowcaseError {
287 fn from(err: anyhow::Error) -> Self {
288 ShowcaseError::ProcessEventHandlingFailed {
289 event_type: err.to_string(),
290 }
291 }
292}
293
294impl From<atproto_record::errors::AturiError> for ShowcaseError {
295 fn from(err: atproto_record::errors::AturiError) -> Self {
296 ShowcaseError::ProcessInvalidAturi {
297 uri: err.to_string(),
298 }
299 }
300}
301
302impl IntoResponse for ShowcaseError {
303 fn into_response(self) -> Response {
304 tracing::error!(error = ?self, "internal server error");
305 (StatusCode::INTERNAL_SERVER_ERROR).into_response()
306 }
307}