From ee8d6e1390096dd518c81744df97b6d545bb0013 Mon Sep 17 00:00:00 2001 From: Abhishek Chauhan <60182103+abhu85@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:31:09 +0530 Subject: [PATCH] fix(sns): support SubscriptionConfirmation and UnsubscribeConfirmation message types The SnsMessage and SnsMessageObj structs were failing to deserialize SubscriptionConfirmation and UnsubscribeConfirmation SNS messages because: 1. `unsubscribe_url` was required (String) but these message types don't have it 2. `subscribe_url` and `token` fields were missing entirely This fix: - Makes `unsubscribe_url` optional (Option) since it's only present in Notification messages - Adds `subscribe_url` field (Option) for confirmation messages - Adds `token` field (Option) for confirmation messages All fields use #[serde(default)] to handle missing fields gracefully. Fixes #966 Signed-off-by: abhu85 <151518127+abhu85@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 --- lambda-events/src/event/sns/mod.rs | 77 ++++++++++++++++++- ...example-sns-subscription-confirmation.json | 23 ++++++ 2 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 lambda-events/src/fixtures/example-sns-subscription-confirmation.json diff --git a/lambda-events/src/event/sns/mod.rs b/lambda-events/src/event/sns/mod.rs index 3e752259..0fc2bbd6 100644 --- a/lambda-events/src/event/sns/mod.rs +++ b/lambda-events/src/event/sns/mod.rs @@ -94,8 +94,24 @@ pub struct SnsMessage { pub signing_cert_url: String, /// A URL that you can use to unsubscribe the endpoint from this topic. If you visit this URL, Amazon SNS unsubscribes the endpoint and stops sending notifications to this endpoint. + /// + /// Note: This field is only present in Notification messages. It is not present in SubscriptionConfirmation or UnsubscribeConfirmation messages. #[serde(alias = "UnsubscribeURL")] - pub unsubscribe_url: String, + #[serde(default)] + pub unsubscribe_url: Option, + + /// A URL that you can visit to re-confirm the subscription or confirm the unsubscription. + /// + /// Note: This field is only present in SubscriptionConfirmation and UnsubscribeConfirmation messages. + #[serde(alias = "SubscribeURL")] + #[serde(default)] + pub subscribe_url: Option, + + /// A value you can use with the ConfirmSubscription action to re-confirm the subscription. + /// + /// Note: This field is only present in SubscriptionConfirmation and UnsubscribeConfirmation messages. + #[serde(default)] + pub token: Option, /// The Message value specified when the notification was published to the topic. pub message: String, @@ -205,8 +221,24 @@ pub struct SnsMessageObj { pub signing_cert_url: String, /// A URL that you can use to unsubscribe the endpoint from this topic. If you visit this URL, Amazon SNS unsubscribes the endpoint and stops sending notifications to this endpoint. + /// + /// Note: This field is only present in Notification messages. It is not present in SubscriptionConfirmation or UnsubscribeConfirmation messages. #[serde(alias = "UnsubscribeURL")] - pub unsubscribe_url: String, + #[serde(default)] + pub unsubscribe_url: Option, + + /// A URL that you can visit to re-confirm the subscription or confirm the unsubscription. + /// + /// Note: This field is only present in SubscriptionConfirmation and UnsubscribeConfirmation messages. + #[serde(alias = "SubscribeURL")] + #[serde(default)] + pub subscribe_url: Option, + + /// A value you can use with the ConfirmSubscription action to re-confirm the subscription. + /// + /// Note: This field is only present in SubscriptionConfirmation and UnsubscribeConfirmation messages. + #[serde(default)] + pub token: Option, /// Deserialized into a `T` from nested JSON inside the SNS message string. `T` must implement the `Deserialize` or `DeserializeOwned` trait. #[serde_as(as = "serde_with::json::JsonString")] @@ -462,4 +494,45 @@ mod test { let reparsed: SnsEventObj = serde_json::from_slice(output.as_bytes()).unwrap(); assert_eq!(parsed, reparsed); } + + #[test] + #[cfg(feature = "sns")] + fn my_example_sns_subscription_confirmation() { + // Test for issue #966: SnsMessage struct fails with SubscriptionConfirmation types + // SubscriptionConfirmation messages have SubscribeURL and Token fields instead of UnsubscribeURL + let data = include_bytes!("../../fixtures/example-sns-subscription-confirmation.json"); + let parsed: SnsEvent = serde_json::from_slice(data).unwrap(); + assert_eq!(1, parsed.records.len()); + + let sns_message = &parsed.records[0].sns; + assert_eq!("SubscriptionConfirmation", sns_message.sns_message_type); + assert!(sns_message.unsubscribe_url.is_none()); + assert!(sns_message.subscribe_url.is_some()); + assert!(sns_message.token.is_some()); + assert_eq!( + "https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-1:123456789012:MyTopic&Token=2336412f37fb687f5d51e6e2425dacbbffff", + sns_message.subscribe_url.as_ref().unwrap() + ); + assert_eq!( + "2336412f37fb687f5d51e6e2425dacbbffff", + sns_message.token.as_ref().unwrap() + ); + + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: SnsEvent = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "sns")] + fn my_example_sns_notification_has_unsubscribe_url() { + // Verify that Notification messages still have unsubscribe_url + let data = include_bytes!("../../fixtures/example-sns-event.json"); + let parsed: SnsEvent = serde_json::from_slice(data).unwrap(); + let sns_message = &parsed.records[0].sns; + assert_eq!("Notification", sns_message.sns_message_type); + assert!(sns_message.unsubscribe_url.is_some()); + assert!(sns_message.subscribe_url.is_none()); + assert!(sns_message.token.is_none()); + } } diff --git a/lambda-events/src/fixtures/example-sns-subscription-confirmation.json b/lambda-events/src/fixtures/example-sns-subscription-confirmation.json new file mode 100644 index 00000000..7e934e98 --- /dev/null +++ b/lambda-events/src/fixtures/example-sns-subscription-confirmation.json @@ -0,0 +1,23 @@ +{ + "Records": [ + { + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:us-east-1:123456789012:MyTopic:00000000-0000-0000-0000-000000000000", + "EventSource": "aws:sns", + "Sns": { + "Type": "SubscriptionConfirmation", + "MessageId": "165545c9-2a5c-472c-8df2-7ff2be2b3b1b", + "TopicArn": "arn:aws:sns:us-east-1:123456789012:MyTopic", + "Message": "You have chosen to subscribe to the topic arn:aws:sns:us-east-1:123456789012:MyTopic.\nTo confirm the subscription, visit the SubscribeURL included in this message.", + "Timestamp": "2012-04-26T20:45:04.751Z", + "SignatureVersion": "1", + "Signature": "EXAMPLEpH+DcEwjAPg8O9mY8dReBSwksfg2S7WKQcikcNKWLQjwu6A4VbeS0QHVCkhRS7fUQvi2egU3N858fiTDN6bkkOxYDVrY0Ad8L10Hs3zH81mtnPk5uvvolIC1CXGu43obcgFxeL3khZl8IKvO61GWB6jI9b5+gLPoBc1Q=", + "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem", + "SubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-1:123456789012:MyTopic&Token=2336412f37fb687f5d51e6e2425dacbbffff", + "Token": "2336412f37fb687f5d51e6e2425dacbbffff", + "Subject": null, + "MessageAttributes": {} + } + } + ] +}