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