A Rust application to showcase badge awards in the AT Protocol ecosystem.
at main 10 kB view raw
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}