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