The smokesignal.events web application

refactor: error cleanup

Signed-off-by: Nick Gerakines <nick.gerakines@gmail.com>

+398 -149
+17 -1
CLAUDE.md
··· 232 232 - `atproto-client` - AT Protocol client functionality 233 233 - `atproto-record` - Record management 234 234 - `atproto-jetstream` - Firehose event streaming 235 - - `atproto-xrpcs` - XRPC server implementation 235 + - `atproto-xrpcs` - XRPC server implementation 236 + 237 + ## Error Handling 238 + 239 + All error strings must use this format: 240 + 241 + error-smokesignal-<domain>-<number> <message>: <details> 242 + 243 + Example errors: 244 + 245 + * error-smokesignal-resolve-1 Multiple DIDs resolved for method 246 + * error-smokesignal-plc-1 HTTP request failed: https://google.com/ Not Found 247 + * error-smokesignal-key-1 Error decoding key: invalid 248 + 249 + Errors should be represented as enums using the `thiserror` library when possible using `src/errors.rs` as a reference and example. 250 + 251 + Avoid creating new errors with the `anyhow!(...)` macro.
+2 -4
src/config.rs
··· 184 184 OAuthBackendConfig::ATProtocol { signing_keys } => { 185 185 let item = signing_keys.as_ref().first(); 186 186 item.map(|(key, value)| (key.clone(), value.clone())) 187 - .ok_or(anyhow::anyhow!("signing keys is empty")) 187 + .ok_or(ConfigError::SigningKeysEmpty.into()) 188 188 } 189 - OAuthBackendConfig::AIP { .. } => Err(anyhow::anyhow!( 190 - "signing keys not available for AIP OAuth backend" 191 - )), 189 + OAuthBackendConfig::AIP { .. } => Err(ConfigError::SigningKeysNotAvailableForAIP.into()), 192 190 } 193 191 } 194 192
+32 -18
src/config_errors.rs
··· 10 10 /// 11 11 /// This error occurs when the application starts up and a required 12 12 /// environment variable is missing from the execution environment. 13 - #[error("error-config-1 {0} must be set")] 13 + #[error("error-smokesignal-config-1 {0} must be set")] 14 14 EnvVarRequired(String), 15 15 16 16 /// Error when the signing keys file cannot be read. ··· 18 18 /// This error occurs when the application fails to read the file 19 19 /// containing signing keys, typically due to file system permissions 20 20 /// or missing file issues. 21 - #[error("error-config-2 Unable to read signing keys file: {0:?}")] 21 + #[error("error-smokesignal-config-2 Unable to read signing keys file: {0:?}")] 22 22 ReadSigningKeysFailed(std::io::Error), 23 23 24 24 /// Error when the signing keys file cannot be parsed. 25 25 /// 26 26 /// This error occurs when the signing keys file contains malformed JSON 27 27 /// that cannot be properly deserialized. 28 - #[error("error-config-3 Unable to parse signing keys file: {0:?}")] 28 + #[error("error-smokesignal-config-3 Unable to parse signing keys file: {0:?}")] 29 29 ParseSigningKeysFailed(serde_json::Error), 30 30 31 31 /// Error when no valid signing keys are found. 32 32 /// 33 33 /// This error occurs when the signing keys file does not contain any 34 34 /// valid keys that the application can use for signing operations. 35 - #[error("error-config-4 Signing keys must contain at least one valid key")] 35 + #[error("error-smokesignal-config-4 Signing keys must contain at least one valid key")] 36 36 EmptySigningKeys, 37 37 38 38 /// Error when no valid OAuth active keys are found. 39 39 /// 40 40 /// This error occurs when the configuration does not include any 41 41 /// valid keys that can be used for OAuth operations. 42 - #[error("error-config-6 OAuth active keys must contain at least one valid key")] 42 + #[error("error-smokesignal-config-5 OAuth active keys must contain at least one valid key")] 43 43 EmptyOAuthActiveKeys, 44 44 45 45 /// Error when no valid invitation active keys are found. 46 46 /// 47 47 /// This error occurs when the configuration does not include any 48 48 /// valid keys that can be used for invitation operations. 49 - #[error("error-config-7 Invitation active keys must contain at least one valid key")] 49 + #[error("error-smokesignal-config-6 Invitation active keys must contain at least one valid key")] 50 50 EmptyInvitationActiveKeys, 51 51 52 52 /// Error when the PORT environment variable cannot be parsed. 53 53 /// 54 54 /// This error occurs when the PORT environment variable contains a value 55 55 /// that cannot be parsed as a valid u16 integer. 56 - #[error("error-config-8 Parsing PORT into u16 failed: {0:?}")] 56 + #[error("error-smokesignal-config-7 Parsing PORT into u16 failed: {0:?}")] 57 57 PortParsingFailed(std::num::ParseIntError), 58 58 59 59 /// Error when the HTTP_COOKIE_KEY cannot be decoded. 60 60 /// 61 61 /// This error occurs when the HTTP_COOKIE_KEY environment variable 62 62 /// contains a value that is not valid base64-encoded data. 63 - #[error("error-config-9 Unable to base64 decode HTTP_COOKIE_KEY: {0:?}")] 63 + #[error("error-smokesignal-config-8 Unable to base64 decode HTTP_COOKIE_KEY: {0:?}")] 64 64 CookieKeyDecodeFailed(base64::DecodeSliceError), 65 65 66 66 /// Error when the decoded HTTP_COOKIE_KEY cannot be processed. 67 67 /// 68 68 /// This error occurs when the decoded HTTP_COOKIE_KEY has an invalid 69 69 /// format or length that prevents it from being used. 70 - #[error("error-config-10 Unable to process decoded HTTP_COOKIE_KEY")] 70 + #[error("error-smokesignal-config-9 Unable to process decoded HTTP_COOKIE_KEY")] 71 71 CookieKeyProcessFailed, 72 72 73 73 /// Error when version information is not available. 74 74 /// 75 75 /// This error occurs when neither GIT_HASH nor CARGO_PKG_VERSION 76 76 /// environment variables are set, preventing version identification. 77 - #[error("error-config-11 One of GIT_HASH or CARGO_PKG_VERSION must be set")] 77 + #[error("error-smokesignal-config-10 One of GIT_HASH or CARGO_PKG_VERSION must be set")] 78 78 VersionNotSet, 79 79 80 80 /// Error when a referenced signing key is not found. 81 81 /// 82 82 /// This error occurs when attempting to use a signing key that 83 83 /// does not exist in the loaded signing keys configuration. 84 - #[error("error-config-12 Signing key not found")] 84 + #[error("error-smokesignal-config-11 Signing key not found")] 85 85 SigningKeyNotFound, 86 86 87 87 /// Error when a DNS nameserver IP cannot be parsed. 88 88 /// 89 89 /// This error occurs when the DNS_NAMESERVERS environment variable contains 90 90 /// an IP address that cannot be parsed as a valid IpAddr. 91 - #[error("error-config-13 Unable to parse nameserver IP '{0}': {1}")] 91 + #[error("error-smokesignal-config-12 Unable to parse nameserver IP '{0}': {1}")] 92 92 NameserverParsingFailed(String, std::net::AddrParseError), 93 93 94 94 /// Error when the signing keys file is not found. 95 95 /// 96 96 /// This error occurs when the file specified in the SIGNING_KEYS environment 97 97 /// variable does not exist on the file system. 98 - #[error("error-config-14 Signing keys file not found: {0}")] 98 + #[error("error-smokesignal-config-13 Signing keys file not found: {0}")] 99 99 SigningKeysFileNotFound(String), 100 100 101 101 /// Error when the signing keys file is empty. 102 102 /// 103 103 /// This error occurs when the file specified in the SIGNING_KEYS environment 104 104 /// variable exists but contains no data. 105 - #[error("error-config-15 Signing keys file is empty")] 105 + #[error("error-smokesignal-config-14 Signing keys file is empty")] 106 106 EmptySigningKeysFile, 107 107 108 108 /// Error when the JWKS structure doesn't contain any keys. 109 109 /// 110 110 /// This error occurs when the signing keys file contains a valid JWKS structure, 111 111 /// but the 'keys' array is empty. 112 - #[error("error-config-16 No keys found in JWKS")] 112 + #[error("error-smokesignal-config-15 No keys found in JWKS")] 113 113 MissingKeysInJWKS, 114 114 115 115 /// Error when signing keys fail validation. 116 116 /// 117 117 /// This error occurs when the signing keys file contains keys 118 118 /// that fail validation checks (such as having invalid format). 119 - #[error("error-config-17 Signing keys validation failed: {0:?}")] 119 + #[error("error-smokesignal-config-16 Signing keys validation failed: {0:?}")] 120 120 SigningKeysValidationFailed(Vec<String>), 121 121 122 122 /// Error when AIP OAuth configuration is incomplete. ··· 124 124 /// This error occurs when oauth_backend is set to "aip" but 125 125 /// required AIP configuration values are missing. 126 126 #[error( 127 - "error-config-18 When oauth_backend is 'aip', AIP_HOSTNAME, AIP_CLIENT_ID, and AIP_CLIENT_SECRET must all be set" 127 + "error-smokesignal-config-17 When oauth_backend is 'aip', AIP_HOSTNAME, AIP_CLIENT_ID, and AIP_CLIENT_SECRET must all be set" 128 128 )] 129 129 AipConfigurationIncomplete, 130 130 ··· 132 132 /// 133 133 /// This error occurs when the OAUTH_BACKEND environment variable 134 134 /// contains a value other than "aip" or "pds". 135 - #[error("error-config-19 oauth_backend must be either 'aip' or 'pds', got: {0}")] 135 + #[error("error-smokesignal-config-18 oauth_backend must be either 'aip' or 'pds', got: {0}")] 136 136 InvalidOAuthBackend(String), 137 + 138 + /// Error when signing keys list is empty. 139 + /// 140 + /// This error occurs when attempting to select an OAuth signing key 141 + /// but the signing keys list contains no entries. 142 + #[error("error-smokesignal-config-19 signing keys is empty")] 143 + SigningKeysEmpty, 144 + 145 + /// Error when signing keys are not available for AIP OAuth backend. 146 + /// 147 + /// This error occurs when attempting to access signing keys 148 + /// while using the AIP OAuth backend, which doesn't support signing keys. 149 + #[error("error-smokesignal-config-20 signing keys not available for AIP OAuth backend")] 150 + SigningKeysNotAvailableForAIP, 137 151 }
+1 -1
src/errors.rs
··· 2 2 //! 3 3 //! All errors are represented as a string in this format: 4 4 //! 5 - //! "error-<domain>-<number> <message>: <details>" 5 + //! "error-smokesignal-<domain>-<number> <message>: <details>" 6 6 //! 7 7 //! The first part containing the "error-" prefix, domain, and number, is used 8 8 //! to uniquely identify the error. This standard code format is used to convey
+4 -4
src/http/errors/common_error.rs
··· 7 7 /// 8 8 /// This error occurs when a URL contains a handle slug that doesn't conform 9 9 /// to the expected format or contains invalid characters. 10 - #[error("error-common-1 Invalid handle slug")] 10 + #[error("error-smokesignal-common-1 Invalid handle slug")] 11 11 InvalidHandleSlug, 12 12 13 13 /// Error when a user lacks permission for an action. 14 14 /// 15 15 /// This error occurs when a user attempts to perform an action they 16 16 /// are not authorized to do, such as modifying another user's data. 17 - #[error("error-common-2 Not authorized to perform this action")] 17 + #[error("error-smokesignal-common-2 Not authorized to perform this action")] 18 18 NotAuthorized, 19 19 20 20 /// Error when a required field is missing. 21 21 /// 22 22 /// This error occurs when a form or request is missing a mandatory field 23 23 /// that is needed to complete the operation. 24 - #[error("error-common-4 Required field not provided")] 24 + #[error("error-smokesignal-common-3 Required field not provided")] 25 25 FieldRequired, 26 26 27 27 /// Error when event data has an invalid format or is corrupted. 28 28 /// 29 29 /// This error occurs when event data doesn't match the expected format 30 30 /// or appears to be corrupted or tampered with. 31 - #[error("error-common-9 Invalid event format or corrupted data")] 31 + #[error("error-smokesignal-common-4 Invalid event format or corrupted data")] 32 32 InvalidEventFormat, 33 33 }
+9 -2
src/http/errors/create_event_errors.rs
··· 10 10 /// 11 11 /// This error occurs when a user attempts to create an event without 12 12 /// specifying a name, which is a required field. 13 - #[error("error-create-event-1 Name not set")] 13 + #[error("error-smokesignal-create-event-1 Name not set")] 14 14 NameNotSet, 15 15 16 16 /// Error when the event description is not provided. 17 17 /// 18 18 /// This error occurs when a user attempts to create an event without 19 19 /// specifying a description, which is a required field. 20 - #[error("error-create-event-2 Description not set")] 20 + #[error("error-smokesignal-create-event-2 Description not set")] 21 21 DescriptionNotSet, 22 + 23 + /// Error when the AT Protocol server returns an error response. 24 + /// 25 + /// This error occurs when the PDS or other AT Protocol service 26 + /// returns an error response during event record creation. 27 + #[error("error-smokesignal-create-event-3 Server error: {message}")] 28 + ServerError { message: String }, 22 29 }
+15
src/http/errors/create_rsvp_errors.rs
··· 1 + use thiserror::Error; 2 + 3 + /// Represents errors that can occur during RSVP creation. 4 + /// 5 + /// These errors are typically triggered during the process of creating 6 + /// and storing RSVP records in the AT Protocol ecosystem. 7 + #[derive(Debug, Error)] 8 + pub(crate) enum CreateRsvpError { 9 + /// Error when the AT Protocol server returns an error response. 10 + /// 11 + /// This error occurs when the PDS or other AT Protocol service 12 + /// returns an error response during RSVP record creation. 13 + #[error("error-smokesignal-create-rsvp-1 Server error: {message}")] 14 + ServerError { message: String }, 15 + }
+22
src/http/errors/delete_event_errors.rs
··· 1 + use thiserror::Error; 2 + 3 + /// Represents errors that can occur during event deletion. 4 + /// 5 + /// These errors are typically triggered during validation or authorization 6 + /// checks when a user attempts to delete an event. 7 + #[derive(Debug, Error)] 8 + pub(crate) enum DeleteEventError { 9 + /// Error when the identity does not have an event with the specified AT-URI. 10 + /// 11 + /// This error occurs when a user attempts to delete an event that doesn't 12 + /// exist in their collection or when the AT-URI is invalid. 13 + #[error("error-smokesignal-delete-event-1 identity does not have event with that AT-URI: {aturi}")] 14 + EventNotFound { aturi: String }, 15 + 16 + /// Error when there is an authentication configuration mismatch. 17 + /// 18 + /// This error occurs when the authentication type doesn't match the 19 + /// configured OAuth backend, indicating a system configuration issue. 20 + #[error("error-smokesignal-delete-event-2 Authentication configuration mismatch")] 21 + AuthenticationConfigurationMismatch, 22 + }
+11 -4
src/http/errors/edit_event_error.rs
··· 11 11 /// This error occurs when a user attempts to edit an event that they 12 12 /// do not have permission to modify, typically because they are not 13 13 /// the event creator or an administrator. 14 - #[error("error-edit-event-2 Not authorized to edit this event")] 14 + #[error("error-smokesignal-edit-event-1 Not authorized to edit this event")] 15 15 NotAuthorized, 16 16 17 17 /// Error when attempting to edit an unsupported event type. ··· 20 20 /// does not support editing, as only community calendar events can be 21 21 /// modified after creation. 22 22 #[error( 23 - "error-edit-event-3 Unsupported event type. Only community calendar events can be edited" 23 + "error-smokesignal-edit-event-2 Unsupported event type. Only community calendar events can be edited" 24 24 )] 25 25 UnsupportedEventType, 26 26 ··· 28 28 /// 29 29 /// This error occurs when a user attempts to modify location information for an event 30 30 /// that has multiple locations defined. Such events can only be edited through the API. 31 - #[error("error-edit-event-4 Cannot edit locations: Event has multiple locations")] 31 + #[error("error-smokesignal-edit-event-3 Cannot edit locations: Event has multiple locations")] 32 32 MultipleLocationsPresent, 33 33 34 34 /// Error when attempting to edit location data on an event that has an unsupported location type. 35 35 /// 36 36 /// This error occurs when a user attempts to modify location information for an event 37 37 /// that has a location type that is not supported for editing through the web interface. 38 - #[error("error-edit-event-5 Cannot edit locations: Event has unsupported location type")] 38 + #[error("error-smokesignal-edit-event-4 Cannot edit locations: Event has unsupported location type")] 39 39 UnsupportedLocationType, 40 + 41 + /// Error when the AT Protocol server returns an error response. 42 + /// 43 + /// This error occurs when the PDS or other AT Protocol service 44 + /// returns an error response during event record editing. 45 + #[error("error-smokesignal-edit-event-5 Server error: {message}")] 46 + ServerError { message: String }, 40 47 }
+3 -3
src/http/errors/event_view_errors.rs
··· 10 10 /// 11 11 /// This error occurs when an event view request specifies a collection 12 12 /// name that doesn't exist or isn't supported by the system. 13 - #[error("error-event-view-1 Invalid collection: {0}")] 13 + #[error("error-smokesignal-event-view-1 Invalid collection: {0}")] 14 14 InvalidCollection(String), 15 15 16 16 /// Error when an event name is missing. 17 17 /// 18 18 /// This error occurs when attempting to view an event that is missing 19 19 /// a required name field, which is necessary for display. 20 - #[error("error-event-view-2 Event name is missing")] 20 + #[error("error-smokesignal-event-view-2 Event name is missing")] 21 21 MissingEventName, 22 22 23 23 /// Error when RSVP count calculation fails. 24 24 /// 25 25 /// This error occurs when the system fails to retrieve or calculate 26 26 /// the RSVP counts (going, interested, not going) for an event. 27 - #[error("error-event-view-3 Failed to hydrate event RSVP counts: {0}")] 27 + #[error("error-smokesignal-event-view-3 Failed to hydrate event RSVP counts: {0}")] 28 28 FailedToHydrateRsvpCounts(String), 29 29 }
+1 -1
src/http/errors/import_error.rs
··· 10 10 /// 11 11 /// This error occurs when attempting to retrieve a list of Smokesignal 12 12 /// events during an import operation fails, preventing the import. 13 - #[error("error-import-1 Failed to list Smokesignal events: {0}")] 13 + #[error("error-smokesignal-import-1 Failed to list Smokesignal events: {0}")] 14 14 FailedToListSmokesignalEvents(String), 15 15 }
+34 -5
src/http/errors/login_error.rs
··· 6 6 /// are logging in to the application, including OAuth flows and DID validation. 7 7 #[derive(Debug, Error)] 8 8 pub(crate) enum LoginError { 9 - #[error("error-login-1 DID document does not contain a handle identifier")] 9 + #[error("error-smokesignal-login-1 DID document does not contain a handle identifier")] 10 10 NoHandle, 11 11 12 12 /// Error when a DID document does not contain a PDS endpoint. ··· 14 14 /// This error occurs during authentication when the user's DID document 15 15 /// is retrieved but does not contain a required AT Protocol Personal 16 16 /// Data Server (PDS) endpoint. 17 - #[error("error-login-2 DID document does not contain an AT Protocol PDS endpoint")] 17 + #[error("error-smokesignal-login-2 DID document does not contain an AT Protocol PDS endpoint")] 18 18 NoPDS, 19 19 20 20 /// Error when an OAuth callback is incomplete. 21 21 /// 22 22 /// This error occurs when the OAuth authentication flow callback 23 23 /// returns with incomplete information, preventing successful authentication. 24 - #[error("error-login-100 OAuth callback incomplete")] 24 + #[error("error-smokesignal-login-3 OAuth callback incomplete")] 25 25 OAuthCallbackIncomplete, 26 26 27 27 /// Error when there is an OAuth issuer mismatch. 28 28 /// 29 29 /// This error occurs when the issuer in the OAuth response does not 30 30 /// match the expected issuer, which could indicate a security issue. 31 - #[error("error-login-101 OAuth issuer mismatch")] 31 + #[error("error-smokesignal-login-4 OAuth issuer mismatch")] 32 32 OAuthIssuerMismatch, 33 33 34 34 /// Error when the login input appears to be an incomplete AT-handle. ··· 36 36 /// This error occurs when a user enters what appears to be a partial 37 37 /// AT-handle (alphanumeric without a domain). The user should append 38 38 /// ".bsky.social" and resubmit the form. 39 - #[error("error-login-102 Please add '.bsky.social' to your handle and try again")] 39 + #[error("error-smokesignal-login-5 Please add '.bsky.social' to your handle and try again")] 40 40 IncompleteHandle, 41 + 42 + /// Error when an OAuth request is not found in storage. 43 + /// 44 + /// This error occurs during the OAuth callback when the system cannot 45 + /// find the corresponding OAuth request that was initiated, possibly 46 + /// due to expiration or invalid state parameter. 47 + #[error("error-smokesignal-login-6 oauth request not found in storage")] 48 + OAuthRequestNotFound, 49 + 50 + /// Error when HTTP request for AIP OAuth userinfo fails. 51 + /// 52 + /// This error occurs during AIP OAuth flow when the HTTP request 53 + /// to retrieve user information from the AIP server fails. 54 + #[error("error-smokesignal-login-7 HTTP request for userinfo failed")] 55 + AipUserinfoRequestFailed, 56 + 57 + /// Error when parsing AIP OAuth userinfo response fails. 58 + /// 59 + /// This error occurs during AIP OAuth flow when the JSON response 60 + /// from the AIP userinfo endpoint cannot be parsed. 61 + #[error("error-smokesignal-login-8 Parsing HTTP response for userinfo failed")] 62 + AipUserinfoParsingFailed, 63 + 64 + /// Error when AIP OAuth server returns an error response. 65 + /// 66 + /// This error occurs when the AIP OAuth server returns an error 67 + /// in the userinfo response instead of valid user claims. 68 + #[error("error-smokesignal-login-9 AIP OAuth server error: {message}")] 69 + AipServerError { message: String }, 41 70 }
+5 -5
src/http/errors/middleware_errors.rs
··· 14 14 /// 15 15 /// This error occurs when attempting to deserialize a web session from JSON 16 16 /// format, typically when retrieving a session from storage or a cookie. 17 - #[error("error-websession-1 Unable to deserialize WebSession: {0:?}")] 17 + #[error("error-smokesignal-websession-1 Unable to deserialize WebSession: {0:?}")] 18 18 DeserializeFailed(serde_json::Error), 19 19 20 20 /// Error when web session serialization fails. 21 21 /// 22 22 /// This error occurs when attempting to serialize a web session to JSON 23 23 /// format, typically when storing a session in storage or a cookie. 24 - #[error("error-websession-2 Unable to serialize WebSession: {0:?}")] 24 + #[error("error-smokesignal-websession-2 Unable to serialize WebSession: {0:?}")] 25 25 SerializeFailed(serde_json::Error), 26 26 } 27 27 28 28 #[derive(Debug, Error)] 29 29 pub(crate) enum MiddlewareAuthError { 30 - #[error("error-middleware-auth-1 Access Denied: {0}")] 30 + #[error("error-smokesignal-middleware-auth-1 Access Denied: {0}")] 31 31 AccessDenied(String), 32 32 33 - #[error("error-middleware-auth-2 Not Found")] 33 + #[error("error-smokesignal-middleware-auth-2 Not Found")] 34 34 NotFound, 35 35 36 - #[error("error-middleware-auth-3 Unhandled Auth Error: {0:?}")] 36 + #[error("error-smokesignal-middleware-auth-3 Unhandled Auth Error: {0:?}")] 37 37 Anyhow(#[from] anyhow::Error), 38 38 } 39 39
+4 -4
src/http/errors/migrate_event_error.rs
··· 11 11 /// This error occurs when a user attempts to migrate an event that they 12 12 /// do not have permission to modify, typically because they are not 13 13 /// the event creator or an administrator. 14 - #[error("error-migrate-event-2 Not authorized to migrate this event")] 14 + #[error("error-smokesignal-migrate-event-1 Not authorized to migrate this event")] 15 15 NotAuthorized, 16 16 17 17 /// Error when attempting to migrate an unsupported event type. ··· 20 20 /// cannot be migrated, as only smokesignal events can be converted to 21 21 /// the community event format. 22 22 #[error( 23 - "error-migrate-event-3 Unsupported event type. Only smokesignal events can be migrated" 23 + "error-smokesignal-migrate-event-2 Unsupported event type. Only smokesignal events can be migrated" 24 24 )] 25 25 UnsupportedEventType, 26 26 ··· 28 28 /// 29 29 /// This error occurs when attempting to migrate an event that is already 30 30 /// in the community event format, which would be redundant. 31 - #[error("error-migrate-event-4 Event is already a community event")] 31 + #[error("error-smokesignal-migrate-event-3 Event is already a community event")] 32 32 AlreadyMigrated, 33 33 34 34 /// Error when a destination URI conflict exists. 35 35 /// 36 36 /// This error occurs when attempting to migrate an event to a URI that 37 37 /// already has an event associated with it, which would cause a conflict. 38 - #[error("error-migrate-event-5 An event already exists at the destination URI")] 38 + #[error("error-smokesignal-migrate-event-4 An event already exists at the destination URI")] 39 39 DestinationExists, 40 40 }
+2 -2
src/http/errors/migrate_rsvp_error.rs
··· 12 12 /// that doesn't match one of the expected values ('going', 'interested', 13 13 /// or 'notgoing'). 14 14 #[error( 15 - "error-migrate-rsvp-1 Invalid RSVP status: {0}. Expected 'going', 'interested', or 'notgoing'." 15 + "error-smokesignal-migrate-rsvp-1 Invalid RSVP status: {0}. Expected 'going', 'interested', or 'notgoing'." 16 16 )] 17 17 InvalidRsvpStatus(String), 18 18 ··· 21 21 /// This error occurs when a user attempts to migrate an RSVP that they 22 22 /// do not have permission to modify, typically because they are not 23 23 /// the RSVP owner or an administrator. 24 - #[error("error-migrate-rsvp-2 Not authorized to migrate this RSVP")] 24 + #[error("error-smokesignal-migrate-rsvp-2 Not authorized to migrate this RSVP")] 25 25 NotAuthorized, 26 26 }
+4
src/http/errors/mod.rs
··· 2 2 pub mod admin_errors; 3 3 pub mod common_error; 4 4 pub mod create_event_errors; 5 + pub mod create_rsvp_errors; 6 + pub mod delete_event_errors; 5 7 pub mod edit_event_error; 6 8 pub mod event_view_errors; 7 9 pub mod import_error; ··· 15 17 16 18 pub(crate) use common_error::CommonError; 17 19 pub(crate) use create_event_errors::CreateEventError; 20 + pub(crate) use create_rsvp_errors::CreateRsvpError; 21 + pub(crate) use delete_event_errors::DeleteEventError; 18 22 pub(crate) use edit_event_error::EditEventError; 19 23 pub(crate) use event_view_errors::EventViewError; 20 24 pub(crate) use import_error::ImportError;
+1 -1
src/http/errors/url_error.rs
··· 10 10 /// 11 11 /// This error occurs when a URL contains a collection type that is not 12 12 /// supported by the system, typically in an AT Protocol URI path. 13 - #[error("error-url-1 Unsupported collection type")] 13 + #[error("error-smokesignal-url-1 Unsupported collection type")] 14 14 UnsupportedCollection, 15 15 }
+2 -2
src/http/errors/view_event_error.rs
··· 10 10 /// 11 11 /// This error occurs when attempting to view an event that doesn't 12 12 /// exist in the system, typically due to an invalid identifier. 13 - #[error("error-view-event-1 Event not found: {0}")] 13 + #[error("error-smokesignal-view-event-1 Event not found: {0}")] 14 14 EventNotFound(String), 15 15 16 16 /// Error when a fallback retrieval method fails. 17 17 /// 18 18 /// This error occurs when the primary method of retrieving an event fails, 19 19 /// and the fallback method also fails to retrieve the event. 20 - #[error("error-view-event-2 Failed to get event from fallback: {0}")] 20 + #[error("error-smokesignal-view-event-2 Failed to get event from fallback: {0}")] 21 21 FallbackFailed(String), 22 22 }
+6 -6
src/http/errors/web_error.rs
··· 5 5 //! uniformly at the HTTP boundary and converted into appropriate HTTP responses. 6 6 //! 7 7 //! Specific error variants use their own error codes, while general errors use the 8 - //! format: `error-web-<number> <message>: <details>` 8 + //! format: `error-smokesignal-web-<number> <message>: <details>` 9 9 10 10 use axum::http::StatusCode; 11 11 use axum::response::IntoResponse; ··· 30 30 /// 31 31 /// Most variants use transparent error forwarding to preserve the original error message 32 32 /// and error code, while a few web-specific errors have their own error code format: 33 - /// `error-web-<number> <message>: <details>` 33 + /// `error-smokesignal-web-<number> <message>: <details>` 34 34 #[derive(Debug, Error)] 35 35 pub(crate) enum WebError { 36 36 /// Error when authentication middleware fails. ··· 39 39 /// through the authentication middleware, such as invalid credentials or 40 40 /// expired sessions. 41 41 /// 42 - /// **Error Code:** `error-web-1` 43 - #[error("error-web-1 Middleware Auth Error: {0:?}")] 42 + /// **Error Code:** `error-smokesignal-web-1` 43 + #[error("error-smokesignal-web-1 Middleware Auth Error: {0:?}")] 44 44 MiddlewareAuthError(#[from] MiddlewareAuthError), 45 45 46 46 /// Error when an unexpected error occurs that isn't covered by other error types. ··· 48 48 /// This error is a fallback for any unhandled errors in the system. In production, 49 49 /// these should be rare as most errors should be properly typed. 50 50 /// 51 - /// **Error Code:** `error-web-2` 51 + /// **Error Code:** `error-smokesignal-web-2` 52 52 /// 53 53 /// Note: This should be replaced with more specific error types as part of 54 54 /// the ongoing effort to use typed errors throughout the codebase. 55 - #[error("error-web-2 Unhandled web error: {0:?}")] 55 + #[error("error-smokesignal-web-2 Unhandled web error: {0:?}")] 56 56 Anyhow(#[from] anyhow::Error), 57 57 58 58 /// Common HTTP errors.
+17 -17
src/http/event_form.rs
··· 7 7 8 8 #[derive(Debug, Error)] 9 9 pub(crate) enum BuildEventError { 10 - #[error("error-event-builder-1 Invalid Name")] 10 + #[error("error-smokesignal-event-builder-1 Invalid Name")] 11 11 InvalidName, 12 12 13 - #[error("error-event-builder-2 Invalid Description")] 13 + #[error("error-smokesignal-event-builder-2 Invalid Description")] 14 14 InvalidDescription, 15 15 16 - #[error("error-event-builder-3 Invalid Time Zone")] 16 + #[error("error-smokesignal-event-builder-3 Invalid Time Zone")] 17 17 InvalidTimeZone, 18 18 19 - #[error("error-event-builder-4 Invalid Status")] 19 + #[error("error-smokesignal-event-builder-4 Invalid Status")] 20 20 InvalidStatus, 21 21 22 - #[error("error-event-builder-5 Invalid Mode")] 22 + #[error("error-smokesignal-event-builder-5 Invalid Mode")] 23 23 InvalidMode, 24 24 25 - #[error("error-event-builder-6 Invalid Start Date/Time Format")] 25 + #[error("error-smokesignal-event-builder-6 Invalid Start Date/Time Format")] 26 26 InvalidStartDateTime, 27 27 28 - #[error("error-event-builder-7 Invalid End Date/Time Format")] 28 + #[error("error-smokesignal-event-builder-7 Invalid End Date/Time Format")] 29 29 InvalidEndDateTime, 30 30 31 - #[error("error-event-builder-8 End Date/Time Must Be After Start Date/Time")] 31 + #[error("error-smokesignal-event-builder-8 End Date/Time Must Be After Start Date/Time")] 32 32 EndBeforeStart, 33 33 34 - #[error("error-event-builder-9 Address Location Country Missing")] 34 + #[error("error-smokesignal-event-builder-9 Address Location Country Missing")] 35 35 LocationCountryRequired, 36 36 37 - #[error("error-event-builder-10 Invalid Address Location Country: {0}")] 37 + #[error("error-smokesignal-event-builder-10 Invalid Address Location Country: {0}")] 38 38 LocationCountryInvalid(String), 39 39 40 - #[error("error-event-builder-11 Invalid Address Location Locality")] 40 + #[error("error-smokesignal-event-builder-11 Invalid Address Location Locality")] 41 41 InvalidLocationAddressLocality, 42 42 43 - #[error("error-event-builder-12 Invalid Address Location Region")] 43 + #[error("error-smokesignal-event-builder-12 Invalid Address Location Region")] 44 44 InvalidLocationAddressRegion, 45 45 46 - #[error("error-event-builder-13 Invalid Address Location Street")] 46 + #[error("error-smokesignal-event-builder-13 Invalid Address Location Street")] 47 47 InvalidLocationAddressStreet, 48 48 49 - #[error("error-event-builder-14 Invalid Address Location Postal Code")] 49 + #[error("error-smokesignal-event-builder-14 Invalid Address Location Postal Code")] 50 50 InvalidLocationAddressPostalCode, 51 51 52 - #[error("error-event-builder-15 Invalid Address Location Name")] 52 + #[error("error-smokesignal-event-builder-15 Invalid Address Location Name")] 53 53 InvalidLocationAddressName, 54 54 55 - #[error("error-event-builder-16 Invalid Link URL")] 55 + #[error("error-smokesignal-event-builder-16 Invalid Link URL")] 56 56 InvalidLinkValue, 57 57 58 - #[error("error-event-builder-17 Invalid Link Name")] 58 + #[error("error-smokesignal-event-builder-17 Invalid Link Name")] 59 59 InvalidLinkName, 60 60 } 61 61
+1 -1
src/http/handle_create_event.rs
··· 304 304 language, 305 305 error_template, 306 306 default_context, 307 - anyhow::anyhow!("Server error: {}", err.error_message()) 307 + CreateEventError::ServerError { message: err.error_message() } 308 308 ); 309 309 } 310 310 Err(err) => {
+2 -2
src/http/handle_create_rsvp.rs
··· 24 24 contextual_error, 25 25 http::{ 26 26 context::WebContext, 27 - errors::{CommonError, WebError}, 27 + errors::{CommonError, CreateRsvpError, WebError}, 28 28 middleware_auth::Auth, 29 29 middleware_i18n::Language, 30 30 rsvp_form::{BuildRSVPForm, BuildRsvpContentState}, ··· 195 195 language, 196 196 error_template, 197 197 default_context, 198 - anyhow::anyhow!("Server error: {}", err.error_message()) 198 + CreateRsvpError::ServerError { message: err.error_message() } 199 199 ); 200 200 } 201 201 Err(err) => {
+4 -6
src/http/handle_delete_event.rs
··· 1 - use anyhow::{Result, anyhow}; 1 + use anyhow::Result; 2 2 use axum::{extract::Path, response::IntoResponse}; 3 3 use axum_extra::extract::Form; 4 4 use axum_template::RenderHtml; ··· 15 15 }, 16 16 config::OAuthBackendConfig, 17 17 contextual_error, 18 - http::{context::UserRequestContext, errors::WebError, middleware_auth::Auth}, 18 + http::{context::UserRequestContext, errors::{DeleteEventError, WebError}, middleware_auth::Auth}, 19 19 select_template, 20 20 storage::event::{event_delete, event_exists}, 21 21 }; ··· 70 70 ctx.language, 71 71 error_template, 72 72 default_context, 73 - anyhow!( 74 - "error-delete-event-1 identity does not have event with that AT-URI: {lookup_aturi}" 75 - ) 73 + DeleteEventError::EventNotFound { aturi: lookup_aturi.clone() } 76 74 ); 77 75 } 78 76 ··· 129 127 ctx.language, 130 128 error_template, 131 129 default_context, 132 - anyhow!("Authentication configuration mismatch"), 130 + DeleteEventError::AuthenticationConfigurationMismatch, 133 131 StatusCode::INTERNAL_SERVER_ERROR 134 132 ); 135 133 }
+1 -1
src/http/handle_edit_event.rs
··· 633 633 ctx.language, 634 634 error_template, 635 635 default_context, 636 - anyhow::anyhow!("Server error: {}", err.error_message()), 636 + EditEventError::ServerError { message: err.error_message() }, 637 637 StatusCode::OK 638 638 ); 639 639 }
+7 -5
src/http/handle_oauth_aip_callback.rs
··· 4 4 config::OAuthBackendConfig, contextual_error, select_template, 5 5 storage::identity_profile::handle_warm_up, 6 6 }; 7 - use anyhow::{Context, Result, anyhow, bail}; 7 + use anyhow::Result; 8 8 use atproto_client::errors::SimpleError; 9 9 use atproto_identity::resolve::IdentityResolver; 10 10 use axum::{ ··· 84 84 language, 85 85 error_template, 86 86 default_context, 87 - anyhow::anyhow!("oauth request not found in storage") 87 + LoginError::OAuthRequestNotFound 88 88 ); 89 89 } 90 90 Ok(Some(value)) => value, ··· 275 275 .bearer_auth(aip_access_token) 276 276 .send() 277 277 .await 278 - .context(anyhow!("HTTP request for userinfo failed"))? 278 + .map_err(|_| LoginError::AipUserinfoRequestFailed)? 279 279 .json() 280 280 .await 281 - .context(anyhow!("Parsing HTTP response for userinfo failed"))?; 281 + .map_err(|_| LoginError::AipUserinfoParsingFailed)?; 282 282 283 283 match response { 284 284 OpenIDClaimsResponse::OpenIDClaims(claims) => Ok((claims.did, claims.email)), 285 - OpenIDClaimsResponse::SimpleError(simple_error) => bail!(simple_error.error_message()), 285 + OpenIDClaimsResponse::SimpleError(simple_error) => Err(LoginError::AipServerError { 286 + message: simple_error.error_message() 287 + }.into()), 286 288 } 287 289 }
+2 -2
src/http/handle_oauth_callback.rs
··· 1 - use anyhow::{Result, anyhow}; 1 + use anyhow::Result; 2 2 use atproto_identity::{axum::state::KeyProviderExtractor, key::identify_key}; 3 3 use atproto_oauth::{ 4 4 resources::oauth_authorization_server, ··· 75 75 language, 76 76 error_template, 77 77 default_context, 78 - anyhow!("oauth request not found in storage") 78 + LoginError::OAuthRequestNotFound 79 79 ); 80 80 } 81 81 Ok(Some(value)) => value,
+5 -5
src/http/handle_settings.rs
··· 138 138 language, 139 139 error_template, 140 140 default_context, 141 - "error-xxx Invalid timezone" 141 + "error-smokesignal-settings-1 Invalid timezone" 142 142 ); 143 143 } 144 144 ··· 209 209 language, 210 210 error_template, 211 211 default_context, 212 - "error-xxx Invalid language" 212 + "error-smokesignal-settings-2 Invalid language" 213 213 ); 214 214 } 215 215 ··· 231 231 language, 232 232 error_template, 233 233 default_context, 234 - "error-xxx Invalid language" 234 + "error-smokesignal-settings-2 Invalid language" 235 235 ); 236 236 } 237 237 ··· 473 473 language, 474 474 error_template, 475 475 default_context, 476 - "error-xxx Service cannot be empty" 476 + "error-smokesignal-settings-3 Service cannot be empty" 477 477 ); 478 478 } 479 479 ··· 487 487 language, 488 488 error_template, 489 489 default_context, 490 - "error-xxx Only SmokeSignalAutomation services are supported" 490 + "error-smokesignal-settings-4 Only SmokeSignalAutomation services are supported" 491 491 ); 492 492 } 493 493
+1
src/http/mod.rs
··· 47 47 pub mod server; 48 48 pub mod tab_selector; 49 49 pub mod templates; 50 + pub mod timezone_errors; 50 51 pub mod timezones; 51 52 pub mod utils;
+22
src/http/timezone_errors.rs
··· 1 + use thiserror::Error; 2 + 3 + /// Represents errors that can occur during timezone and datetime operations. 4 + /// 5 + /// These errors typically occur when parsing date/time strings or converting 6 + /// between timezones, especially during daylight saving time transitions. 7 + #[derive(Debug, Error)] 8 + pub(crate) enum TimezoneError { 9 + /// Error when parsing date and time strings fails. 10 + /// 11 + /// This error occurs when the provided date or time string cannot be parsed 12 + /// into a valid datetime format, typically due to incorrect format or invalid values. 13 + #[error("error-smokesignal-timezone-1 Failed to parse date and time: {error}")] 14 + DateTimeParsingFailed { error: String }, 15 + 16 + /// Error when local time is ambiguous or non-existent. 17 + /// 18 + /// This error occurs during daylight saving time transitions when a local time 19 + /// either doesn't exist (spring forward) or is ambiguous (fall back). 20 + #[error("error-smokesignal-timezone-2 Ambiguous or non-existent local time")] 21 + AmbiguousOrNonExistentLocalTime, 22 + }
+4 -3
src/http/timezones.rs
··· 1 - use anyhow::{Result, anyhow}; 1 + use anyhow::Result; 2 2 use chrono::{DateTime, NaiveDateTime, Utc}; 3 3 use itertools::Itertools; 4 4 5 + use crate::http::timezone_errors::TimezoneError; 5 6 use crate::storage::identity_profile::model::IdentityProfile; 6 7 7 8 pub(crate) fn supported_timezones(handle: Option<&IdentityProfile>) -> (&str, Vec<&str>) { ··· 79 80 80 81 // Parse the combined string into a NaiveDateTime 81 82 let naive_dt = NaiveDateTime::parse_from_str(&datetime_str, "%Y-%m-%dT%H:%M") 82 - .map_err(|e| anyhow!("Failed to parse date and time: {}", e))?; 83 + .map_err(|e| TimezoneError::DateTimeParsingFailed { error: e.to_string() })?; 83 84 84 85 // Convert to timezone-aware datetime in the specified timezone 85 86 let local_dt = naive_dt 86 87 .and_local_timezone(timezone) 87 88 .single() 88 - .ok_or_else(|| anyhow!("Ambiguous or non-existent local time"))?; 89 + .ok_or(TimezoneError::AmbiguousOrNonExistentLocalTime)?; 89 90 90 91 // Convert to UTC 91 92 Ok(local_dt.with_timezone(&Utc))
+3 -3
src/i18n.rs
··· 165 165 166 166 #[derive(Debug, Error)] 167 167 pub enum I18nError { 168 - #[error("error-i18n-1 Invalid language")] 168 + #[error("error-smokesignal-i18n-1 Invalid language")] 169 169 InvalidLanguage, 170 170 171 - #[error("error-i18n-2 Language resource failed")] 171 + #[error("error-smokesignal-i18n-2 Language resource failed")] 172 172 LanguageResourceFailed(Vec<fluent_syntax::parser::ParserError>), 173 173 174 - #[error("error-i18n-3 Bundle load failed")] 174 + #[error("error-smokesignal-i18n-3 Bundle load failed")] 175 175 BundleLoadFailed(Vec<fluent::FluentError>), 176 176 } 177 177 }
+3
src/lib.rs
··· 7 7 pub mod i18n; 8 8 pub mod key_provider; 9 9 pub mod processor; 10 + pub mod processor_errors; 10 11 pub mod refresh_tokens_errors; 11 12 pub mod service; 12 13 pub mod storage; ··· 14 15 pub mod task_oauth_requests_cleanup; 15 16 pub mod task_refresh_tokens; 16 17 pub mod task_search_indexer; 18 + pub mod task_search_indexer_errors; 17 19 pub mod task_webhooks; 20 + pub mod task_webhooks_errors; 18 21 pub mod webhooks;
+3 -2
src/processor.rs
··· 1 - use anyhow::{Result, anyhow}; 1 + use anyhow::Result; 2 2 use atproto_client::com::atproto::repo::get_blob; 3 3 use atproto_identity::model::Document; 4 4 use atproto_identity::storage::DidDocumentStorage; ··· 8 8 use std::sync::Arc; 9 9 10 10 use crate::atproto::lexicon::community::lexicon::calendar::event::Event; 11 + use crate::processor_errors::ProcessorError; 11 12 use crate::atproto::lexicon::community::lexicon::calendar::event::Media; 12 13 use crate::atproto::lexicon::community::lexicon::calendar::event::NSID as LexiconCommunityEventNSID; 13 14 use crate::atproto::lexicon::community::lexicon::calendar::rsvp::NSID as LexiconCommunityRSVPNSID; ··· 144 145 let pds_endpoints = document.pds_endpoints(); 145 146 let pds_endpoint = pds_endpoints 146 147 .first() 147 - .ok_or_else(|| anyhow!("no PDS in DID"))?; 148 + .ok_or(ProcessorError::NoPdsInDid)?; 148 149 149 150 let name = match &event_record { 150 151 Event::Current { name, .. } => name.clone(),
+15
src/processor_errors.rs
··· 1 + use thiserror::Error; 2 + 3 + /// Represents errors that can occur during event processing. 4 + /// 5 + /// These errors typically occur when processing events from the Jetstream 6 + /// consumer, including DID resolution and data validation issues. 7 + #[derive(Debug, Error)] 8 + pub(crate) enum ProcessorError { 9 + /// Error when a DID document does not contain any PDS endpoints. 10 + /// 11 + /// This error occurs when attempting to process an event from a DID 12 + /// that has no Personal Data Server (PDS) endpoints configured. 13 + #[error("error-smokesignal-processor-1 no PDS in DID")] 14 + NoPdsInDid, 15 + }
+4 -4
src/refresh_tokens_errors.rs
··· 10 10 /// 11 11 /// This error occurs when attempting to refresh a token but the necessary 12 12 /// secret key for signing the request is not available in the configuration. 13 - #[error("error-refresh-1 Secret signing key not found")] 13 + #[error("error-smokesignal-refresh-1 Secret signing key not found")] 14 14 SecretSigningKeyNotFound, 15 15 16 16 /// Error when creating a DPoP proof for token refresh fails. 17 17 /// 18 18 /// This error occurs when there is an issue with the cryptographic operations 19 19 /// required to generate a DPoP (Demonstrating Proof-of-Possession) proof. 20 - #[error("error-refresh-2 Failed to create DPoP proof: {0:?}")] 20 + #[error("error-smokesignal-refresh-2 Failed to create DPoP proof: {0:?}")] 21 21 DpopProofCreationFailed(elliptic_curve::Error), 22 22 23 23 /// Error when a session cannot be placed in the refresh queue. 24 24 /// 25 25 /// This error occurs when there is an issue with the Redis-backed queue 26 26 /// used to manage session refresh operations. 27 - #[error("error-refresh-3 Failed to place session group into refresh queue: {0:?}")] 27 + #[error("error-smokesignal-refresh-3 Failed to place session group into refresh queue: {0:?}")] 28 28 PlaceInRefreshQueueFailed(deadpool_redis::redis::RedisError), 29 29 30 30 /// Error when the identity document cannot be found. 31 31 /// 32 32 /// This error occurs when attempting to refresh a token but the necessary 33 33 /// identity document for the user is not available in storage. 34 - #[error("error-refresh-4 Identity document not found")] 34 + #[error("error-smokesignal-refresh-4 Identity document not found")] 35 35 IdentityDocumentNotFound, 36 36 }
+4 -4
src/storage/atproto.rs
··· 44 44 impl DidDocumentStorage for PostgresDidDocumentStorage { 45 45 async fn get_document_by_did(&self, did: &str) -> Result<Option<Document>, anyhow::Error> { 46 46 if did.trim().is_empty() { 47 - return Err(anyhow::anyhow!("DID cannot be empty")); 47 + return Err(StorageError::DidCannotBeEmpty.into()); 48 48 } 49 49 50 50 let mut tx = self.pool.begin().await?; ··· 97 97 98 98 async fn delete_document_by_did(&self, did: &str) -> Result<(), anyhow::Error> { 99 99 if did.trim().is_empty() { 100 - return Err(anyhow::anyhow!("DID cannot be empty")); 100 + return Err(StorageError::DidCannotBeEmpty.into()); 101 101 } 102 102 103 103 let mut tx = self.pool.begin().await?; ··· 226 226 state: &str, 227 227 ) -> Result<Option<OAuthRequest>, anyhow::Error> { 228 228 if state.trim().is_empty() { 229 - return Err(anyhow::anyhow!("OAuth state cannot be empty")); 229 + return Err(StorageError::OAuthStateCannotBeEmpty.into()); 230 230 } 231 231 232 232 let mut tx = self.pool.begin().await?; ··· 263 263 264 264 async fn delete_oauth_request_by_state(&self, state: &str) -> Result<(), anyhow::Error> { 265 265 if state.trim().is_empty() { 266 - return Err(anyhow::anyhow!("OAuth state cannot be empty")); 266 + return Err(StorageError::OAuthStateCannotBeEmpty.into()); 267 267 } 268 268 269 269 let mut tx = self.pool.begin().await?;
+1 -1
src/storage/content.rs
··· 447 447 Err(e) => { 448 448 // If it's a not-found error, add to the not-found filter 449 449 if e.to_string() 450 - .starts_with("error-content-2 File storage operation failed:") 450 + .starts_with("error-smokesignal-content-2 File storage operation failed:") 451 451 { 452 452 self.add_to_not_found_filter(cid).await; 453 453 }
+33 -19
src/storage/errors.rs
··· 11 11 /// This error occurs when attempting to convert a JSON Web Key (JWK) 12 12 /// into a secret key for DPoP (Demonstrating Proof-of-Possession) operations, 13 13 /// typically due to invalid key format or cryptographic errors. 14 - #[error("error-oauth-model-1 Failed to create DPoP secret from JWK: {0:?}")] 14 + #[error("error-smokesignal-oauth-model-1 Failed to create DPoP secret from JWK: {0:?}")] 15 15 DpopSecretFromJwkFailed(elliptic_curve::Error), 16 16 17 17 /// Error when the OAuth flow state is invalid. ··· 19 19 /// This error occurs when the state parameter in an OAuth flow 20 20 /// does not match the expected value or cannot be verified, 21 21 /// which could indicate a potential CSRF attack or session mismatch. 22 - #[error("error-oauth-model-2 Invalid OAuth flow state")] 22 + #[error("error-smokesignal-oauth-model-2 Invalid OAuth flow state")] 23 23 InvalidOAuthFlowState(), 24 24 25 25 /// Error when deserializing DPoP JWK from string fails. 26 26 /// 27 27 /// This error occurs when attempting to deserialize a string-encoded 28 28 /// JSON Web Key (JWK) for DPoP operations, typically due to invalid JSON format. 29 - #[error("error-oauth-model-5 Failed to deserialize DPoP JWK: {0:?}")] 29 + #[error("error-smokesignal-oauth-model-3 Failed to deserialize DPoP JWK: {0:?}")] 30 30 DpopJwkDeserializationFailed(serde_json::Error), 31 31 32 32 /// Error when required OAuth session data is missing. ··· 34 34 /// This error occurs when attempting to use an OAuth session 35 35 /// that is missing critical data needed for authentication or 36 36 /// authorization operations, such as tokens or session identifiers. 37 - #[error("error-oauth-model-3 Missing required OAuth session data")] 37 + #[error("error-smokesignal-oauth-model-4 Missing required OAuth session data")] 38 38 MissingRequiredOAuthSessionData(), 39 39 40 40 /// Error when an OAuth session has expired. ··· 42 42 /// This error occurs when attempting to use an OAuth session 43 43 /// that has exceeded its validity period and is no longer usable 44 44 /// for authentication or authorization purposes. 45 - #[error("error-oauth-model-4 OAuth session has expired")] 45 + #[error("error-smokesignal-oauth-model-5 OAuth session has expired")] 46 46 OAuthSessionExpired(), 47 47 } 48 48 ··· 53 53 /// 54 54 /// This error occurs when attempting to retrieve a web session using an 55 55 /// invalid or expired session ID. 56 - #[error("error-storage-1 Web session not found")] 56 + #[error("error-smokesignal-storage-1 Web session not found")] 57 57 WebSessionNotFound, 58 58 59 59 /// Error when a handle cannot be found in the database. 60 60 /// 61 61 /// This error occurs when attempting to retrieve a user handle that 62 62 /// doesn't exist in the system. 63 - #[error("error-storage-2 Handle not found")] 63 + #[error("error-smokesignal-storage-2 Handle not found")] 64 64 HandleNotFound, 65 65 66 66 /// Error when a database record cannot be found. 67 67 /// 68 68 /// This error occurs when attempting to retrieve a specific record 69 69 /// using an ID or other identifier that doesn't exist in the database. 70 - #[error("error-storage-3 Record not found: {0} {1:?}")] 70 + #[error("error-smokesignal-storage-3 Record not found: {0} {1:?}")] 71 71 RowNotFound(String, sqlx::Error), 72 72 73 73 /// Error when a database transaction cannot be committed. 74 74 /// 75 75 /// This error occurs when there is an issue finalizing a database 76 76 /// transaction, potentially causing data inconsistency. 77 - #[error("error-storage-4 Cannot commit database transaction: {0:?}")] 77 + #[error("error-smokesignal-storage-4 Cannot commit database transaction: {0:?}")] 78 78 CannotCommitDatabaseTransaction(sqlx::Error), 79 79 80 80 /// Error when a database transaction cannot be started. 81 81 /// 82 82 /// This error occurs when there is an issue initiating a database 83 83 /// transaction, typically due to connection issues or database constraints. 84 - #[error("error-storage-5 Cannot begin database transaction: {0:?}")] 84 + #[error("error-smokesignal-storage-5 Cannot begin database transaction: {0:?}")] 85 85 CannotBeginDatabaseTransaction(sqlx::Error), 86 86 87 87 /// Error when a database query cannot be executed. 88 88 /// 89 89 /// This error occurs when a SQL query fails to execute, typically due to 90 90 /// syntax errors, constraint violations, or database connectivity issues. 91 - #[error("error-storage-6 Unable to execute query: {0:?}")] 91 + #[error("error-smokesignal-storage-6 Unable to execute query: {0:?}")] 92 92 UnableToExecuteQuery(sqlx::Error), 93 93 94 94 /// Error when an OAuth request cannot be found. 95 95 /// 96 96 /// This error occurs when attempting to retrieve an OAuth request 97 97 /// that doesn't exist or has expired. 98 - #[error("error-storage-7 OAuth request not found")] 98 + #[error("error-smokesignal-storage-7 OAuth request not found")] 99 99 OAuthRequestNotFound, 100 100 101 101 /// Error when an RSVP cannot be found. 102 102 /// 103 103 /// This error occurs when attempting to retrieve an RSVP record 104 104 /// that doesn't exist in the database. 105 - #[error("error-storage-8 RSVP not found")] 105 + #[error("error-smokesignal-storage-8 RSVP not found")] 106 106 RSVPNotFound, 107 107 108 108 /// Error when an OAuth model operation fails. 109 109 /// 110 110 /// This error occurs when there's an issue with OAuth model operations, 111 111 /// such as token generation, validation, or storage. 112 - #[error("error-storage-9 OAuth model error: {0}")] 112 + #[error("error-smokesignal-storage-9 OAuth model error: {0}")] 113 113 OAuthModelError(#[from] OAuthModelError), 114 + 115 + /// Error when a DID parameter is empty or invalid. 116 + /// 117 + /// This error occurs when a DID string is empty, which is not valid 118 + /// for DID-based operations. 119 + #[error("error-smokesignal-storage-10 DID cannot be empty")] 120 + DidCannotBeEmpty, 121 + 122 + /// Error when an OAuth state parameter is empty or invalid. 123 + /// 124 + /// This error occurs when an OAuth state string is empty, which is not valid 125 + /// for OAuth operations. 126 + #[error("error-smokesignal-storage-11 OAuth state cannot be empty")] 127 + OAuthStateCannotBeEmpty, 114 128 } 115 129 116 130 /// Represents errors that can occur during cache operations. ··· 120 134 /// 121 135 /// This error occurs when the system fails to initialize the Redis 122 136 /// connection pool, typically due to configuration or connectivity issues. 123 - #[error("error-cache-1 Failed to create cache pool: {0:?}")] 137 + #[error("error-smokesignal-cache-1 Failed to create cache pool: {0:?}")] 124 138 FailedToCreatePool(deadpool_redis::CreatePoolError), 125 139 126 140 /// Error when a cache connection cannot be obtained. 127 141 /// 128 142 /// This error occurs when the system fails to get a connection from 129 143 /// the Redis connection pool, typically due to pool exhaustion or connectivity issues. 130 - #[error("error-cache-2 Failed to get connection: {0:?}")] 144 + #[error("error-smokesignal-cache-2 Failed to get connection: {0:?}")] 131 145 FailedToGetConnection(deadpool_redis::PoolError), 132 146 133 147 /// Error when a session cannot be placed in the refresh queue. 134 148 /// 135 149 /// This error occurs when the system fails to add a session to the 136 150 /// Redis-backed refresh queue, typically due to Redis errors or connectivity issues. 137 - #[error("error-cache-3 Failed to place session group into refresh queue: {0:?}")] 151 + #[error("error-smokesignal-cache-3 Failed to place session group into refresh queue: {0:?}")] 138 152 FailedToPlaceInRefreshQueue(deadpool_redis::redis::RedisError), 139 153 } 140 154 141 155 #[derive(Debug, Error)] 142 156 pub enum ContentError { 143 - #[error("error-content-1 Invalid S3 URL format: {details}")] 157 + #[error("error-smokesignal-content-1 Invalid S3 URL format: {details}")] 144 158 /// Failed to parse S3 URL format from environment variable. 145 159 ConfigS3UrlInvalid { 146 160 /// Details about the S3 URL parsing error. 147 161 details: String, 148 162 }, 149 163 150 - #[error("error-content-2 File storage operation failed: {operation}")] 164 + #[error("error-smokesignal-content-2 File storage operation failed: {operation}")] 151 165 /// File storage operation failed. 152 166 StorageFileOperationFailed { 153 167 /// Description of the failed file operation.
+2 -1
src/task_search_indexer.rs
··· 10 10 use tokio_util::sync::CancellationToken; 11 11 12 12 use crate::atproto::lexicon::community::lexicon::calendar::event::NSID as LexiconCommunityEventNSID; 13 + use crate::task_search_indexer_errors::SearchIndexerError; 13 14 use crate::{ 14 15 atproto::lexicon::community::lexicon::calendar::event::Event, 15 16 consumer::{SmokeSignalEvent, SmokeSignalEventReceiver}, ··· 89 90 tracing::info!("Created OpenSearch index {}", INDEX_NAME); 90 91 } else { 91 92 let error_body = response.text().await?; 92 - return Err(anyhow::anyhow!("Failed to create index: {}", error_body)); 93 + return Err(SearchIndexerError::IndexCreationFailed { error_body }.into()); 93 94 } 94 95 95 96 Ok(())
+15
src/task_search_indexer_errors.rs
··· 1 + use thiserror::Error; 2 + 3 + /// Represents errors that can occur during search indexing operations. 4 + /// 5 + /// These errors typically occur when interacting with OpenSearch 6 + /// during the indexing process. 7 + #[derive(Debug, Error)] 8 + pub(crate) enum SearchIndexerError { 9 + /// Error when OpenSearch index creation fails. 10 + /// 11 + /// This error occurs when attempting to create an OpenSearch index 12 + /// and the operation fails with a server error response. 13 + #[error("error-smokesignal-search-indexer-1 Failed to create index: {error_body}")] 14 + IndexCreationFailed { error_body: String }, 15 + }
+17 -10
src/task_webhooks.rs
··· 14 14 use crate::{ 15 15 service::{ServiceDID, ServiceKey}, 16 16 storage::webhook::webhook_failed, 17 + task_webhooks_errors::WebhookError, 17 18 webhooks::{ 18 19 EVENT_CREATED_EVENT, RSVP_CREATED_EVENT, SMOKE_SIGNAL_AUTOMATION_SERVICE, TEST_EVENT, 19 20 }, ··· 153 154 ) -> Result<()> { 154 155 // Remove the suffix from service 155 156 if !service.ends_with(SMOKE_SIGNAL_AUTOMATION_SERVICE) { 156 - return Err(anyhow::anyhow!( 157 - "Service must end with {}", 158 - SMOKE_SIGNAL_AUTOMATION_SERVICE 159 - )); 157 + return Err(WebhookError::InvalidServiceSuffix { 158 + suffix: SMOKE_SIGNAL_AUTOMATION_SERVICE.to_string() 159 + }.into()); 160 160 } 161 161 162 162 let service_did = service ··· 168 168 .document_storage 169 169 .get_document_by_did(service_did) 170 170 .await 171 - .map_err(|e| anyhow::anyhow!("Failed to get DID document for {}: {}", service_did, e))? 172 - .ok_or_else(|| anyhow::anyhow!("DID document not found for {}", service_did))?; 171 + .map_err(|e| WebhookError::DidDocumentRetrievalFailed { 172 + did: service_did.to_string(), 173 + error: e.to_string() 174 + })? 175 + .ok_or_else(|| WebhookError::DidDocumentNotFound { 176 + did: service_did.to_string() 177 + })?; 173 178 174 179 // Extract the service endpoint 175 180 let automation_service = document 176 181 .service 177 182 .iter() 178 183 .find(|service| service.id.ends_with(SMOKE_SIGNAL_AUTOMATION_SERVICE)) 179 - .ok_or_else(|| anyhow::anyhow!("service not found in DID document"))?; 184 + .ok_or(WebhookError::ServiceNotFoundInDidDocument)?; 180 185 181 186 // Get the service endpoint - it should be a string URL 182 187 let endpoint_url = &automation_service.service_endpoint; ··· 206 211 ); 207 212 208 213 let token = mint(&self.service_key.0, &header, &claims) 209 - .map_err(|e| anyhow::anyhow!("Failed to create JWT: {}", e))?; 214 + .map_err(|e| WebhookError::JwtCreationFailed { 215 + error: e.to_string() 216 + })?; 210 217 211 218 // Prepare headers with JWT authorization 212 219 let mut headers = reqwest::header::HeaderMap::new(); ··· 241 248 // Update database to mark webhook as failed 242 249 webhook_failed(&self.pool, identity, service, &error_msg).await?; 243 250 244 - Err(anyhow::anyhow!("Webhook failed: {}", error_msg)) 251 + Err(WebhookError::WebhookRequestFailed { error: error_msg }.into()) 245 252 } 246 253 Err(e) => { 247 254 let error_msg = format!("Request failed: {}", e); ··· 250 257 // Update database to mark webhook as failed 251 258 webhook_failed(&self.pool, identity, service, &error_msg).await?; 252 259 253 - Err(anyhow::anyhow!("Webhook request failed: {}", e)) 260 + Err(WebhookError::WebhookTransportFailed { error: e.to_string() }.into()) 254 261 } 255 262 } 256 263 }
+57
src/task_webhooks_errors.rs
··· 1 + use thiserror::Error; 2 + 3 + /// Represents errors that can occur during webhook processing. 4 + /// 5 + /// These errors typically occur when processing webhook notifications, 6 + /// including service validation, DID resolution, and HTTP request failures. 7 + #[derive(Debug, Error)] 8 + pub(crate) enum WebhookError { 9 + /// Error when a service doesn't end with the required suffix. 10 + /// 11 + /// This error occurs when validating a webhook service URL that doesn't 12 + /// end with the expected automation service suffix. 13 + #[error("error-smokesignal-webhook-1 Service must end with {suffix}")] 14 + InvalidServiceSuffix { suffix: String }, 15 + 16 + /// Error when failing to get a DID document. 17 + /// 18 + /// This error occurs when attempting to retrieve a DID document 19 + /// for service validation fails. 20 + #[error("error-smokesignal-webhook-2 Failed to get DID document for {did}: {error}")] 21 + DidDocumentRetrievalFailed { did: String, error: String }, 22 + 23 + /// Error when a DID document is not found. 24 + /// 25 + /// This error occurs when a required DID document cannot be found 26 + /// in the system. 27 + #[error("error-smokesignal-webhook-3 DID document not found for {did}")] 28 + DidDocumentNotFound { did: String }, 29 + 30 + /// Error when a service is not found in a DID document. 31 + /// 32 + /// This error occurs when a DID document doesn't contain the expected 33 + /// automation service entry. 34 + #[error("error-smokesignal-webhook-4 service not found in DID document")] 35 + ServiceNotFoundInDidDocument, 36 + 37 + /// Error when JWT creation fails. 38 + /// 39 + /// This error occurs when attempting to create a JWT token for 40 + /// webhook authentication fails. 41 + #[error("error-smokesignal-webhook-5 Failed to create JWT: {error}")] 42 + JwtCreationFailed { error: String }, 43 + 44 + /// Error when a webhook request fails with an error response. 45 + /// 46 + /// This error occurs when the webhook endpoint returns an error 47 + /// status code or the request fails. 48 + #[error("error-smokesignal-webhook-6 Webhook failed: {error}")] 49 + WebhookRequestFailed { error: String }, 50 + 51 + /// Error when a webhook request fails due to network or other issues. 52 + /// 53 + /// This error occurs when the HTTP request to the webhook endpoint 54 + /// fails due to network issues or other transport problems. 55 + #[error("error-smokesignal-webhook-7 Webhook request failed: {error}")] 56 + WebhookTransportFailed { error: String }, 57 + }