diff --git a/CLAUDE.md b/CLAUDE.md index 9fc9f27..6ac6f18 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -232,4 +232,20 @@ Types and methods should have the lowest visibility necessary, defaulting to `pr - `atproto-client` - AT Protocol client functionality - `atproto-record` - Record management - `atproto-jetstream` - Firehose event streaming - - `atproto-xrpcs` - XRPC server implementation \ No newline at end of file + - `atproto-xrpcs` - XRPC server implementation + +## Error Handling + +All error strings must use this format: + + error-smokesignal-- :
+ +Example errors: + +* error-smokesignal-resolve-1 Multiple DIDs resolved for method +* error-smokesignal-plc-1 HTTP request failed: https://google.com/ Not Found +* error-smokesignal-key-1 Error decoding key: invalid + +Errors should be represented as enums using the `thiserror` library when possible using `src/errors.rs` as a reference and example. + +Avoid creating new errors with the `anyhow!(...)` macro. \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 329d0b0..80bedbd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -184,11 +184,9 @@ impl Config { OAuthBackendConfig::ATProtocol { signing_keys } => { let item = signing_keys.as_ref().first(); item.map(|(key, value)| (key.clone(), value.clone())) - .ok_or(anyhow::anyhow!("signing keys is empty")) + .ok_or(ConfigError::SigningKeysEmpty.into()) } - OAuthBackendConfig::AIP { .. } => Err(anyhow::anyhow!( - "signing keys not available for AIP OAuth backend" - )), + OAuthBackendConfig::AIP { .. } => Err(ConfigError::SigningKeysNotAvailableForAIP.into()), } } diff --git a/src/config_errors.rs b/src/config_errors.rs index 9ff43d8..e0f4260 100644 --- a/src/config_errors.rs +++ b/src/config_errors.rs @@ -10,7 +10,7 @@ pub enum ConfigError { /// /// This error occurs when the application starts up and a required /// environment variable is missing from the execution environment. - #[error("error-config-1 {0} must be set")] + #[error("error-smokesignal-config-1 {0} must be set")] EnvVarRequired(String), /// Error when the signing keys file cannot be read. @@ -18,105 +18,105 @@ pub enum ConfigError { /// This error occurs when the application fails to read the file /// containing signing keys, typically due to file system permissions /// or missing file issues. - #[error("error-config-2 Unable to read signing keys file: {0:?}")] + #[error("error-smokesignal-config-2 Unable to read signing keys file: {0:?}")] ReadSigningKeysFailed(std::io::Error), /// Error when the signing keys file cannot be parsed. /// /// This error occurs when the signing keys file contains malformed JSON /// that cannot be properly deserialized. - #[error("error-config-3 Unable to parse signing keys file: {0:?}")] + #[error("error-smokesignal-config-3 Unable to parse signing keys file: {0:?}")] ParseSigningKeysFailed(serde_json::Error), /// Error when no valid signing keys are found. /// /// This error occurs when the signing keys file does not contain any /// valid keys that the application can use for signing operations. - #[error("error-config-4 Signing keys must contain at least one valid key")] + #[error("error-smokesignal-config-4 Signing keys must contain at least one valid key")] EmptySigningKeys, /// Error when no valid OAuth active keys are found. /// /// This error occurs when the configuration does not include any /// valid keys that can be used for OAuth operations. - #[error("error-config-6 OAuth active keys must contain at least one valid key")] + #[error("error-smokesignal-config-5 OAuth active keys must contain at least one valid key")] EmptyOAuthActiveKeys, /// Error when no valid invitation active keys are found. /// /// This error occurs when the configuration does not include any /// valid keys that can be used for invitation operations. - #[error("error-config-7 Invitation active keys must contain at least one valid key")] + #[error("error-smokesignal-config-6 Invitation active keys must contain at least one valid key")] EmptyInvitationActiveKeys, /// Error when the PORT environment variable cannot be parsed. /// /// This error occurs when the PORT environment variable contains a value /// that cannot be parsed as a valid u16 integer. - #[error("error-config-8 Parsing PORT into u16 failed: {0:?}")] + #[error("error-smokesignal-config-7 Parsing PORT into u16 failed: {0:?}")] PortParsingFailed(std::num::ParseIntError), /// Error when the HTTP_COOKIE_KEY cannot be decoded. /// /// This error occurs when the HTTP_COOKIE_KEY environment variable /// contains a value that is not valid base64-encoded data. - #[error("error-config-9 Unable to base64 decode HTTP_COOKIE_KEY: {0:?}")] + #[error("error-smokesignal-config-8 Unable to base64 decode HTTP_COOKIE_KEY: {0:?}")] CookieKeyDecodeFailed(base64::DecodeSliceError), /// Error when the decoded HTTP_COOKIE_KEY cannot be processed. /// /// This error occurs when the decoded HTTP_COOKIE_KEY has an invalid /// format or length that prevents it from being used. - #[error("error-config-10 Unable to process decoded HTTP_COOKIE_KEY")] + #[error("error-smokesignal-config-9 Unable to process decoded HTTP_COOKIE_KEY")] CookieKeyProcessFailed, /// Error when version information is not available. /// /// This error occurs when neither GIT_HASH nor CARGO_PKG_VERSION /// environment variables are set, preventing version identification. - #[error("error-config-11 One of GIT_HASH or CARGO_PKG_VERSION must be set")] + #[error("error-smokesignal-config-10 One of GIT_HASH or CARGO_PKG_VERSION must be set")] VersionNotSet, /// Error when a referenced signing key is not found. /// /// This error occurs when attempting to use a signing key that /// does not exist in the loaded signing keys configuration. - #[error("error-config-12 Signing key not found")] + #[error("error-smokesignal-config-11 Signing key not found")] SigningKeyNotFound, /// Error when a DNS nameserver IP cannot be parsed. /// /// This error occurs when the DNS_NAMESERVERS environment variable contains /// an IP address that cannot be parsed as a valid IpAddr. - #[error("error-config-13 Unable to parse nameserver IP '{0}': {1}")] + #[error("error-smokesignal-config-12 Unable to parse nameserver IP '{0}': {1}")] NameserverParsingFailed(String, std::net::AddrParseError), /// Error when the signing keys file is not found. /// /// This error occurs when the file specified in the SIGNING_KEYS environment /// variable does not exist on the file system. - #[error("error-config-14 Signing keys file not found: {0}")] + #[error("error-smokesignal-config-13 Signing keys file not found: {0}")] SigningKeysFileNotFound(String), /// Error when the signing keys file is empty. /// /// This error occurs when the file specified in the SIGNING_KEYS environment /// variable exists but contains no data. - #[error("error-config-15 Signing keys file is empty")] + #[error("error-smokesignal-config-14 Signing keys file is empty")] EmptySigningKeysFile, /// Error when the JWKS structure doesn't contain any keys. /// /// This error occurs when the signing keys file contains a valid JWKS structure, /// but the 'keys' array is empty. - #[error("error-config-16 No keys found in JWKS")] + #[error("error-smokesignal-config-15 No keys found in JWKS")] MissingKeysInJWKS, /// Error when signing keys fail validation. /// /// This error occurs when the signing keys file contains keys /// that fail validation checks (such as having invalid format). - #[error("error-config-17 Signing keys validation failed: {0:?}")] + #[error("error-smokesignal-config-16 Signing keys validation failed: {0:?}")] SigningKeysValidationFailed(Vec), /// Error when AIP OAuth configuration is incomplete. @@ -124,7 +124,7 @@ pub enum ConfigError { /// This error occurs when oauth_backend is set to "aip" but /// required AIP configuration values are missing. #[error( - "error-config-18 When oauth_backend is 'aip', AIP_HOSTNAME, AIP_CLIENT_ID, and AIP_CLIENT_SECRET must all be set" + "error-smokesignal-config-17 When oauth_backend is 'aip', AIP_HOSTNAME, AIP_CLIENT_ID, and AIP_CLIENT_SECRET must all be set" )] AipConfigurationIncomplete, @@ -132,6 +132,20 @@ pub enum ConfigError { /// /// This error occurs when the OAUTH_BACKEND environment variable /// contains a value other than "aip" or "pds". - #[error("error-config-19 oauth_backend must be either 'aip' or 'pds', got: {0}")] + #[error("error-smokesignal-config-18 oauth_backend must be either 'aip' or 'pds', got: {0}")] InvalidOAuthBackend(String), + + /// Error when signing keys list is empty. + /// + /// This error occurs when attempting to select an OAuth signing key + /// but the signing keys list contains no entries. + #[error("error-smokesignal-config-19 signing keys is empty")] + SigningKeysEmpty, + + /// Error when signing keys are not available for AIP OAuth backend. + /// + /// This error occurs when attempting to access signing keys + /// while using the AIP OAuth backend, which doesn't support signing keys. + #[error("error-smokesignal-config-20 signing keys not available for AIP OAuth backend")] + SigningKeysNotAvailableForAIP, } diff --git a/src/errors.rs b/src/errors.rs index 280be9d..1714bb2 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -2,7 +2,7 @@ //! //! All errors are represented as a string in this format: //! -//! "error-- :
" +//! "error-smokesignal-- :
" //! //! The first part containing the "error-" prefix, domain, and number, is used //! to uniquely identify the error. This standard code format is used to convey diff --git a/src/http/errors/common_error.rs b/src/http/errors/common_error.rs index 245ab07..38dc582 100644 --- a/src/http/errors/common_error.rs +++ b/src/http/errors/common_error.rs @@ -7,27 +7,27 @@ pub(crate) enum CommonError { /// /// This error occurs when a URL contains a handle slug that doesn't conform /// to the expected format or contains invalid characters. - #[error("error-common-1 Invalid handle slug")] + #[error("error-smokesignal-common-1 Invalid handle slug")] InvalidHandleSlug, /// Error when a user lacks permission for an action. /// /// This error occurs when a user attempts to perform an action they /// are not authorized to do, such as modifying another user's data. - #[error("error-common-2 Not authorized to perform this action")] + #[error("error-smokesignal-common-2 Not authorized to perform this action")] NotAuthorized, /// Error when a required field is missing. /// /// This error occurs when a form or request is missing a mandatory field /// that is needed to complete the operation. - #[error("error-common-4 Required field not provided")] + #[error("error-smokesignal-common-3 Required field not provided")] FieldRequired, /// Error when event data has an invalid format or is corrupted. /// /// This error occurs when event data doesn't match the expected format /// or appears to be corrupted or tampered with. - #[error("error-common-9 Invalid event format or corrupted data")] + #[error("error-smokesignal-common-4 Invalid event format or corrupted data")] InvalidEventFormat, } diff --git a/src/http/errors/create_event_errors.rs b/src/http/errors/create_event_errors.rs index 154b543..25552c9 100644 --- a/src/http/errors/create_event_errors.rs +++ b/src/http/errors/create_event_errors.rs @@ -10,13 +10,20 @@ pub(crate) enum CreateEventError { /// /// This error occurs when a user attempts to create an event without /// specifying a name, which is a required field. - #[error("error-create-event-1 Name not set")] + #[error("error-smokesignal-create-event-1 Name not set")] NameNotSet, /// Error when the event description is not provided. /// /// This error occurs when a user attempts to create an event without /// specifying a description, which is a required field. - #[error("error-create-event-2 Description not set")] + #[error("error-smokesignal-create-event-2 Description not set")] DescriptionNotSet, + + /// Error when the AT Protocol server returns an error response. + /// + /// This error occurs when the PDS or other AT Protocol service + /// returns an error response during event record creation. + #[error("error-smokesignal-create-event-3 Server error: {message}")] + ServerError { message: String }, } diff --git a/src/http/errors/create_rsvp_errors.rs b/src/http/errors/create_rsvp_errors.rs new file mode 100644 index 0000000..b3ed1a5 --- /dev/null +++ b/src/http/errors/create_rsvp_errors.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +/// Represents errors that can occur during RSVP creation. +/// +/// These errors are typically triggered during the process of creating +/// and storing RSVP records in the AT Protocol ecosystem. +#[derive(Debug, Error)] +pub(crate) enum CreateRsvpError { + /// Error when the AT Protocol server returns an error response. + /// + /// This error occurs when the PDS or other AT Protocol service + /// returns an error response during RSVP record creation. + #[error("error-smokesignal-create-rsvp-1 Server error: {message}")] + ServerError { message: String }, +} \ No newline at end of file diff --git a/src/http/errors/delete_event_errors.rs b/src/http/errors/delete_event_errors.rs new file mode 100644 index 0000000..d8fa596 --- /dev/null +++ b/src/http/errors/delete_event_errors.rs @@ -0,0 +1,22 @@ +use thiserror::Error; + +/// Represents errors that can occur during event deletion. +/// +/// These errors are typically triggered during validation or authorization +/// checks when a user attempts to delete an event. +#[derive(Debug, Error)] +pub(crate) enum DeleteEventError { + /// Error when the identity does not have an event with the specified AT-URI. + /// + /// This error occurs when a user attempts to delete an event that doesn't + /// exist in their collection or when the AT-URI is invalid. + #[error("error-smokesignal-delete-event-1 identity does not have event with that AT-URI: {aturi}")] + EventNotFound { aturi: String }, + + /// Error when there is an authentication configuration mismatch. + /// + /// This error occurs when the authentication type doesn't match the + /// configured OAuth backend, indicating a system configuration issue. + #[error("error-smokesignal-delete-event-2 Authentication configuration mismatch")] + AuthenticationConfigurationMismatch, +} \ No newline at end of file diff --git a/src/http/errors/edit_event_error.rs b/src/http/errors/edit_event_error.rs index fe234bb..dcf339b 100644 --- a/src/http/errors/edit_event_error.rs +++ b/src/http/errors/edit_event_error.rs @@ -11,7 +11,7 @@ pub(crate) enum EditEventError { /// This error occurs when a user attempts to edit an event that they /// do not have permission to modify, typically because they are not /// the event creator or an administrator. - #[error("error-edit-event-2 Not authorized to edit this event")] + #[error("error-smokesignal-edit-event-1 Not authorized to edit this event")] NotAuthorized, /// Error when attempting to edit an unsupported event type. @@ -20,7 +20,7 @@ pub(crate) enum EditEventError { /// does not support editing, as only community calendar events can be /// modified after creation. #[error( - "error-edit-event-3 Unsupported event type. Only community calendar events can be edited" + "error-smokesignal-edit-event-2 Unsupported event type. Only community calendar events can be edited" )] UnsupportedEventType, @@ -28,13 +28,20 @@ pub(crate) enum EditEventError { /// /// This error occurs when a user attempts to modify location information for an event /// that has multiple locations defined. Such events can only be edited through the API. - #[error("error-edit-event-4 Cannot edit locations: Event has multiple locations")] + #[error("error-smokesignal-edit-event-3 Cannot edit locations: Event has multiple locations")] MultipleLocationsPresent, /// Error when attempting to edit location data on an event that has an unsupported location type. /// /// This error occurs when a user attempts to modify location information for an event /// that has a location type that is not supported for editing through the web interface. - #[error("error-edit-event-5 Cannot edit locations: Event has unsupported location type")] + #[error("error-smokesignal-edit-event-4 Cannot edit locations: Event has unsupported location type")] UnsupportedLocationType, + + /// Error when the AT Protocol server returns an error response. + /// + /// This error occurs when the PDS or other AT Protocol service + /// returns an error response during event record editing. + #[error("error-smokesignal-edit-event-5 Server error: {message}")] + ServerError { message: String }, } diff --git a/src/http/errors/event_view_errors.rs b/src/http/errors/event_view_errors.rs index 509a3b8..c738acc 100644 --- a/src/http/errors/event_view_errors.rs +++ b/src/http/errors/event_view_errors.rs @@ -10,20 +10,20 @@ pub(crate) enum EventViewError { /// /// This error occurs when an event view request specifies a collection /// name that doesn't exist or isn't supported by the system. - #[error("error-event-view-1 Invalid collection: {0}")] + #[error("error-smokesignal-event-view-1 Invalid collection: {0}")] InvalidCollection(String), /// Error when an event name is missing. /// /// This error occurs when attempting to view an event that is missing /// a required name field, which is necessary for display. - #[error("error-event-view-2 Event name is missing")] + #[error("error-smokesignal-event-view-2 Event name is missing")] MissingEventName, /// Error when RSVP count calculation fails. /// /// This error occurs when the system fails to retrieve or calculate /// the RSVP counts (going, interested, not going) for an event. - #[error("error-event-view-3 Failed to hydrate event RSVP counts: {0}")] + #[error("error-smokesignal-event-view-3 Failed to hydrate event RSVP counts: {0}")] FailedToHydrateRsvpCounts(String), } diff --git a/src/http/errors/import_error.rs b/src/http/errors/import_error.rs index 5d68d2d..15758a5 100644 --- a/src/http/errors/import_error.rs +++ b/src/http/errors/import_error.rs @@ -10,6 +10,6 @@ pub(crate) enum ImportError { /// /// This error occurs when attempting to retrieve a list of Smokesignal /// events during an import operation fails, preventing the import. - #[error("error-import-1 Failed to list Smokesignal events: {0}")] + #[error("error-smokesignal-import-1 Failed to list Smokesignal events: {0}")] FailedToListSmokesignalEvents(String), } diff --git a/src/http/errors/login_error.rs b/src/http/errors/login_error.rs index fe8e8c6..e6eaabe 100644 --- a/src/http/errors/login_error.rs +++ b/src/http/errors/login_error.rs @@ -6,7 +6,7 @@ use thiserror::Error; /// are logging in to the application, including OAuth flows and DID validation. #[derive(Debug, Error)] pub(crate) enum LoginError { - #[error("error-login-1 DID document does not contain a handle identifier")] + #[error("error-smokesignal-login-1 DID document does not contain a handle identifier")] NoHandle, /// Error when a DID document does not contain a PDS endpoint. @@ -14,21 +14,21 @@ pub(crate) enum LoginError { /// This error occurs during authentication when the user's DID document /// is retrieved but does not contain a required AT Protocol Personal /// Data Server (PDS) endpoint. - #[error("error-login-2 DID document does not contain an AT Protocol PDS endpoint")] + #[error("error-smokesignal-login-2 DID document does not contain an AT Protocol PDS endpoint")] NoPDS, /// Error when an OAuth callback is incomplete. /// /// This error occurs when the OAuth authentication flow callback /// returns with incomplete information, preventing successful authentication. - #[error("error-login-100 OAuth callback incomplete")] + #[error("error-smokesignal-login-3 OAuth callback incomplete")] OAuthCallbackIncomplete, /// Error when there is an OAuth issuer mismatch. /// /// This error occurs when the issuer in the OAuth response does not /// match the expected issuer, which could indicate a security issue. - #[error("error-login-101 OAuth issuer mismatch")] + #[error("error-smokesignal-login-4 OAuth issuer mismatch")] OAuthIssuerMismatch, /// Error when the login input appears to be an incomplete AT-handle. @@ -36,6 +36,35 @@ pub(crate) enum LoginError { /// This error occurs when a user enters what appears to be a partial /// AT-handle (alphanumeric without a domain). The user should append /// ".bsky.social" and resubmit the form. - #[error("error-login-102 Please add '.bsky.social' to your handle and try again")] + #[error("error-smokesignal-login-5 Please add '.bsky.social' to your handle and try again")] IncompleteHandle, + + /// Error when an OAuth request is not found in storage. + /// + /// This error occurs during the OAuth callback when the system cannot + /// find the corresponding OAuth request that was initiated, possibly + /// due to expiration or invalid state parameter. + #[error("error-smokesignal-login-6 oauth request not found in storage")] + OAuthRequestNotFound, + + /// Error when HTTP request for AIP OAuth userinfo fails. + /// + /// This error occurs during AIP OAuth flow when the HTTP request + /// to retrieve user information from the AIP server fails. + #[error("error-smokesignal-login-7 HTTP request for userinfo failed")] + AipUserinfoRequestFailed, + + /// Error when parsing AIP OAuth userinfo response fails. + /// + /// This error occurs during AIP OAuth flow when the JSON response + /// from the AIP userinfo endpoint cannot be parsed. + #[error("error-smokesignal-login-8 Parsing HTTP response for userinfo failed")] + AipUserinfoParsingFailed, + + /// Error when AIP OAuth server returns an error response. + /// + /// This error occurs when the AIP OAuth server returns an error + /// in the userinfo response instead of valid user claims. + #[error("error-smokesignal-login-9 AIP OAuth server error: {message}")] + AipServerError { message: String }, } diff --git a/src/http/errors/middleware_errors.rs b/src/http/errors/middleware_errors.rs index fed02c2..7f04de0 100644 --- a/src/http/errors/middleware_errors.rs +++ b/src/http/errors/middleware_errors.rs @@ -14,26 +14,26 @@ pub(crate) enum WebSessionError { /// /// This error occurs when attempting to deserialize a web session from JSON /// format, typically when retrieving a session from storage or a cookie. - #[error("error-websession-1 Unable to deserialize WebSession: {0:?}")] + #[error("error-smokesignal-websession-1 Unable to deserialize WebSession: {0:?}")] DeserializeFailed(serde_json::Error), /// Error when web session serialization fails. /// /// This error occurs when attempting to serialize a web session to JSON /// format, typically when storing a session in storage or a cookie. - #[error("error-websession-2 Unable to serialize WebSession: {0:?}")] + #[error("error-smokesignal-websession-2 Unable to serialize WebSession: {0:?}")] SerializeFailed(serde_json::Error), } #[derive(Debug, Error)] pub(crate) enum MiddlewareAuthError { - #[error("error-middleware-auth-1 Access Denied: {0}")] + #[error("error-smokesignal-middleware-auth-1 Access Denied: {0}")] AccessDenied(String), - #[error("error-middleware-auth-2 Not Found")] + #[error("error-smokesignal-middleware-auth-2 Not Found")] NotFound, - #[error("error-middleware-auth-3 Unhandled Auth Error: {0:?}")] + #[error("error-smokesignal-middleware-auth-3 Unhandled Auth Error: {0:?}")] Anyhow(#[from] anyhow::Error), } diff --git a/src/http/errors/migrate_event_error.rs b/src/http/errors/migrate_event_error.rs index 8955939..242934e 100644 --- a/src/http/errors/migrate_event_error.rs +++ b/src/http/errors/migrate_event_error.rs @@ -11,7 +11,7 @@ pub(crate) enum MigrateEventError { /// This error occurs when a user attempts to migrate an event that they /// do not have permission to modify, typically because they are not /// the event creator or an administrator. - #[error("error-migrate-event-2 Not authorized to migrate this event")] + #[error("error-smokesignal-migrate-event-1 Not authorized to migrate this event")] NotAuthorized, /// Error when attempting to migrate an unsupported event type. @@ -20,7 +20,7 @@ pub(crate) enum MigrateEventError { /// cannot be migrated, as only smokesignal events can be converted to /// the community event format. #[error( - "error-migrate-event-3 Unsupported event type. Only smokesignal events can be migrated" + "error-smokesignal-migrate-event-2 Unsupported event type. Only smokesignal events can be migrated" )] UnsupportedEventType, @@ -28,13 +28,13 @@ pub(crate) enum MigrateEventError { /// /// This error occurs when attempting to migrate an event that is already /// in the community event format, which would be redundant. - #[error("error-migrate-event-4 Event is already a community event")] + #[error("error-smokesignal-migrate-event-3 Event is already a community event")] AlreadyMigrated, /// Error when a destination URI conflict exists. /// /// This error occurs when attempting to migrate an event to a URI that /// already has an event associated with it, which would cause a conflict. - #[error("error-migrate-event-5 An event already exists at the destination URI")] + #[error("error-smokesignal-migrate-event-4 An event already exists at the destination URI")] DestinationExists, } diff --git a/src/http/errors/migrate_rsvp_error.rs b/src/http/errors/migrate_rsvp_error.rs index 29fd9be..44be2cb 100644 --- a/src/http/errors/migrate_rsvp_error.rs +++ b/src/http/errors/migrate_rsvp_error.rs @@ -12,7 +12,7 @@ pub(crate) enum MigrateRsvpError { /// that doesn't match one of the expected values ('going', 'interested', /// or 'notgoing'). #[error( - "error-migrate-rsvp-1 Invalid RSVP status: {0}. Expected 'going', 'interested', or 'notgoing'." + "error-smokesignal-migrate-rsvp-1 Invalid RSVP status: {0}. Expected 'going', 'interested', or 'notgoing'." )] InvalidRsvpStatus(String), @@ -21,6 +21,6 @@ pub(crate) enum MigrateRsvpError { /// This error occurs when a user attempts to migrate an RSVP that they /// do not have permission to modify, typically because they are not /// the RSVP owner or an administrator. - #[error("error-migrate-rsvp-2 Not authorized to migrate this RSVP")] + #[error("error-smokesignal-migrate-rsvp-2 Not authorized to migrate this RSVP")] NotAuthorized, } diff --git a/src/http/errors/mod.rs b/src/http/errors/mod.rs index bee680b..8a04200 100644 --- a/src/http/errors/mod.rs +++ b/src/http/errors/mod.rs @@ -2,6 +2,8 @@ pub mod admin_errors; pub mod common_error; pub mod create_event_errors; +pub mod create_rsvp_errors; +pub mod delete_event_errors; pub mod edit_event_error; pub mod event_view_errors; pub mod import_error; @@ -15,6 +17,8 @@ pub mod web_error; pub(crate) use common_error::CommonError; pub(crate) use create_event_errors::CreateEventError; +pub(crate) use create_rsvp_errors::CreateRsvpError; +pub(crate) use delete_event_errors::DeleteEventError; pub(crate) use edit_event_error::EditEventError; pub(crate) use event_view_errors::EventViewError; pub(crate) use import_error::ImportError; diff --git a/src/http/errors/url_error.rs b/src/http/errors/url_error.rs index dab5c4b..253f79d 100644 --- a/src/http/errors/url_error.rs +++ b/src/http/errors/url_error.rs @@ -10,6 +10,6 @@ pub(crate) enum UrlError { /// /// This error occurs when a URL contains a collection type that is not /// supported by the system, typically in an AT Protocol URI path. - #[error("error-url-1 Unsupported collection type")] + #[error("error-smokesignal-url-1 Unsupported collection type")] UnsupportedCollection, } diff --git a/src/http/errors/view_event_error.rs b/src/http/errors/view_event_error.rs index 384e3d3..0b5c5cf 100644 --- a/src/http/errors/view_event_error.rs +++ b/src/http/errors/view_event_error.rs @@ -10,13 +10,13 @@ pub(crate) enum ViewEventError { /// /// This error occurs when attempting to view an event that doesn't /// exist in the system, typically due to an invalid identifier. - #[error("error-view-event-1 Event not found: {0}")] + #[error("error-smokesignal-view-event-1 Event not found: {0}")] EventNotFound(String), /// Error when a fallback retrieval method fails. /// /// This error occurs when the primary method of retrieving an event fails, /// and the fallback method also fails to retrieve the event. - #[error("error-view-event-2 Failed to get event from fallback: {0}")] + #[error("error-smokesignal-view-event-2 Failed to get event from fallback: {0}")] FallbackFailed(String), } diff --git a/src/http/errors/web_error.rs b/src/http/errors/web_error.rs index 528e314..88f50c0 100644 --- a/src/http/errors/web_error.rs +++ b/src/http/errors/web_error.rs @@ -5,7 +5,7 @@ //! uniformly at the HTTP boundary and converted into appropriate HTTP responses. //! //! Specific error variants use their own error codes, while general errors use the -//! format: `error-web- :
` +//! format: `error-smokesignal-web- :
` use axum::http::StatusCode; use axum::response::IntoResponse; @@ -30,7 +30,7 @@ use super::url_error::UrlError; /// /// Most variants use transparent error forwarding to preserve the original error message /// and error code, while a few web-specific errors have their own error code format: -/// `error-web- :
` +/// `error-smokesignal-web- :
` #[derive(Debug, Error)] pub(crate) enum WebError { /// Error when authentication middleware fails. @@ -39,8 +39,8 @@ pub(crate) enum WebError { /// through the authentication middleware, such as invalid credentials or /// expired sessions. /// - /// **Error Code:** `error-web-1` - #[error("error-web-1 Middleware Auth Error: {0:?}")] + /// **Error Code:** `error-smokesignal-web-1` + #[error("error-smokesignal-web-1 Middleware Auth Error: {0:?}")] MiddlewareAuthError(#[from] MiddlewareAuthError), /// Error when an unexpected error occurs that isn't covered by other error types. @@ -48,11 +48,11 @@ pub(crate) enum WebError { /// This error is a fallback for any unhandled errors in the system. In production, /// these should be rare as most errors should be properly typed. /// - /// **Error Code:** `error-web-2` + /// **Error Code:** `error-smokesignal-web-2` /// /// Note: This should be replaced with more specific error types as part of /// the ongoing effort to use typed errors throughout the codebase. - #[error("error-web-2 Unhandled web error: {0:?}")] + #[error("error-smokesignal-web-2 Unhandled web error: {0:?}")] Anyhow(#[from] anyhow::Error), /// Common HTTP errors. diff --git a/src/http/event_form.rs b/src/http/event_form.rs index 1226026..c44d14f 100644 --- a/src/http/event_form.rs +++ b/src/http/event_form.rs @@ -7,55 +7,55 @@ use super::cache_countries::cached_countries; #[derive(Debug, Error)] pub(crate) enum BuildEventError { - #[error("error-event-builder-1 Invalid Name")] + #[error("error-smokesignal-event-builder-1 Invalid Name")] InvalidName, - #[error("error-event-builder-2 Invalid Description")] + #[error("error-smokesignal-event-builder-2 Invalid Description")] InvalidDescription, - #[error("error-event-builder-3 Invalid Time Zone")] + #[error("error-smokesignal-event-builder-3 Invalid Time Zone")] InvalidTimeZone, - #[error("error-event-builder-4 Invalid Status")] + #[error("error-smokesignal-event-builder-4 Invalid Status")] InvalidStatus, - #[error("error-event-builder-5 Invalid Mode")] + #[error("error-smokesignal-event-builder-5 Invalid Mode")] InvalidMode, - #[error("error-event-builder-6 Invalid Start Date/Time Format")] + #[error("error-smokesignal-event-builder-6 Invalid Start Date/Time Format")] InvalidStartDateTime, - #[error("error-event-builder-7 Invalid End Date/Time Format")] + #[error("error-smokesignal-event-builder-7 Invalid End Date/Time Format")] InvalidEndDateTime, - #[error("error-event-builder-8 End Date/Time Must Be After Start Date/Time")] + #[error("error-smokesignal-event-builder-8 End Date/Time Must Be After Start Date/Time")] EndBeforeStart, - #[error("error-event-builder-9 Address Location Country Missing")] + #[error("error-smokesignal-event-builder-9 Address Location Country Missing")] LocationCountryRequired, - #[error("error-event-builder-10 Invalid Address Location Country: {0}")] + #[error("error-smokesignal-event-builder-10 Invalid Address Location Country: {0}")] LocationCountryInvalid(String), - #[error("error-event-builder-11 Invalid Address Location Locality")] + #[error("error-smokesignal-event-builder-11 Invalid Address Location Locality")] InvalidLocationAddressLocality, - #[error("error-event-builder-12 Invalid Address Location Region")] + #[error("error-smokesignal-event-builder-12 Invalid Address Location Region")] InvalidLocationAddressRegion, - #[error("error-event-builder-13 Invalid Address Location Street")] + #[error("error-smokesignal-event-builder-13 Invalid Address Location Street")] InvalidLocationAddressStreet, - #[error("error-event-builder-14 Invalid Address Location Postal Code")] + #[error("error-smokesignal-event-builder-14 Invalid Address Location Postal Code")] InvalidLocationAddressPostalCode, - #[error("error-event-builder-15 Invalid Address Location Name")] + #[error("error-smokesignal-event-builder-15 Invalid Address Location Name")] InvalidLocationAddressName, - #[error("error-event-builder-16 Invalid Link URL")] + #[error("error-smokesignal-event-builder-16 Invalid Link URL")] InvalidLinkValue, - #[error("error-event-builder-17 Invalid Link Name")] + #[error("error-smokesignal-event-builder-17 Invalid Link Name")] InvalidLinkName, } diff --git a/src/http/handle_create_event.rs b/src/http/handle_create_event.rs index ca9b70b..c06cb89 100644 --- a/src/http/handle_create_event.rs +++ b/src/http/handle_create_event.rs @@ -304,7 +304,7 @@ pub(crate) async fn handle_create_event( language, error_template, default_context, - anyhow::anyhow!("Server error: {}", err.error_message()) + CreateEventError::ServerError { message: err.error_message() } ); } Err(err) => { diff --git a/src/http/handle_create_rsvp.rs b/src/http/handle_create_rsvp.rs index 90d2f43..e7d634f 100644 --- a/src/http/handle_create_rsvp.rs +++ b/src/http/handle_create_rsvp.rs @@ -24,7 +24,7 @@ use crate::{ contextual_error, http::{ context::WebContext, - errors::{CommonError, WebError}, + errors::{CommonError, CreateRsvpError, WebError}, middleware_auth::Auth, middleware_i18n::Language, rsvp_form::{BuildRSVPForm, BuildRsvpContentState}, @@ -195,7 +195,7 @@ pub(crate) async fn handle_create_rsvp( language, error_template, default_context, - anyhow::anyhow!("Server error: {}", err.error_message()) + CreateRsvpError::ServerError { message: err.error_message() } ); } Err(err) => { diff --git a/src/http/handle_delete_event.rs b/src/http/handle_delete_event.rs index 99f50c2..0dba12e 100644 --- a/src/http/handle_delete_event.rs +++ b/src/http/handle_delete_event.rs @@ -1,4 +1,4 @@ -use anyhow::{Result, anyhow}; +use anyhow::Result; use axum::{extract::Path, response::IntoResponse}; use axum_extra::extract::Form; use axum_template::RenderHtml; @@ -15,7 +15,7 @@ use crate::{ }, config::OAuthBackendConfig, contextual_error, - http::{context::UserRequestContext, errors::WebError, middleware_auth::Auth}, + http::{context::UserRequestContext, errors::{DeleteEventError, WebError}, middleware_auth::Auth}, select_template, storage::event::{event_delete, event_exists}, }; @@ -70,9 +70,7 @@ pub(crate) async fn handle_delete_event( ctx.language, error_template, default_context, - anyhow!( - "error-delete-event-1 identity does not have event with that AT-URI: {lookup_aturi}" - ) + DeleteEventError::EventNotFound { aturi: lookup_aturi.clone() } ); } @@ -129,7 +127,7 @@ pub(crate) async fn handle_delete_event( ctx.language, error_template, default_context, - anyhow!("Authentication configuration mismatch"), + DeleteEventError::AuthenticationConfigurationMismatch, StatusCode::INTERNAL_SERVER_ERROR ); } diff --git a/src/http/handle_edit_event.rs b/src/http/handle_edit_event.rs index dd7d92b..a3f7692 100644 --- a/src/http/handle_edit_event.rs +++ b/src/http/handle_edit_event.rs @@ -633,7 +633,7 @@ pub(crate) async fn handle_edit_event( ctx.language, error_template, default_context, - anyhow::anyhow!("Server error: {}", err.error_message()), + EditEventError::ServerError { message: err.error_message() }, StatusCode::OK ); } diff --git a/src/http/handle_oauth_aip_callback.rs b/src/http/handle_oauth_aip_callback.rs index 3777785..a0c3dc3 100644 --- a/src/http/handle_oauth_aip_callback.rs +++ b/src/http/handle_oauth_aip_callback.rs @@ -4,7 +4,7 @@ use crate::{ config::OAuthBackendConfig, contextual_error, select_template, storage::identity_profile::handle_warm_up, }; -use anyhow::{Context, Result, anyhow, bail}; +use anyhow::Result; use atproto_client::errors::SimpleError; use atproto_identity::resolve::IdentityResolver; use axum::{ @@ -84,7 +84,7 @@ pub(crate) async fn handle_oauth_callback( language, error_template, default_context, - anyhow::anyhow!("oauth request not found in storage") + LoginError::OAuthRequestNotFound ); } Ok(Some(value)) => value, @@ -275,13 +275,15 @@ async fn get_email_from_userinfo( .bearer_auth(aip_access_token) .send() .await - .context(anyhow!("HTTP request for userinfo failed"))? + .map_err(|_| LoginError::AipUserinfoRequestFailed)? .json() .await - .context(anyhow!("Parsing HTTP response for userinfo failed"))?; + .map_err(|_| LoginError::AipUserinfoParsingFailed)?; match response { OpenIDClaimsResponse::OpenIDClaims(claims) => Ok((claims.did, claims.email)), - OpenIDClaimsResponse::SimpleError(simple_error) => bail!(simple_error.error_message()), + OpenIDClaimsResponse::SimpleError(simple_error) => Err(LoginError::AipServerError { + message: simple_error.error_message() + }.into()), } } diff --git a/src/http/handle_oauth_callback.rs b/src/http/handle_oauth_callback.rs index 67f27a3..a8a3b25 100644 --- a/src/http/handle_oauth_callback.rs +++ b/src/http/handle_oauth_callback.rs @@ -1,4 +1,4 @@ -use anyhow::{Result, anyhow}; +use anyhow::Result; use atproto_identity::{axum::state::KeyProviderExtractor, key::identify_key}; use atproto_oauth::{ resources::oauth_authorization_server, @@ -75,7 +75,7 @@ pub(crate) async fn handle_oauth_callback( language, error_template, default_context, - anyhow!("oauth request not found in storage") + LoginError::OAuthRequestNotFound ); } Ok(Some(value)) => value, diff --git a/src/http/handle_settings.rs b/src/http/handle_settings.rs index 4b1a7f8..856c05f 100644 --- a/src/http/handle_settings.rs +++ b/src/http/handle_settings.rs @@ -138,7 +138,7 @@ pub(crate) async fn handle_timezone_update( language, error_template, default_context, - "error-xxx Invalid timezone" + "error-smokesignal-settings-1 Invalid timezone" ); } @@ -209,7 +209,7 @@ pub(crate) async fn handle_language_update( language, error_template, default_context, - "error-xxx Invalid language" + "error-smokesignal-settings-2 Invalid language" ); } @@ -231,7 +231,7 @@ pub(crate) async fn handle_language_update( language, error_template, default_context, - "error-xxx Invalid language" + "error-smokesignal-settings-2 Invalid language" ); } @@ -473,7 +473,7 @@ pub(crate) async fn handle_add_webhook( language, error_template, default_context, - "error-xxx Service cannot be empty" + "error-smokesignal-settings-3 Service cannot be empty" ); } @@ -487,7 +487,7 @@ pub(crate) async fn handle_add_webhook( language, error_template, default_context, - "error-xxx Only SmokeSignalAutomation services are supported" + "error-smokesignal-settings-4 Only SmokeSignalAutomation services are supported" ); } diff --git a/src/http/mod.rs b/src/http/mod.rs index e610b05..4c8ae96 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -47,5 +47,6 @@ pub mod rsvp_form; pub mod server; pub mod tab_selector; pub mod templates; +pub mod timezone_errors; pub mod timezones; pub mod utils; diff --git a/src/http/timezone_errors.rs b/src/http/timezone_errors.rs new file mode 100644 index 0000000..e4d28a1 --- /dev/null +++ b/src/http/timezone_errors.rs @@ -0,0 +1,22 @@ +use thiserror::Error; + +/// Represents errors that can occur during timezone and datetime operations. +/// +/// These errors typically occur when parsing date/time strings or converting +/// between timezones, especially during daylight saving time transitions. +#[derive(Debug, Error)] +pub(crate) enum TimezoneError { + /// Error when parsing date and time strings fails. + /// + /// This error occurs when the provided date or time string cannot be parsed + /// into a valid datetime format, typically due to incorrect format or invalid values. + #[error("error-smokesignal-timezone-1 Failed to parse date and time: {error}")] + DateTimeParsingFailed { error: String }, + + /// Error when local time is ambiguous or non-existent. + /// + /// This error occurs during daylight saving time transitions when a local time + /// either doesn't exist (spring forward) or is ambiguous (fall back). + #[error("error-smokesignal-timezone-2 Ambiguous or non-existent local time")] + AmbiguousOrNonExistentLocalTime, +} \ No newline at end of file diff --git a/src/http/timezones.rs b/src/http/timezones.rs index eeeccde..deb785e 100644 --- a/src/http/timezones.rs +++ b/src/http/timezones.rs @@ -1,7 +1,8 @@ -use anyhow::{Result, anyhow}; +use anyhow::Result; use chrono::{DateTime, NaiveDateTime, Utc}; use itertools::Itertools; +use crate::http::timezone_errors::TimezoneError; use crate::storage::identity_profile::model::IdentityProfile; pub(crate) fn supported_timezones(handle: Option<&IdentityProfile>) -> (&str, Vec<&str>) { @@ -79,13 +80,13 @@ pub(crate) fn combine_html_datetime( // Parse the combined string into a NaiveDateTime let naive_dt = NaiveDateTime::parse_from_str(&datetime_str, "%Y-%m-%dT%H:%M") - .map_err(|e| anyhow!("Failed to parse date and time: {}", e))?; + .map_err(|e| TimezoneError::DateTimeParsingFailed { error: e.to_string() })?; // Convert to timezone-aware datetime in the specified timezone let local_dt = naive_dt .and_local_timezone(timezone) .single() - .ok_or_else(|| anyhow!("Ambiguous or non-existent local time"))?; + .ok_or(TimezoneError::AmbiguousOrNonExistentLocalTime)?; // Convert to UTC Ok(local_dt.with_timezone(&Utc)) diff --git a/src/i18n.rs b/src/i18n.rs index e313cfc..b791a71 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -165,13 +165,13 @@ pub mod errors { #[derive(Debug, Error)] pub enum I18nError { - #[error("error-i18n-1 Invalid language")] + #[error("error-smokesignal-i18n-1 Invalid language")] InvalidLanguage, - #[error("error-i18n-2 Language resource failed")] + #[error("error-smokesignal-i18n-2 Language resource failed")] LanguageResourceFailed(Vec), - #[error("error-i18n-3 Bundle load failed")] + #[error("error-smokesignal-i18n-3 Bundle load failed")] BundleLoadFailed(Vec), } } diff --git a/src/lib.rs b/src/lib.rs index 51b88d3..cb1b782 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub mod http; pub mod i18n; pub mod key_provider; pub mod processor; +pub mod processor_errors; pub mod refresh_tokens_errors; pub mod service; pub mod storage; @@ -14,5 +15,7 @@ pub mod task_identity_refresh; pub mod task_oauth_requests_cleanup; pub mod task_refresh_tokens; pub mod task_search_indexer; +pub mod task_search_indexer_errors; pub mod task_webhooks; +pub mod task_webhooks_errors; pub mod webhooks; diff --git a/src/processor.rs b/src/processor.rs index 8a901a3..4483965 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -1,4 +1,4 @@ -use anyhow::{Result, anyhow}; +use anyhow::Result; use atproto_client::com::atproto::repo::get_blob; use atproto_identity::model::Document; use atproto_identity::storage::DidDocumentStorage; @@ -8,6 +8,7 @@ use serde_json::Value; use std::sync::Arc; use crate::atproto::lexicon::community::lexicon::calendar::event::Event; +use crate::processor_errors::ProcessorError; use crate::atproto::lexicon::community::lexicon::calendar::event::Media; use crate::atproto::lexicon::community::lexicon::calendar::event::NSID as LexiconCommunityEventNSID; use crate::atproto::lexicon::community::lexicon::calendar::rsvp::NSID as LexiconCommunityRSVPNSID; @@ -144,7 +145,7 @@ impl ContentFetcher { let pds_endpoints = document.pds_endpoints(); let pds_endpoint = pds_endpoints .first() - .ok_or_else(|| anyhow!("no PDS in DID"))?; + .ok_or(ProcessorError::NoPdsInDid)?; let name = match &event_record { Event::Current { name, .. } => name.clone(), diff --git a/src/processor_errors.rs b/src/processor_errors.rs new file mode 100644 index 0000000..b0d7084 --- /dev/null +++ b/src/processor_errors.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +/// Represents errors that can occur during event processing. +/// +/// These errors typically occur when processing events from the Jetstream +/// consumer, including DID resolution and data validation issues. +#[derive(Debug, Error)] +pub(crate) enum ProcessorError { + /// Error when a DID document does not contain any PDS endpoints. + /// + /// This error occurs when attempting to process an event from a DID + /// that has no Personal Data Server (PDS) endpoints configured. + #[error("error-smokesignal-processor-1 no PDS in DID")] + NoPdsInDid, +} \ No newline at end of file diff --git a/src/refresh_tokens_errors.rs b/src/refresh_tokens_errors.rs index 0a6c22a..9def523 100644 --- a/src/refresh_tokens_errors.rs +++ b/src/refresh_tokens_errors.rs @@ -10,27 +10,27 @@ pub enum RefreshError { /// /// This error occurs when attempting to refresh a token but the necessary /// secret key for signing the request is not available in the configuration. - #[error("error-refresh-1 Secret signing key not found")] + #[error("error-smokesignal-refresh-1 Secret signing key not found")] SecretSigningKeyNotFound, /// Error when creating a DPoP proof for token refresh fails. /// /// This error occurs when there is an issue with the cryptographic operations /// required to generate a DPoP (Demonstrating Proof-of-Possession) proof. - #[error("error-refresh-2 Failed to create DPoP proof: {0:?}")] + #[error("error-smokesignal-refresh-2 Failed to create DPoP proof: {0:?}")] DpopProofCreationFailed(elliptic_curve::Error), /// Error when a session cannot be placed in the refresh queue. /// /// This error occurs when there is an issue with the Redis-backed queue /// used to manage session refresh operations. - #[error("error-refresh-3 Failed to place session group into refresh queue: {0:?}")] + #[error("error-smokesignal-refresh-3 Failed to place session group into refresh queue: {0:?}")] PlaceInRefreshQueueFailed(deadpool_redis::redis::RedisError), /// Error when the identity document cannot be found. /// /// This error occurs when attempting to refresh a token but the necessary /// identity document for the user is not available in storage. - #[error("error-refresh-4 Identity document not found")] + #[error("error-smokesignal-refresh-4 Identity document not found")] IdentityDocumentNotFound, } diff --git a/src/storage/atproto.rs b/src/storage/atproto.rs index 66986d6..cac00f0 100644 --- a/src/storage/atproto.rs +++ b/src/storage/atproto.rs @@ -44,7 +44,7 @@ impl PostgresDidDocumentStorage { impl DidDocumentStorage for PostgresDidDocumentStorage { async fn get_document_by_did(&self, did: &str) -> Result, anyhow::Error> { if did.trim().is_empty() { - return Err(anyhow::anyhow!("DID cannot be empty")); + return Err(StorageError::DidCannotBeEmpty.into()); } let mut tx = self.pool.begin().await?; @@ -97,7 +97,7 @@ impl DidDocumentStorage for PostgresDidDocumentStorage { async fn delete_document_by_did(&self, did: &str) -> Result<(), anyhow::Error> { if did.trim().is_empty() { - return Err(anyhow::anyhow!("DID cannot be empty")); + return Err(StorageError::DidCannotBeEmpty.into()); } let mut tx = self.pool.begin().await?; @@ -226,7 +226,7 @@ impl OAuthRequestStorage for PostgresOAuthRequestStorage { state: &str, ) -> Result, anyhow::Error> { if state.trim().is_empty() { - return Err(anyhow::anyhow!("OAuth state cannot be empty")); + return Err(StorageError::OAuthStateCannotBeEmpty.into()); } let mut tx = self.pool.begin().await?; @@ -263,7 +263,7 @@ impl OAuthRequestStorage for PostgresOAuthRequestStorage { async fn delete_oauth_request_by_state(&self, state: &str) -> Result<(), anyhow::Error> { if state.trim().is_empty() { - return Err(anyhow::anyhow!("OAuth state cannot be empty")); + return Err(StorageError::OAuthStateCannotBeEmpty.into()); } let mut tx = self.pool.begin().await?; diff --git a/src/storage/content.rs b/src/storage/content.rs index 3eb3065..36a955b 100644 --- a/src/storage/content.rs +++ b/src/storage/content.rs @@ -447,7 +447,7 @@ impl ContentStorage for CachedContentStorage { Err(e) => { // If it's a not-found error, add to the not-found filter if e.to_string() - .starts_with("error-content-2 File storage operation failed:") + .starts_with("error-smokesignal-content-2 File storage operation failed:") { self.add_to_not_found_filter(cid).await; } diff --git a/src/storage/errors.rs b/src/storage/errors.rs index c6b742c..e66331c 100644 --- a/src/storage/errors.rs +++ b/src/storage/errors.rs @@ -11,7 +11,7 @@ pub enum OAuthModelError { /// This error occurs when attempting to convert a JSON Web Key (JWK) /// into a secret key for DPoP (Demonstrating Proof-of-Possession) operations, /// typically due to invalid key format or cryptographic errors. - #[error("error-oauth-model-1 Failed to create DPoP secret from JWK: {0:?}")] + #[error("error-smokesignal-oauth-model-1 Failed to create DPoP secret from JWK: {0:?}")] DpopSecretFromJwkFailed(elliptic_curve::Error), /// Error when the OAuth flow state is invalid. @@ -19,14 +19,14 @@ pub enum OAuthModelError { /// This error occurs when the state parameter in an OAuth flow /// does not match the expected value or cannot be verified, /// which could indicate a potential CSRF attack or session mismatch. - #[error("error-oauth-model-2 Invalid OAuth flow state")] + #[error("error-smokesignal-oauth-model-2 Invalid OAuth flow state")] InvalidOAuthFlowState(), /// Error when deserializing DPoP JWK from string fails. /// /// This error occurs when attempting to deserialize a string-encoded /// JSON Web Key (JWK) for DPoP operations, typically due to invalid JSON format. - #[error("error-oauth-model-5 Failed to deserialize DPoP JWK: {0:?}")] + #[error("error-smokesignal-oauth-model-3 Failed to deserialize DPoP JWK: {0:?}")] DpopJwkDeserializationFailed(serde_json::Error), /// Error when required OAuth session data is missing. @@ -34,7 +34,7 @@ pub enum OAuthModelError { /// This error occurs when attempting to use an OAuth session /// that is missing critical data needed for authentication or /// authorization operations, such as tokens or session identifiers. - #[error("error-oauth-model-3 Missing required OAuth session data")] + #[error("error-smokesignal-oauth-model-4 Missing required OAuth session data")] MissingRequiredOAuthSessionData(), /// Error when an OAuth session has expired. @@ -42,7 +42,7 @@ pub enum OAuthModelError { /// This error occurs when attempting to use an OAuth session /// that has exceeded its validity period and is no longer usable /// for authentication or authorization purposes. - #[error("error-oauth-model-4 OAuth session has expired")] + #[error("error-smokesignal-oauth-model-5 OAuth session has expired")] OAuthSessionExpired(), } @@ -53,64 +53,78 @@ pub enum StorageError { /// /// This error occurs when attempting to retrieve a web session using an /// invalid or expired session ID. - #[error("error-storage-1 Web session not found")] + #[error("error-smokesignal-storage-1 Web session not found")] WebSessionNotFound, /// Error when a handle cannot be found in the database. /// /// This error occurs when attempting to retrieve a user handle that /// doesn't exist in the system. - #[error("error-storage-2 Handle not found")] + #[error("error-smokesignal-storage-2 Handle not found")] HandleNotFound, /// Error when a database record cannot be found. /// /// This error occurs when attempting to retrieve a specific record /// using an ID or other identifier that doesn't exist in the database. - #[error("error-storage-3 Record not found: {0} {1:?}")] + #[error("error-smokesignal-storage-3 Record not found: {0} {1:?}")] RowNotFound(String, sqlx::Error), /// Error when a database transaction cannot be committed. /// /// This error occurs when there is an issue finalizing a database /// transaction, potentially causing data inconsistency. - #[error("error-storage-4 Cannot commit database transaction: {0:?}")] + #[error("error-smokesignal-storage-4 Cannot commit database transaction: {0:?}")] CannotCommitDatabaseTransaction(sqlx::Error), /// Error when a database transaction cannot be started. /// /// This error occurs when there is an issue initiating a database /// transaction, typically due to connection issues or database constraints. - #[error("error-storage-5 Cannot begin database transaction: {0:?}")] + #[error("error-smokesignal-storage-5 Cannot begin database transaction: {0:?}")] CannotBeginDatabaseTransaction(sqlx::Error), /// Error when a database query cannot be executed. /// /// This error occurs when a SQL query fails to execute, typically due to /// syntax errors, constraint violations, or database connectivity issues. - #[error("error-storage-6 Unable to execute query: {0:?}")] + #[error("error-smokesignal-storage-6 Unable to execute query: {0:?}")] UnableToExecuteQuery(sqlx::Error), /// Error when an OAuth request cannot be found. /// /// This error occurs when attempting to retrieve an OAuth request /// that doesn't exist or has expired. - #[error("error-storage-7 OAuth request not found")] + #[error("error-smokesignal-storage-7 OAuth request not found")] OAuthRequestNotFound, /// Error when an RSVP cannot be found. /// /// This error occurs when attempting to retrieve an RSVP record /// that doesn't exist in the database. - #[error("error-storage-8 RSVP not found")] + #[error("error-smokesignal-storage-8 RSVP not found")] RSVPNotFound, /// Error when an OAuth model operation fails. /// /// This error occurs when there's an issue with OAuth model operations, /// such as token generation, validation, or storage. - #[error("error-storage-9 OAuth model error: {0}")] + #[error("error-smokesignal-storage-9 OAuth model error: {0}")] OAuthModelError(#[from] OAuthModelError), + + /// Error when a DID parameter is empty or invalid. + /// + /// This error occurs when a DID string is empty, which is not valid + /// for DID-based operations. + #[error("error-smokesignal-storage-10 DID cannot be empty")] + DidCannotBeEmpty, + + /// Error when an OAuth state parameter is empty or invalid. + /// + /// This error occurs when an OAuth state string is empty, which is not valid + /// for OAuth operations. + #[error("error-smokesignal-storage-11 OAuth state cannot be empty")] + OAuthStateCannotBeEmpty, } /// Represents errors that can occur during cache operations. @@ -120,34 +134,34 @@ pub enum CacheError { /// /// This error occurs when the system fails to initialize the Redis /// connection pool, typically due to configuration or connectivity issues. - #[error("error-cache-1 Failed to create cache pool: {0:?}")] + #[error("error-smokesignal-cache-1 Failed to create cache pool: {0:?}")] FailedToCreatePool(deadpool_redis::CreatePoolError), /// Error when a cache connection cannot be obtained. /// /// This error occurs when the system fails to get a connection from /// the Redis connection pool, typically due to pool exhaustion or connectivity issues. - #[error("error-cache-2 Failed to get connection: {0:?}")] + #[error("error-smokesignal-cache-2 Failed to get connection: {0:?}")] FailedToGetConnection(deadpool_redis::PoolError), /// Error when a session cannot be placed in the refresh queue. /// /// This error occurs when the system fails to add a session to the /// Redis-backed refresh queue, typically due to Redis errors or connectivity issues. - #[error("error-cache-3 Failed to place session group into refresh queue: {0:?}")] + #[error("error-smokesignal-cache-3 Failed to place session group into refresh queue: {0:?}")] FailedToPlaceInRefreshQueue(deadpool_redis::redis::RedisError), } #[derive(Debug, Error)] pub enum ContentError { - #[error("error-content-1 Invalid S3 URL format: {details}")] + #[error("error-smokesignal-content-1 Invalid S3 URL format: {details}")] /// Failed to parse S3 URL format from environment variable. ConfigS3UrlInvalid { /// Details about the S3 URL parsing error. details: String, }, - #[error("error-content-2 File storage operation failed: {operation}")] + #[error("error-smokesignal-content-2 File storage operation failed: {operation}")] /// File storage operation failed. StorageFileOperationFailed { /// Description of the failed file operation. diff --git a/src/task_search_indexer.rs b/src/task_search_indexer.rs index 928c619..0ecc049 100644 --- a/src/task_search_indexer.rs +++ b/src/task_search_indexer.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use tokio_util::sync::CancellationToken; use crate::atproto::lexicon::community::lexicon::calendar::event::NSID as LexiconCommunityEventNSID; +use crate::task_search_indexer_errors::SearchIndexerError; use crate::{ atproto::lexicon::community::lexicon::calendar::event::Event, consumer::{SmokeSignalEvent, SmokeSignalEventReceiver}, @@ -89,7 +90,7 @@ impl SearchIndexer { tracing::info!("Created OpenSearch index {}", INDEX_NAME); } else { let error_body = response.text().await?; - return Err(anyhow::anyhow!("Failed to create index: {}", error_body)); + return Err(SearchIndexerError::IndexCreationFailed { error_body }.into()); } Ok(()) diff --git a/src/task_search_indexer_errors.rs b/src/task_search_indexer_errors.rs new file mode 100644 index 0000000..c893b0a --- /dev/null +++ b/src/task_search_indexer_errors.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +/// Represents errors that can occur during search indexing operations. +/// +/// These errors typically occur when interacting with OpenSearch +/// during the indexing process. +#[derive(Debug, Error)] +pub(crate) enum SearchIndexerError { + /// Error when OpenSearch index creation fails. + /// + /// This error occurs when attempting to create an OpenSearch index + /// and the operation fails with a server error response. + #[error("error-smokesignal-search-indexer-1 Failed to create index: {error_body}")] + IndexCreationFailed { error_body: String }, +} \ No newline at end of file diff --git a/src/task_webhooks.rs b/src/task_webhooks.rs index ea34c2d..6d0c5c8 100644 --- a/src/task_webhooks.rs +++ b/src/task_webhooks.rs @@ -14,6 +14,7 @@ use tokio_util::sync::CancellationToken; use crate::{ service::{ServiceDID, ServiceKey}, storage::webhook::webhook_failed, + task_webhooks_errors::WebhookError, webhooks::{ EVENT_CREATED_EVENT, RSVP_CREATED_EVENT, SMOKE_SIGNAL_AUTOMATION_SERVICE, TEST_EVENT, }, @@ -153,10 +154,9 @@ impl WebhookProcessor { ) -> Result<()> { // Remove the suffix from service if !service.ends_with(SMOKE_SIGNAL_AUTOMATION_SERVICE) { - return Err(anyhow::anyhow!( - "Service must end with {}", - SMOKE_SIGNAL_AUTOMATION_SERVICE - )); + return Err(WebhookError::InvalidServiceSuffix { + suffix: SMOKE_SIGNAL_AUTOMATION_SERVICE.to_string() + }.into()); } let service_did = service @@ -168,15 +168,20 @@ impl WebhookProcessor { .document_storage .get_document_by_did(service_did) .await - .map_err(|e| anyhow::anyhow!("Failed to get DID document for {}: {}", service_did, e))? - .ok_or_else(|| anyhow::anyhow!("DID document not found for {}", service_did))?; + .map_err(|e| WebhookError::DidDocumentRetrievalFailed { + did: service_did.to_string(), + error: e.to_string() + })? + .ok_or_else(|| WebhookError::DidDocumentNotFound { + did: service_did.to_string() + })?; // Extract the service endpoint let automation_service = document .service .iter() .find(|service| service.id.ends_with(SMOKE_SIGNAL_AUTOMATION_SERVICE)) - .ok_or_else(|| anyhow::anyhow!("service not found in DID document"))?; + .ok_or(WebhookError::ServiceNotFoundInDidDocument)?; // Get the service endpoint - it should be a string URL let endpoint_url = &automation_service.service_endpoint; @@ -206,7 +211,9 @@ impl WebhookProcessor { ); let token = mint(&self.service_key.0, &header, &claims) - .map_err(|e| anyhow::anyhow!("Failed to create JWT: {}", e))?; + .map_err(|e| WebhookError::JwtCreationFailed { + error: e.to_string() + })?; // Prepare headers with JWT authorization let mut headers = reqwest::header::HeaderMap::new(); @@ -241,7 +248,7 @@ impl WebhookProcessor { // Update database to mark webhook as failed webhook_failed(&self.pool, identity, service, &error_msg).await?; - Err(anyhow::anyhow!("Webhook failed: {}", error_msg)) + Err(WebhookError::WebhookRequestFailed { error: error_msg }.into()) } Err(e) => { let error_msg = format!("Request failed: {}", e); @@ -250,7 +257,7 @@ impl WebhookProcessor { // Update database to mark webhook as failed webhook_failed(&self.pool, identity, service, &error_msg).await?; - Err(anyhow::anyhow!("Webhook request failed: {}", e)) + Err(WebhookError::WebhookTransportFailed { error: e.to_string() }.into()) } } } diff --git a/src/task_webhooks_errors.rs b/src/task_webhooks_errors.rs new file mode 100644 index 0000000..0cd2009 --- /dev/null +++ b/src/task_webhooks_errors.rs @@ -0,0 +1,57 @@ +use thiserror::Error; + +/// Represents errors that can occur during webhook processing. +/// +/// These errors typically occur when processing webhook notifications, +/// including service validation, DID resolution, and HTTP request failures. +#[derive(Debug, Error)] +pub(crate) enum WebhookError { + /// Error when a service doesn't end with the required suffix. + /// + /// This error occurs when validating a webhook service URL that doesn't + /// end with the expected automation service suffix. + #[error("error-smokesignal-webhook-1 Service must end with {suffix}")] + InvalidServiceSuffix { suffix: String }, + + /// Error when failing to get a DID document. + /// + /// This error occurs when attempting to retrieve a DID document + /// for service validation fails. + #[error("error-smokesignal-webhook-2 Failed to get DID document for {did}: {error}")] + DidDocumentRetrievalFailed { did: String, error: String }, + + /// Error when a DID document is not found. + /// + /// This error occurs when a required DID document cannot be found + /// in the system. + #[error("error-smokesignal-webhook-3 DID document not found for {did}")] + DidDocumentNotFound { did: String }, + + /// Error when a service is not found in a DID document. + /// + /// This error occurs when a DID document doesn't contain the expected + /// automation service entry. + #[error("error-smokesignal-webhook-4 service not found in DID document")] + ServiceNotFoundInDidDocument, + + /// Error when JWT creation fails. + /// + /// This error occurs when attempting to create a JWT token for + /// webhook authentication fails. + #[error("error-smokesignal-webhook-5 Failed to create JWT: {error}")] + JwtCreationFailed { error: String }, + + /// Error when a webhook request fails with an error response. + /// + /// This error occurs when the webhook endpoint returns an error + /// status code or the request fails. + #[error("error-smokesignal-webhook-6 Webhook failed: {error}")] + WebhookRequestFailed { error: String }, + + /// Error when a webhook request fails due to network or other issues. + /// + /// This error occurs when the HTTP request to the webhook endpoint + /// fails due to network issues or other transport problems. + #[error("error-smokesignal-webhook-7 Webhook request failed: {error}")] + WebhookTransportFailed { error: String }, +} \ No newline at end of file