Skip to content

Commit 1ad7322

Browse files
authored
fix #154 (and fix #159 because that's a duplicate) (#165)
1 parent 7c3157c commit 1ad7322

File tree

6 files changed

+148
-72
lines changed

6 files changed

+148
-72
lines changed

crates/mdk-core/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
### Added
2727

28+
- **`PreviouslyFailed` Result Variant**: Added `MessageProcessingResult::PreviouslyFailed` variant to handle cases where a previously failed message arrives again but the MLS group ID cannot be extracted. This prevents crashes in client applications by returning a result instead of throwing an error. ([#165](https://github.com/marmot-protocol/mdk/pull/165), fixes [#154](https://github.com/marmot-protocol/mdk/issues/154), [#159](https://github.com/marmot-protocol/mdk/issues/159))
2829
- **Message Retry Support**: Implemented better handling for retryable message states. When a message fails processing, it now preserves the `message_event_id` and other context. Added logic to allow reprocessing of messages marked as `Retryable`, with automatic state recovery to `Processed` upon success. ([#161](https://github.com/marmot-protocol/mdk/pull/161))
2930
- Configurable `out_of_order_tolerance` and `maximum_forward_distance` in `MdkConfig` for MLS sender ratchet settings. Default `out_of_order_tolerance` increased from 5 to 100 for better handling of out-of-order message delivery on Nostr relays. ([`#155`](https://github.com/marmot-protocol/mdk/pull/155))
3031

crates/mdk-core/src/messages/error_handling.rs

Lines changed: 119 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ mod tests {
504504
/// of previously failed events, mitigating DoS attacks.
505505
///
506506
/// When a previously failed message cannot provide a valid group_id (missing or
507-
/// malformed h-tag), we return an error to be explicit about the failure.
507+
/// malformed h-tag), we return PreviouslyFailed to avoid crashing client apps.
508508
#[test]
509509
fn test_repeated_validation_failure_rejected_immediately() {
510510
let mdk = create_test_mdk();
@@ -520,18 +520,15 @@ mod tests {
520520
assert!(result1.is_err(), "First attempt should fail validation");
521521

522522
// Second attempt - should be rejected immediately via deduplication
523-
// Returns error because group_id cannot be extracted from malformed event
523+
// Returns PreviouslyFailed because group_id cannot be extracted from malformed event
524524
let result2 = mdk.process_message(&event);
525525
assert!(
526-
result2.is_err(),
527-
"Second attempt should return error for malformed event without valid h-tag"
526+
result2.is_ok(),
527+
"Second attempt should return Ok(PreviouslyFailed), not error"
528528
);
529529
assert!(
530-
result2
531-
.unwrap_err()
532-
.to_string()
533-
.contains("Message processing previously failed"),
534-
"Should indicate message previously failed"
530+
matches!(result2.unwrap(), MessageProcessingResult::PreviouslyFailed),
531+
"Should return PreviouslyFailed variant"
535532
);
536533
}
537534

@@ -584,7 +581,7 @@ mod tests {
584581
///
585582
/// This test verifies the deduplication mechanism works for decryption failures,
586583
/// preventing expensive repeated decryption attempts. When the group doesn't exist
587-
/// in storage, we return an error since we can't determine the MLS group ID.
584+
/// in storage, we return PreviouslyFailed to avoid crashing client apps.
588585
#[test]
589586
fn test_repeated_decryption_failure_rejected_immediately() {
590587
let mdk = create_test_mdk();
@@ -602,26 +599,23 @@ mod tests {
602599
let result1 = mdk.process_message(&event);
603600
assert!(result1.is_err(), "First attempt should fail decryption");
604601

605-
// Second attempt - should return error because group isn't in storage
602+
// Second attempt - should return PreviouslyFailed because group isn't in storage
606603
// (we can't determine the MLS group ID from just the Nostr group ID)
607604
let result2 = mdk.process_message(&event);
608605
assert!(
609-
result2.is_err(),
610-
"Second attempt should return error when group not in storage"
606+
result2.is_ok(),
607+
"Second attempt should return Ok(PreviouslyFailed), not error"
611608
);
612609
assert!(
613-
result2
614-
.unwrap_err()
615-
.to_string()
616-
.contains("Message processing previously failed"),
617-
"Should indicate message previously failed"
610+
matches!(result2.unwrap(), MessageProcessingResult::PreviouslyFailed),
611+
"Should return PreviouslyFailed variant"
618612
);
619613
}
620614

621-
/// Test that previously failed message without group in storage returns error
615+
/// Test that previously failed message without group in storage returns PreviouslyFailed
622616
///
623617
/// This test verifies that when a previously failed message has a valid h-tag
624-
/// but the group doesn't exist in storage, we return an error since we can't
618+
/// but the group doesn't exist in storage, we return PreviouslyFailed since we can't
625619
/// determine the MLS group ID (Nostr group ID != MLS group ID).
626620
#[test]
627621
fn test_previously_failed_message_without_group_in_storage() {
@@ -655,26 +649,23 @@ mod tests {
655649
"State should be Failed"
656650
);
657651

658-
// Second attempt - should return error because we can't determine MLS group ID
652+
// Second attempt - should return PreviouslyFailed because we can't determine MLS group ID
659653
let result2 = mdk.process_message(&event);
660654
assert!(
661-
result2.is_err(),
662-
"Second attempt should return error when group not in storage"
655+
result2.is_ok(),
656+
"Second attempt should return Ok(PreviouslyFailed), not error"
663657
);
664658
assert!(
665-
result2
666-
.unwrap_err()
667-
.to_string()
668-
.contains("Message processing previously failed"),
669-
"Should indicate message previously failed"
659+
matches!(result2.unwrap(), MessageProcessingResult::PreviouslyFailed),
660+
"Should return PreviouslyFailed variant"
670661
);
671662
}
672663

673-
/// Test that previously failed message with oversized hex in h-tag returns error
664+
/// Test that previously failed message with oversized hex in h-tag returns PreviouslyFailed
674665
///
675666
/// This test verifies that when a previously failed message has an oversized hex string
676667
/// in the h-tag (potential DoS vector), the size check prevents decoding and returns
677-
/// an explicit error.
668+
/// PreviouslyFailed to avoid crashing client apps.
678669
#[test]
679670
fn test_previously_failed_message_with_oversized_hex() {
680671
let mdk = create_test_mdk();
@@ -703,25 +694,23 @@ mod tests {
703694
message_types::ProcessedMessageState::Failed
704695
);
705696

706-
// Second attempt - should return error due to malformed h-tag
697+
// Second attempt - should return PreviouslyFailed due to malformed h-tag
707698
let result2 = mdk.process_message(&event);
708699
assert!(
709-
result2.is_err(),
710-
"Second attempt should return error for oversized hex"
700+
result2.is_ok(),
701+
"Second attempt should return Ok(PreviouslyFailed), not error"
711702
);
712703
assert!(
713-
result2
714-
.unwrap_err()
715-
.to_string()
716-
.contains("Message processing previously failed"),
717-
"Should indicate message previously failed"
704+
matches!(result2.unwrap(), MessageProcessingResult::PreviouslyFailed),
705+
"Should return PreviouslyFailed variant"
718706
);
719707
}
720708

721-
/// Test that previously failed message with undersized hex in h-tag returns error
709+
/// Test that previously failed message with undersized hex in h-tag returns PreviouslyFailed
722710
///
723711
/// This test verifies that when a previously failed message has an undersized hex string
724-
/// in the h-tag, the size check prevents decoding and returns an explicit error.
712+
/// in the h-tag, the size check prevents decoding and returns PreviouslyFailed to avoid
713+
/// crashing client apps.
725714
#[test]
726715
fn test_previously_failed_message_with_undersized_hex() {
727716
let mdk = create_test_mdk();
@@ -750,18 +739,15 @@ mod tests {
750739
message_types::ProcessedMessageState::Failed
751740
);
752741

753-
// Second attempt - should return error due to malformed h-tag
742+
// Second attempt - should return PreviouslyFailed due to malformed h-tag
754743
let result2 = mdk.process_message(&event);
755744
assert!(
756-
result2.is_err(),
757-
"Second attempt should return error for undersized hex"
745+
result2.is_ok(),
746+
"Second attempt should return Ok(PreviouslyFailed), not error"
758747
);
759748
assert!(
760-
result2
761-
.unwrap_err()
762-
.to_string()
763-
.contains("Message processing previously failed"),
764-
"Should indicate message previously failed"
749+
matches!(result2.unwrap(), MessageProcessingResult::PreviouslyFailed),
750+
"Should return PreviouslyFailed variant"
765751
);
766752
}
767753

@@ -826,10 +812,10 @@ mod tests {
826812
}
827813
}
828814

829-
/// Test that previously failed message with invalid hex characters returns error
815+
/// Test that previously failed message with invalid hex characters returns PreviouslyFailed
830816
///
831817
/// This test verifies that when hex::decode fails due to invalid characters,
832-
/// the code returns an explicit error.
818+
/// the code returns PreviouslyFailed to avoid crashing client apps.
833819
#[test]
834820
fn test_previously_failed_message_with_invalid_hex_chars() {
835821
let mdk = create_test_mdk();
@@ -864,18 +850,15 @@ mod tests {
864850
message_types::ProcessedMessageState::Failed
865851
);
866852

867-
// Second attempt - should return error due to invalid hex
853+
// Second attempt - should return PreviouslyFailed due to invalid hex
868854
let result2 = mdk.process_message(&event);
869855
assert!(
870-
result2.is_err(),
871-
"Second attempt should return error for invalid hex chars"
856+
result2.is_ok(),
857+
"Second attempt should return Ok(PreviouslyFailed), not error"
872858
);
873859
assert!(
874-
result2
875-
.unwrap_err()
876-
.to_string()
877-
.contains("Message processing previously failed"),
878-
"Should indicate message previously failed"
860+
matches!(result2.unwrap(), MessageProcessingResult::PreviouslyFailed),
861+
"Should return PreviouslyFailed variant"
879862
);
880863
}
881864

@@ -1007,6 +990,84 @@ mod tests {
1007990
}
1008991
}
1009992

993+
/// Test that sanitize_error_reason covers all explicitly mapped error variants
994+
/// and falls back to "processing_failed" for unmapped variants
995+
#[test]
996+
fn test_sanitize_error_reason_all_variants() {
997+
use crate::MDK;
998+
use mdk_memory_storage::MdkMemoryStorage;
999+
use nostr::Kind;
1000+
1001+
// Test explicitly mapped error variants
1002+
assert_eq!(
1003+
MDK::<MdkMemoryStorage>::sanitize_error_reason(&Error::UnexpectedEvent {
1004+
expected: Kind::MlsGroupMessage,
1005+
received: Kind::TextNote,
1006+
}),
1007+
"invalid_event_type"
1008+
);
1009+
1010+
assert_eq!(
1011+
MDK::<MdkMemoryStorage>::sanitize_error_reason(&Error::MissingGroupIdTag),
1012+
"invalid_event_format"
1013+
);
1014+
1015+
assert_eq!(
1016+
MDK::<MdkMemoryStorage>::sanitize_error_reason(&Error::InvalidGroupIdFormat(
1017+
"bad format".to_string()
1018+
)),
1019+
"invalid_event_format"
1020+
);
1021+
1022+
assert_eq!(
1023+
MDK::<MdkMemoryStorage>::sanitize_error_reason(&Error::MultipleGroupIdTags(3)),
1024+
"invalid_event_format"
1025+
);
1026+
1027+
assert_eq!(
1028+
MDK::<MdkMemoryStorage>::sanitize_error_reason(&Error::InvalidTimestamp(
1029+
"future timestamp".to_string()
1030+
)),
1031+
"invalid_event_format"
1032+
);
1033+
1034+
assert_eq!(
1035+
MDK::<MdkMemoryStorage>::sanitize_error_reason(&Error::GroupNotFound),
1036+
"group_not_found"
1037+
);
1038+
1039+
assert_eq!(
1040+
MDK::<MdkMemoryStorage>::sanitize_error_reason(&Error::CannotDecryptOwnMessage),
1041+
"own_message"
1042+
);
1043+
1044+
assert_eq!(
1045+
MDK::<MdkMemoryStorage>::sanitize_error_reason(&Error::AuthorMismatch),
1046+
"authentication_failed"
1047+
);
1048+
1049+
assert_eq!(
1050+
MDK::<MdkMemoryStorage>::sanitize_error_reason(&Error::CommitFromNonAdmin),
1051+
"authorization_failed"
1052+
);
1053+
1054+
// Test catch-all for unmapped variants (should return "processing_failed")
1055+
assert_eq!(
1056+
MDK::<MdkMemoryStorage>::sanitize_error_reason(&Error::MessageNotFound),
1057+
"processing_failed"
1058+
);
1059+
1060+
assert_eq!(
1061+
MDK::<MdkMemoryStorage>::sanitize_error_reason(&Error::OwnLeafNotFound),
1062+
"processing_failed"
1063+
);
1064+
1065+
assert_eq!(
1066+
MDK::<MdkMemoryStorage>::sanitize_error_reason(&Error::ProcessMessageWrongEpoch(5)),
1067+
"processing_failed"
1068+
);
1069+
}
1070+
10101071
#[test]
10111072
fn test_record_failure_preserves_message_event_id() {
10121073
let mdk = create_test_mdk();

crates/mdk-core/src/messages/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ pub enum MessageProcessingResult {
105105
/// The MLS group ID of the message that could not be processed
106106
mls_group_id: GroupId,
107107
},
108+
/// Message was previously marked as failed and cannot be reprocessed
109+
///
110+
/// This variant is returned when a message that previously failed processing
111+
/// is received again. Unlike `Unprocessable`, this does not require an MLS group ID
112+
/// because the group ID may not be extractable from malformed messages.
113+
PreviouslyFailed,
108114
}
109115

110116
impl std::fmt::Debug for MessageProcessingResult {
@@ -145,6 +151,7 @@ impl std::fmt::Debug for MessageProcessingResult {
145151
.debug_struct("Unprocessable")
146152
.field("mls_group_id", &"[REDACTED]")
147153
.finish(),
154+
Self::PreviouslyFailed => f.debug_struct("PreviouslyFailed").finish(),
148155
}
149156
}
150157
}

crates/mdk-core/src/messages/process.rs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -279,30 +279,22 @@ where
279279
processed.state == message_types::ProcessedMessageState::EpochInvalidated;
280280

281281
if is_failed || is_epoch_invalidated {
282-
let mls_group_id = match self.extract_mls_group_id_from_event(event) {
283-
Some(id) => {
282+
match self.extract_mls_group_id_from_event(event) {
283+
Some(mls_group_id) => {
284284
tracing::debug!(
285285
target: "mdk_core::messages::process_message",
286286
"Returning Unprocessable for previously failed/invalidated message with extracted group_id"
287287
);
288-
id
288+
return Ok(MessageProcessingResult::Unprocessable { mls_group_id });
289289
}
290290
None => {
291291
tracing::debug!(
292292
target: "mdk_core::messages::process_message",
293-
"Cannot extract group_id from previously failed/invalidated message (missing or malformed h-tag)"
293+
"Returning PreviouslyFailed for message without extractable group_id"
294294
);
295-
// Return the appropriate error message based on the prior state
296-
let error_msg = if is_failed {
297-
"Message processing previously failed"
298-
} else {
299-
"Message epoch was invalidated"
300-
};
301-
return Err(Error::Message(error_msg.to_string()));
295+
return Ok(MessageProcessingResult::PreviouslyFailed);
302296
}
303-
};
304-
305-
return Ok(MessageProcessingResult::Unprocessable { mls_group_id });
297+
}
306298
}
307299

308300
// Allow Retryable messages to be reprocessed after rollback
@@ -431,6 +423,8 @@ mod tests {
431423
let pending_proposal_result = MessageProcessingResult::PendingProposal {
432424
mls_group_id: test_group_id.clone(),
433425
};
426+
// PreviouslyFailed: for when a message that previously failed cannot provide a group_id
427+
let previously_failed_result = MessageProcessingResult::PreviouslyFailed;
434428

435429
// Test that we can match on variants
436430
match app_result {
@@ -457,6 +451,11 @@ mod tests {
457451
MessageProcessingResult::PendingProposal { .. } => {}
458452
_ => panic!("Expected PendingProposal variant"),
459453
}
454+
455+
match previously_failed_result {
456+
MessageProcessingResult::PreviouslyFailed => {}
457+
_ => panic!("Expected PreviouslyFailed variant"),
458+
}
460459
}
461460

462461
#[test]

crates/mdk-uniffi/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
### Added
4343

44+
- **`PreviouslyFailed` Result Variant**: Added `ProcessMessageResult.PreviouslyFailed` enum variant to handle cases where a previously failed message arrives again but the MLS group ID cannot be extracted. This prevents crashes in client applications (fixes [#153](https://github.com/marmot-protocol/mdk/issues/153)) by returning a result instead of throwing an exception. ([#165](https://github.com/marmot-protocol/mdk/pull/165), fixes [#154](https://github.com/marmot-protocol/mdk/issues/154), [#159](https://github.com/marmot-protocol/mdk/issues/159))
4445
- Added `MdkConfig` record for configuring MDK behavior, including `out_of_order_tolerance` and `maximum_forward_distance` settings for MLS sender ratchet configuration. All fields are optional and default to sensible values. ([`#155`](https://github.com/marmot-protocol/mdk/pull/155))
4546
- Exposed pagination control for `get_messages()` to foreign language bindings via optional `limit` and `offset` parameters. ([#111](https://github.com/marmot-protocol/mdk/pull/111))
4647
- Exposed pagination control for `get_pending_welcomes()` to foreign language bindings via optional `limit` and `offset` parameters. ([#119](https://github.com/marmot-protocol/mdk/pull/119))

0 commit comments

Comments
 (0)