Skip to main content

xmtp_mls/groups/
error.rs

1use super::group_permissions::GroupMutablePermissionsError;
2use super::mls_sync::GroupMessageProcessingError;
3use super::summary::SyncSummary;
4use super::{intents::IntentError, validated_commit::CommitValidationError};
5use crate::identity::IdentityError;
6use crate::mls_store::MlsStoreError;
7use crate::{
8    identity_updates::{IdentityUpdateError, InstallationDiffError},
9    intents::ProcessIntentError,
10};
11use openmls::{
12    error::LibraryError,
13    group::{
14        CommitToPendingProposalsError, CreateGroupContextExtProposalError, ProposalError,
15        ProposeAddMemberError, ProposeRemoveMemberError,
16    },
17    prelude::{BasicCredentialError, Error as TlsCodecError},
18};
19use thiserror::Error;
20use xmtp_common::ErrorCode;
21use xmtp_common::retry::RetryableError;
22use xmtp_content_types::CodecError;
23use xmtp_cryptography::signature::IdentifierValidationError;
24use xmtp_db::NotFound;
25use xmtp_db::sql_key_store;
26use xmtp_mls_group_keys::mls_ext::payload_encryption::{UnwrapPayloadError, WrapPayloadError};
27use xmtp_mls_group_state::group_metadata::GroupMetadataError;
28use xmtp_mls_group_state::group_mutable_metadata::GroupMutableMetadataError;
29use xmtp_proto::types::GroupId;
30
31/// Installation IDs that failed key package verification during a membership update.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct FailedInstallationIds(pub Vec<Vec<u8>>);
34
35impl std::fmt::Display for FailedInstallationIds {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        let encoded: Vec<String> = self.0.iter().map(hex::encode).collect();
38        write!(f, "{}", encoded.join(","))
39    }
40}
41
42#[derive(Debug, Error, ErrorCode)]
43pub enum GroupError {
44    /// Application messages cannot use a reserved transcript content type. Not retryable.
45    #[error("reserved transcript content type")]
46    ReservedTranscriptContentType,
47    /// The message id belongs to another sender inbox. Not retryable.
48    #[error("message id belongs to another sender inbox")]
49    MessageSenderConflict,
50    #[error(transparent)]
51    #[error_code(inherit)]
52    OutgoingPreparation(#[from] super::mls_sync::publish::OutgoingPreparationError),
53    #[error(transparent)]
54    #[error_code(inherit)]
55    NotFound(#[from] NotFound),
56    /// Max user limit exceeded.
57    ///
58    /// Attempted to add too many members. Not retryable.
59    #[error("Max user limit exceeded.")]
60    UserLimitExceeded,
61    /// Sequence ID not found.
62    ///
63    /// No sequence ID for an inbox after an identity-update refresh —
64    /// its registration hasn't propagated yet. Retryable.
65    #[error("SequenceId not found in local db")]
66    MissingSequenceId,
67    /// Addresses not found.
68    ///
69    /// Specified addresses have no XMTP identity. Not retryable.
70    #[error("Addresses not found {0:?}")]
71    AddressNotFound(Vec<String>),
72    /// API error.
73    ///
74    /// Network request failed. Retryable.
75    #[error("api error: {0}")]
76    WrappedApi(#[from] xmtp_api::ApiError),
77    /// Invalid group membership.
78    ///
79    /// Group membership state is invalid. Not retryable.
80    #[error("invalid group membership")]
81    InvalidGroupMembership,
82    /// Consent act refused.
83    ///
84    /// The consent of this conversation cannot be set. Not retryable.
85    #[error("consent act: {0}")]
86    #[error_code(inherit)]
87    ConsentAct(#[from] xmtp_db::consent_record::ConsentActError),
88    /// Leave cannot be processed.
89    ///
90    /// Group leave validation failed. Not retryable.
91    #[error(transparent)]
92    LeaveCantProcessed(#[from] GroupLeaveValidationError),
93    /// Storage error.
94    ///
95    /// Database operation failed. May be retryable.
96    #[error("storage error: {0}")]
97    Storage(#[from] xmtp_db::StorageError),
98    /// Intent error.
99    ///
100    /// Failed to process group intent. Not retryable.
101    #[error("intent error: {0}")]
102    Intent(#[from] IntentError),
103    /// Create message error.
104    ///
105    /// MLS message creation failed. Not retryable.
106    #[error("create message: {0}")]
107    CreateMessage(#[from] openmls::prelude::CreateMessageError),
108    /// TLS codec error.
109    ///
110    /// MLS TLS encoding/decoding failed. Not retryable.
111    #[error("TLS Codec error: {0}")]
112    TlsError(#[from] TlsCodecError),
113    /// Group create error.
114    ///
115    /// MLS group creation failed. May be retryable.
116    #[error("group create: {0}")]
117    GroupCreate(#[from] openmls::group::NewGroupError<sql_key_store::SqlKeyStoreError>),
118    /// Self update error.
119    ///
120    /// MLS self-update operation failed. May be retryable.
121    #[error("self update: {0}")]
122    SelfUpdate(#[from] openmls::group::SelfUpdateError<sql_key_store::SqlKeyStoreError>),
123    /// Welcome error.
124    ///
125    /// Processing MLS welcome message failed. May be retryable.
126    #[error("welcome error: {0}")]
127    WelcomeError(#[from] openmls::prelude::WelcomeError<sql_key_store::SqlKeyStoreError>),
128    /// Invalid extension.
129    ///
130    /// MLS extension validation failed. Not retryable.
131    #[error("Invalid extension {0}")]
132    InvalidExtension(#[from] openmls::prelude::InvalidExtensionError),
133    /// Invalid signature.
134    ///
135    /// MLS signature verification failed. Not retryable.
136    #[error("Invalid signature: {0}")]
137    Signature(#[from] openmls::prelude::SignatureError),
138    /// Identity update error.
139    ///
140    /// Reading or verifying identity updates failed. May be retryable.
141    #[error("identity update: {0}")]
142    IdentityUpdate(#[from] IdentityUpdateError),
143    /// Connection blocked.
144    ///
145    /// This database is bound to a different deployment, or the deployment
146    /// now requires a newer client. Not retryable.
147    #[error("connection blocked: {0}")]
148    Blocked(#[from] crate::server_configuration::BlockedConnection),
149    /// Receive error.
150    ///
151    /// Processing received group message failed. May be retryable.
152    #[error("receive error: {0}")]
153    ReceiveError(#[from] GroupMessageProcessingError),
154    /// Address validation error.
155    ///
156    /// An address/identifier is invalid. Not retryable.
157    #[error(transparent)]
158    AddressValidation(#[from] IdentifierValidationError),
159    /// Invalid public keys.
160    ///
161    /// Keys are not valid Ed25519 public keys. Not retryable.
162    #[error("Public Keys {0:?} are not valid ed25519 public keys")]
163    InvalidPublicKeys(Vec<Vec<u8>>),
164    /// Commit validation error.
165    ///
166    /// MLS commit validation failed. May be retryable.
167    #[error("Commit validation error {0}")]
168    CommitValidation(#[from] CommitValidationError),
169    /// Identity error.
170    ///
171    /// Identity operation failed. Not retryable.
172    #[error("identity error: {0}")]
173    Identity(#[from] IdentityError),
174    /// Conversion error.
175    ///
176    /// Proto conversion failed. Not retryable.
177    #[error("conversion error: {0}")]
178    ConversionError(#[from] xmtp_proto::ConversionError),
179    /// Crypto error.
180    ///
181    /// Cryptographic operation failed. Not retryable.
182    #[error("crypto error: {0}")]
183    CryptoError(#[from] openmls::prelude::CryptoError),
184    /// Group context proposal error.
185    ///
186    /// Failed to create group context extension proposal. May be retryable.
187    #[error("create group context proposal error: {0}")]
188    CreateGroupContextExtProposalError(
189        #[from] CreateGroupContextExtProposalError<sql_key_store::SqlKeyStoreError>,
190    ),
191    /// Propose add member error.
192    ///
193    /// Failed to create an add-member proposal. May be retryable.
194    #[error("propose add member error: {0}")]
195    ProposeAddMember(#[from] ProposeAddMemberError<sql_key_store::SqlKeyStoreError>),
196    /// Propose remove member error.
197    ///
198    /// Failed to create a remove-member proposal. May be retryable.
199    #[error("propose remove member error: {0}")]
200    ProposeRemoveMember(#[from] ProposeRemoveMemberError<sql_key_store::SqlKeyStoreError>),
201    /// Proposal error.
202    ///
203    /// Generic MLS proposal creation/handling failure. May be retryable.
204    #[error("proposal error: {0}")]
205    Proposal(#[from] ProposalError<sql_key_store::SqlKeyStoreError>),
206    /// Commit to pending proposals error.
207    ///
208    /// Failed to commit pending proposals into an MLS commit. May be retryable.
209    #[error("commit to pending proposals error: {0}")]
210    CommitToPendingProposals(
211        #[from] CommitToPendingProposalsError<sql_key_store::SqlKeyStoreError>,
212    ),
213    /// Merge pending commit error.
214    ///
215    /// Failed to merge a pending commit into local state. May be retryable.
216    #[error("merge pending commit error: {0}")]
217    MergePendingCommit(
218        #[from] openmls::group::MergePendingCommitError<sql_key_store::SqlKeyStoreError>,
219    ),
220    /// Proposals not supported.
221    ///
222    /// Encountered a proposal when our client does not support proposals. Not retryable.
223    #[error("Proposals not supported: {0}")]
224    ProposalsNotSupported(String),
225    /// Caller asked to set `MIN_SUPPORTED_PROTOCOL_VERSION` to a value
226    /// the caller's own client does not satisfy. Refusing prevents the
227    /// caller from immediately pausing themselves (and every peer at or
228    /// below their version) the moment the bump lands. Not retryable.
229    #[error("min_version {requested} exceeds own pkg_version {own}")]
230    MinVersionExceedsOwnVersion { requested: String, own: String },
231    /// Caller asked to lower `MIN_SUPPORTED_PROTOCOL_VERSION` below the
232    /// floor already on the group. Monotonic-only: a downgrade would
233    /// silently unpause peers between the old and new floors, defeating
234    /// the gate. Not retryable.
235    #[error("min_version {requested} would downgrade existing floor {current}")]
236    MinVersionDowngrade { requested: String, current: String },
237    /// Caller passed a `min_version` string that does not parse as
238    /// semver. The `update_group_min_version` path returns this error
239    /// so SDK consumers can handle malformed input by code. Not retryable.
240    #[error("invalid min_version {value:?}: {reason}")]
241    InvalidMinVersion { value: String, reason: String },
242    /// Component source error.
243    ///
244    /// Failed to encode, decode, or look up a well-known component during the
245    /// AppDataUpdate path. Not retryable.
246    #[error("component source error: {0}")]
247    ComponentSource(#[from] xmtp_mls_group_state::app_data::component_source::ComponentSourceError),
248    /// A metadata field read or write is invalid for the group's committed
249    /// fields: an unknown field, a wrong type, or a write the committed
250    /// state rejects. Not retryable.
251    #[error("metadata field error: {0}")]
252    MetadataField(#[from] xmtp_mls_group_state::app_data::fields::FieldError),
253    /// AppData commit error.
254    ///
255    /// Failed to build or stage a commit that bundles an inline AppDataUpdate
256    /// proposal. Wraps the structured `GroupAppDataError` from
257    /// `stage_app_data_propose_and_commit` so the underlying OpenMLS create/stage
258    /// failure is preserved instead of being string-flattened.
259    #[error("app data commit error: {0}")]
260    AppDataCommit(#[from] super::app_data::GroupAppDataError<sql_key_store::SqlKeyStoreError>),
261    /// App data value encoding failure: the sender could not encode a
262    /// component value for the dictionary.
263    ///
264    /// Not retryable: encoding is deterministic over its inputs.
265    #[error("bootstrap synthesis error: {0}")]
266    BootstrapSynthesis(#[from] super::app_data::migration::BootstrapSynthesisError),
267    /// Credential error.
268    ///
269    /// MLS credential validation failed. Not retryable.
270    #[error("Credential error")]
271    CredentialError(#[from] BasicCredentialError),
272    /// Leaf node error.
273    ///
274    /// MLS leaf node operation failed. Not retryable.
275    #[error("LeafNode error")]
276    LeafNodeError(#[from] LibraryError),
277    /// Installation diff error.
278    ///
279    /// Installation diff computation failed. May be retryable.
280    #[error("Installation diff error: {0}")]
281    InstallationDiff(#[from] InstallationDiffError),
282    /// No PSK support.
283    ///
284    /// Pre-shared keys are not supported. Not retryable.
285    #[error("PSKs are not support")]
286    NoPSKSupport,
287    /// SQL key store error.
288    ///
289    /// OpenMLS key store operation failed. May be retryable.
290    #[error("sql key store error: {0}")]
291    SqlKeyStore(#[from] sql_key_store::SqlKeyStoreError),
292    /// Sync failed to wait.
293    ///
294    /// Waiting for intent sync failed. Retryable.
295    #[error("Sync failed to wait for intent: {}", _0)]
296    SyncFailedToWait(#[source] Box<SyncSummary>),
297    /// Durable processing did not meet the fixed network targets. May be retryable.
298    #[error(transparent)]
299    #[error_code(inherit)]
300    StreamBarrier(#[from] crate::subscriptions::barrier::BarrierError),
301    /// The exact published attempt remains pending. A later call can confirm it. Retryable.
302    #[error("Intent {intent_id} was published but processing is not confirmed")]
303    PublishedButUnconfirmed {
304        intent_id: i32,
305        #[source]
306        cause: Option<Box<crate::subscriptions::barrier::BarrierError>>,
307        /// The delay of a rate limit that refused a request of the same sync,
308        /// such as the published commit's Welcomes (RATE-041).
309        rate_limit_delay: Option<xmtp_common::time::Duration>,
310    },
311    /// Missing pending commit.
312    ///
313    /// Expected pending commit not found. Not retryable.
314    #[error("Missing pending commit")]
315    MissingPendingCommit,
316    /// Process intent error.
317    ///
318    /// Failed to process group intent. May be retryable.
319    #[error(transparent)]
320    ProcessIntent(#[from] ProcessIntentError),
321    /// Failed to load lock.
322    ///
323    /// Concurrency lock acquisition failed. Retryable.
324    #[error("Failed to load lock")]
325    LockUnavailable,
326    /// Exceeded max characters.
327    ///
328    /// Field value exceeds character limit. Not retryable.
329    #[error("Exceeded max characters for this field. Must be under: {length}")]
330    TooManyCharacters { length: usize },
331    /// A guarded metadata update was abandoned because another member changed
332    /// the field first.
333    ///
334    /// Not retryable as-is: the value was derived from state that no longer
335    /// exists. Re-derive it from `actual` and queue a new update. Distinct
336    /// from a sync failure — nothing went wrong, the write is just stale.
337    #[error(
338        "app data update was superseded; expected {expected:?} but the committed value is {actual:?}"
339    )]
340    AppDataSuperseded { expected: String, actual: String },
341    /// Group paused until update.
342    ///
343    /// Group is paused until a newer version is available. Not retryable.
344    #[error("Group is paused until version {0} is available")]
345    GroupPausedUntilUpdate(String),
346    /// Group is inactive.
347    ///
348    /// Operation on an inactive group. Not retryable.
349    #[error("Group is inactive")]
350    GroupInactive,
351    /// Sync summary.
352    ///
353    /// Sync operation completed with errors. May be retryable.
354    #[error("{}", _0.to_string())]
355    Sync(#[from] Box<SyncSummary>),
356    /// Database connection error.
357    ///
358    /// Database connection failed. Retryable.
359    #[error(transparent)]
360    Db(#[from] xmtp_db::ConnectionError),
361    /// MLS store error.
362    ///
363    /// OpenMLS key store failed. Not retryable.
364    #[error(transparent)]
365    MlsStore(#[from] MlsStoreError),
366    /// Metadata permissions error.
367    ///
368    /// Metadata permission check failed. Not retryable.
369    #[error(transparent)]
370    MetadataPermissionsError(#[from] MetadataPermissionsError),
371    /// Failed to verify installations.
372    ///
373    /// Installation verification failed. Not retryable.
374    #[error("Failed to verify all installations: failedInstallations={0}")]
375    FailedToVerifyInstallations(FailedInstallationIds),
376    /// No welcomes to send.
377    ///
378    /// No welcome messages to send to new members. Not retryable.
379    #[error("no welcomes to send")]
380    NoWelcomesToSend,
381    /// Codec error.
382    ///
383    /// Content type codec failed. Retryable.
384    #[error("Codec error: {0}")]
385    CodecError(#[from] CodecError),
386    /// Wrap welcome error.
387    ///
388    /// Failed to wrap welcome message. Not retryable.
389    #[error(transparent)]
390    WrapWelcome(#[from] WrapPayloadError),
391    /// Unwrap welcome error.
392    ///
393    /// Failed to unwrap welcome message. Not retryable.
394    #[error(transparent)]
395    UnwrapWelcome(#[from] UnwrapPayloadError),
396    /// Welcome data not found.
397    ///
398    /// Welcome data missing from topic. Not retryable.
399    #[error("Failed to retrieve welcome data from topic {0}")]
400    WelcomeDataNotFound(String),
401    /// The old group must process its ordered prefix before this Welcome can install. Retryable.
402    #[error("Welcome requires group {group_id} to process through {anchor}")]
403    WelcomeGroupPrefixPending { group_id: GroupId, anchor: u64 },
404    /// The Welcome has missing, malformed, or out-of-range join metadata. Not retryable.
405    #[error("Welcome join metadata is invalid")]
406    InvalidWelcomeMetadata,
407    /// The Welcome's group context has no 32-byte group root, or its root
408    /// does not derive the Welcome's group id. Not retryable.
409    #[error("Welcome group root does not name its group: {0}")]
410    WelcomeGroupRoot(xmtp_mls_group_state::app_data::group_root::GroupRootError),
411    /// The Welcome needs a newer client. Keep it blocked until upgrade. Retryable.
412    #[error("Welcome requires client version {0}")]
413    UnsupportedWelcomeVersion(String),
414    /// Result not initialized.
415    ///
416    /// Expected result was not initialized. Not retryable.
417    #[error("Result was not initialized")]
418    UninitializedResult,
419    /// Diesel ORM error.
420    ///
421    /// Raw database query failed. May be retryable.
422    #[error(transparent)]
423    Diesel(#[from] xmtp_db::diesel::result::Error),
424    /// Uninitialized field.
425    ///
426    /// Builder field not initialized. Not retryable.
427    #[error(transparent)]
428    UninitializedField(#[from] derive_builder::UninitializedFieldError),
429    /// Delete message error.
430    ///
431    /// Failed to delete message. Retryable only while the message is being
432    /// published.
433    #[error(transparent)]
434    DeleteMessage(#[from] DeleteMessageError),
435}
436
437impl From<xmtp_mls_validation::commit::CommitRuleError> for GroupError {
438    fn from(error: xmtp_mls_validation::commit::CommitRuleError) -> Self {
439        Self::CommitValidation(error.into())
440    }
441}
442
443#[derive(Error, Debug)]
444pub enum DeleteMessageError {
445    #[error("Message not found: {0}")]
446    MessageNotFound(String),
447    #[error("Not authorized to delete this message")]
448    NotAuthorized,
449    #[error("Cannot delete this message type")]
450    NonDeletableMessage,
451    #[error("Message already deleted")]
452    MessageAlreadyDeleted,
453    /// An attempt to send the message is saved and may already be on the
454    /// backend. A retry after its echo deletes it for everyone.
455    #[error("Message is being published")]
456    MessageInFlight,
457}
458
459impl RetryableError for DeleteMessageError {
460    fn is_retryable(&self) -> bool {
461        matches!(self, Self::MessageInFlight)
462    }
463}
464
465impl From<prost::EncodeError> for GroupError {
466    fn from(value: prost::EncodeError) -> Self {
467        GroupError::ConversionError(value.into())
468    }
469}
470
471impl From<prost::DecodeError> for GroupError {
472    fn from(value: prost::DecodeError) -> Self {
473        GroupError::ConversionError(value.into())
474    }
475}
476
477impl From<SyncSummary> for GroupError {
478    fn from(value: SyncSummary) -> Self {
479        GroupError::Sync(Box::new(value))
480    }
481}
482
483#[derive(Error, Debug)]
484pub enum MetadataPermissionsError {
485    #[error(transparent)]
486    Permissions(#[from] GroupMutablePermissionsError),
487    #[error(transparent)]
488    Mutable(#[from] GroupMutableMetadataError),
489    #[error("Metadata error {0}")]
490    GroupMetadata(#[from] GroupMetadataError),
491    #[error("Invalid permission update")]
492    InvalidPermissionUpdate,
493    #[error("cannot change metadata of DM")]
494    DmGroupMetadataForbidden,
495    #[error(transparent)]
496    DmValidation(#[from] DmValidationError),
497    #[error("Invalid extension: {0}")]
498    InvalidExtension(#[from] openmls::prelude::InvalidExtensionError),
499    /// Failed to decode a well-known component value from the
500    /// AppData dictionary on a migrated group. Surfaces
501    /// [`xmtp_mls_group_state::app_data::component_source::ComponentSourceError`] via `#[from]` so callers (e.g.
502    /// `mutable_metadata()`, `metadata()`) preserve the structured
503    /// source.
504    #[error(transparent)]
505    ComponentSource(#[from] xmtp_mls_group_state::app_data::component_source::ComponentSourceError),
506}
507
508impl RetryableError for MetadataPermissionsError {
509    fn is_retryable(&self) -> bool {
510        false
511    }
512}
513
514#[derive(Error, Debug)]
515pub enum GroupLeaveValidationError {
516    #[error("cannot leave a DM conversation")]
517    DmLeaveForbidden,
518    #[error("cannot leave a group that has only one member")]
519    SingleMemberLeaveRejected,
520    #[error("super-admin cannot leave a group; must be demoted first")]
521    SuperAdminLeaveForbidden,
522    #[error("inbox ID already exists in the pending leave list")]
523    InboxAlreadyInPendingList,
524    #[error("inbox ID does not exist in the pending leave list")]
525    InboxNotInPendingList,
526    #[error("only a member of the group can send a leave request or retract a leave request")]
527    NotAGroupMember,
528}
529
530impl RetryableError for GroupLeaveValidationError {
531    fn is_retryable(&self) -> bool {
532        false
533    }
534}
535
536#[derive(Error, Debug)]
537pub enum DmValidationError {
538    #[error("DM group must include our inbox in its member pair")]
539    OurInboxMustBeMember,
540    #[error("DM group must have DM_MEMBERS set")]
541    MustHaveMembersSet,
542    #[error("Invalid conversation type for DM group")]
543    InvalidConversationType,
544    #[error("DM members do not match expected inboxes")]
545    ExpectedInboxesDoNotMatch,
546    #[error("DM group must have empty admin and super admin lists")]
547    MustHaveEmptyAdminAndSuperAdmin,
548    #[error("Invalid permissions for DM group")]
549    InvalidPermissions,
550    #[error("DM group membership is invalid")]
551    InvalidMembership,
552    #[error("DM group member credential is invalid")]
553    InvalidMemberCredential,
554    #[error("DM group includes an inbox outside its pair")]
555    MemberOutsidePair,
556    #[error("stored DM identifier does not match its validated pair")]
557    StoredDmIdMismatch,
558}
559
560impl RetryableError for DmValidationError {
561    fn is_retryable(&self) -> bool {
562        match self {
563            Self::OurInboxMustBeMember
564            | Self::MustHaveMembersSet
565            | Self::InvalidConversationType
566            | Self::ExpectedInboxesDoNotMatch
567            | Self::MustHaveEmptyAdminAndSuperAdmin
568            | Self::InvalidPermissions
569            | Self::InvalidMembership
570            | Self::InvalidMemberCredential
571            | Self::MemberOutsidePair
572            | Self::StoredDmIdMismatch => false,
573        }
574    }
575}
576
577impl RetryableError for GroupError {
578    fn is_retryable(&self) -> bool {
579        match self {
580            Self::MessageSenderConflict => false,
581            Self::OutgoingPreparation(error) => error.is_retryable(),
582            Self::IdentityUpdate(error) => error.is_retryable(),
583            Self::Storage(storage) => storage.is_retryable(),
584            Self::ReceiveError(msg) => msg.is_retryable(),
585            Self::Identity(identity) => identity.is_retryable(),
586            Self::GroupCreate(group) => group.is_retryable(),
587            Self::SelfUpdate(update) => update.is_retryable(),
588            Self::WelcomeError(
589                openmls::prelude::WelcomeError::UnsupportedMlsVersion
590                | openmls::prelude::WelcomeError::UnsupportedExtensions,
591            ) => true,
592            Self::WelcomeError(welcome) => welcome.is_retryable(),
593            Self::SqlKeyStore(sql) => sql.is_retryable(),
594            Self::InstallationDiff(diff) => diff.is_retryable(),
595            Self::CreateGroupContextExtProposalError(create) => create.is_retryable(),
596            Self::ProposeAddMember(e) => e.is_retryable(),
597            Self::ProposeRemoveMember(e) => e.is_retryable(),
598            Self::Proposal(e) => e.is_retryable(),
599            Self::CommitToPendingProposals(e) => e.is_retryable(),
600            Self::ProposalsNotSupported(_) => false,
601            Self::MinVersionExceedsOwnVersion { .. } => false,
602            Self::MinVersionDowngrade { .. } => false,
603            Self::InvalidMinVersion { .. } => false,
604            Self::ComponentSource(_) => false,
605            Self::MetadataField(_) => false,
606            Self::AppDataCommit(e) => e.is_retryable(),
607            Self::BootstrapSynthesis(e) => e.is_retryable(),
608            Self::CommitValidation(err) => err.is_retryable(),
609            Self::WrappedApi(err) => err.is_retryable(),
610            Self::ProcessIntent(err) => err.is_retryable(),
611            Self::LockUnavailable => true,
612            Self::WelcomeGroupPrefixPending { .. } | Self::UnsupportedWelcomeVersion(_) => true,
613            Self::SyncFailedToWait(_) => true,
614            Self::StreamBarrier(error) => error.is_retryable(),
615            Self::PublishedButUnconfirmed { .. } => true,
616            Self::CodecError(_) => true,
617            Self::Sync(s) => s.is_retryable(),
618            Self::Db(e) => e.is_retryable(),
619            Self::MlsStore(e) => e.is_retryable(),
620            Self::MetadataPermissionsError(e) => e.is_retryable(),
621            Self::WrapWelcome(e) => e.is_retryable(),
622            Self::UnwrapWelcome(e) => e.is_retryable(),
623            Self::Diesel(e) => e.is_retryable(),
624            Self::LeaveCantProcessed(e) => e.is_retryable(),
625            Self::DeleteMessage(e) => e.is_retryable(),
626            Self::MergePendingCommit(e) => e.is_retryable(),
627            // Only emitted when a fresh `load_identity_updates` network
628            // refresh still has no sequence id for an inbox — i.e. its
629            // registration hasn't propagated to reads yet. Retrying re-runs
630            // the fetch, so the miss is transient; classifying it
631            // non-retryable permanently failed sync-group membership adds
632            // that raced a new installation's identity propagation.
633            Self::MissingSequenceId => true,
634            Self::NotFound(_)
635            | Self::Blocked(_)
636            | Self::ConsentAct(_)
637            | Self::InvalidWelcomeMetadata
638            | Self::WelcomeGroupRoot(_)
639            | Self::UserLimitExceeded
640            | Self::InvalidGroupMembership
641            | Self::Intent(_)
642            | Self::CreateMessage(_)
643            | Self::TlsError(_)
644            | Self::AddressNotFound(_)
645            | Self::InvalidExtension(_)
646            | Self::Signature(_)
647            | Self::LeafNodeError(_)
648            | Self::NoPSKSupport
649            | Self::MissingPendingCommit
650            | Self::AddressValidation(_)
651            | Self::InvalidPublicKeys(_)
652            | Self::CredentialError(_)
653            | Self::ConversionError(_)
654            | Self::CryptoError(_)
655            | Self::TooManyCharacters { .. }
656            | Self::AppDataSuperseded { .. }
657            | Self::GroupPausedUntilUpdate(_)
658            | Self::GroupInactive
659            | Self::ReservedTranscriptContentType
660            | Self::FailedToVerifyInstallations(_)
661            | Self::NoWelcomesToSend
662            | Self::WelcomeDataNotFound(_)
663            | Self::UninitializedField(_)
664            | Self::UninitializedResult => false,
665        }
666    }
667
668    fn rate_limit_delay(&self) -> Option<xmtp_common::time::Duration> {
669        match self {
670            Self::IdentityUpdate(error) => error.rate_limit_delay(),
671            Self::ReceiveError(msg) => msg.rate_limit_delay(),
672            Self::Identity(identity) => identity.rate_limit_delay(),
673            Self::InstallationDiff(diff) => diff.rate_limit_delay(),
674            Self::CommitValidation(err) => err.rate_limit_delay(),
675            Self::WrappedApi(err) => err.rate_limit_delay(),
676            Self::StreamBarrier(error) => error.rate_limit_delay(),
677            Self::SyncFailedToWait(summary) | Self::Sync(summary) => summary.rate_limit_delay(),
678            Self::PublishedButUnconfirmed {
679                cause,
680                rate_limit_delay,
681                ..
682            } => (*rate_limit_delay).max(cause.as_ref().and_then(|cause| cause.rate_limit_delay())),
683            Self::MlsStore(e) => e.rate_limit_delay(),
684            _ => None,
685        }
686    }
687}
688
689impl crate::worker::NeedsDbReconnect for GroupError {
690    /// Forwards a dropped-pool signal from storage-bearing variants so a worker
691    /// catching `GroupError`s per item can stop on disconnect; else `false`.
692    #[deny(clippy::wildcard_enum_match_arm)]
693    fn needs_db_reconnect(&self) -> bool {
694        match self {
695            Self::Storage(s) => s.db_needs_connection(),
696            Self::IdentityUpdate(error) => error.needs_db_reconnect(),
697            Self::Db(c) => c.db_needs_connection(),
698            Self::MlsStore(s) => s.needs_db_reconnect(),
699            Self::Identity(i) => i.needs_db_reconnect(),
700            Self::StreamBarrier(error) => error.needs_db_reconnect(),
701            Self::PublishedButUnconfirmed { cause, .. } => cause
702                .as_ref()
703                .is_some_and(|error| error.needs_db_reconnect()),
704            Self::ReceiveError(error) => error.needs_db_reconnect(),
705            Self::Sync(summary) | Self::SyncFailedToWait(summary) => summary.needs_db_reconnect(),
706            Self::CommitValidation(error) => error.needs_db_reconnect(),
707            Self::InstallationDiff(error) => error.needs_db_reconnect(),
708            Self::ProcessIntent(ProcessIntentError::Storage(error)) => error.db_needs_connection(),
709            Self::SqlKeyStore(sql_key_store::SqlKeyStoreError::Connection(error)) => {
710                error.db_needs_connection()
711            }
712            Self::ProcessIntent(_)
713            | Self::SqlKeyStore(_)
714            | Self::ReservedTranscriptContentType
715            | Self::MessageSenderConflict
716            | Self::OutgoingPreparation(..)
717            | Self::NotFound(..)
718            | Self::UserLimitExceeded
719            | Self::MissingSequenceId
720            | Self::AddressNotFound(..)
721            | Self::WrappedApi(..)
722            | Self::InvalidGroupMembership
723            | Self::ConsentAct(..)
724            | Self::LeaveCantProcessed(..)
725            | Self::Intent(..)
726            | Self::CreateMessage(..)
727            | Self::TlsError(..)
728            | Self::GroupCreate(..)
729            | Self::SelfUpdate(..)
730            | Self::WelcomeError(..)
731            | Self::InvalidExtension(..)
732            | Self::Signature(..)
733            | Self::Blocked(..)
734            | Self::AddressValidation(..)
735            | Self::InvalidPublicKeys(..)
736            | Self::ConversionError(..)
737            | Self::CryptoError(..)
738            | Self::CreateGroupContextExtProposalError(..)
739            | Self::ProposeAddMember(..)
740            | Self::ProposeRemoveMember(..)
741            | Self::Proposal(..)
742            | Self::CommitToPendingProposals(..)
743            | Self::MergePendingCommit(..)
744            | Self::ProposalsNotSupported(..)
745            | Self::MinVersionExceedsOwnVersion { .. }
746            | Self::MinVersionDowngrade { .. }
747            | Self::InvalidMinVersion { .. }
748            | Self::ComponentSource(..)
749            | Self::MetadataField(..)
750            | Self::AppDataCommit(..)
751            | Self::BootstrapSynthesis(..)
752            | Self::CredentialError(..)
753            | Self::LeafNodeError(..)
754            | Self::NoPSKSupport
755            | Self::MissingPendingCommit
756            | Self::LockUnavailable
757            | Self::TooManyCharacters { .. }
758            | Self::AppDataSuperseded { .. }
759            | Self::GroupPausedUntilUpdate(..)
760            | Self::GroupInactive
761            | Self::MetadataPermissionsError(..)
762            | Self::FailedToVerifyInstallations(..)
763            | Self::NoWelcomesToSend
764            | Self::CodecError(..)
765            | Self::WrapWelcome(..)
766            | Self::UnwrapWelcome(..)
767            | Self::WelcomeDataNotFound(..)
768            | Self::WelcomeGroupPrefixPending { .. }
769            | Self::InvalidWelcomeMetadata
770            | Self::WelcomeGroupRoot(..)
771            | Self::UnsupportedWelcomeVersion(..)
772            | Self::UninitializedResult
773            | Self::Diesel(..)
774            | Self::UninitializedField(..)
775            | Self::DeleteMessage(..) => false,
776        }
777    }
778}
779
780#[cfg(test)]
781mod tests {
782    use super::*;
783
784    #[xmtp_common::test]
785    fn missing_sequence_id_is_retryable() {
786        // Regression lock: while non-retryable, a membership add racing a
787        // new installation's identity propagation burned all publish
788        // attempts instantly and permanently failed the sync-group add
789        // (surfaced by device-sync CI failures).
790        assert!(GroupError::MissingSequenceId.is_retryable());
791    }
792
793    #[xmtp_common::test]
794    fn failed_to_verify_installations_includes_hex_ids() {
795        let err = GroupError::FailedToVerifyInstallations(FailedInstallationIds(vec![
796            vec![0xAB; 32],
797            vec![0xCD; 32],
798        ]));
799        let message = err.to_string();
800        assert!(message.contains("failedInstallations="));
801        assert!(message.contains(&hex::encode([0xAB; 32])));
802        assert!(message.contains(&hex::encode([0xCD; 32])));
803        assert!(!err.is_retryable());
804    }
805}