Skip to content

fix(sns): support SubscriptionConfirmation and UnsubscribeConfirmation message types#1102

Open
abhu85 wants to merge 3 commits intoaws:mainfrom
abhu85:fix/sns-subscription-confirmation-966/2026-02-19
Open

fix(sns): support SubscriptionConfirmation and UnsubscribeConfirmation message types#1102
abhu85 wants to merge 3 commits intoaws:mainfrom
abhu85:fix/sns-subscription-confirmation-966/2026-02-19

Conversation

@abhu85
Copy link

@abhu85 abhu85 commented Feb 19, 2026

Summary

Add a separate SnsSubscriptionMessage struct for handling SNS SubscriptionConfirmation and UnsubscribeConfirmation message types. This is a non-breaking change that keeps the existing SnsMessage struct unchanged.

Fixes #966

Changes

1. New SnsSubscriptionMessage Struct

pub struct SnsSubscriptionMessage {
    pub sns_message_type: String,  // "SubscriptionConfirmation" or "UnsubscribeConfirmation"
    pub message_id: String,
    pub topic_arn: String,
    pub subscribe_url: Option<String>,  // Some(url) for SubscriptionConfirmation, None for UnsubscribeConfirmation
    pub token: String,
    pub message: String,
    // ... other standard SNS fields
}

2. Documentation Updates

  • Added struct-level docs to SnsMessage and SnsMessageObj clarifying they are for Notification messages only
  • Added cross-references to SnsSubscriptionMessage for handling confirmation types

3. Test Coverage

  • my_example_sns_subscription_confirmation - verifies SubscriptionConfirmation deserialization
  • my_example_sns_unsubscribe_confirmation - verifies UnsubscribeConfirmation deserialization

Usage Example

use aws_lambda_events::event::sns::SnsSubscriptionMessage;

fn handle_confirmation(msg: SnsSubscriptionMessage) {
    if let Some(url) = &msg.subscribe_url {
        // SubscriptionConfirmation - visit URL or use token to confirm
        println!("Confirm subscription at: {}", url);
    } else {
        // UnsubscribeConfirmation
        println!("Unsubscribe confirmed");
    }
}

Breaking Changes

None. This PR:

  • ✅ Does not modify existing SnsMessage or SnsMessageObj structs
  • ✅ Only adds documentation to existing structs
  • ✅ Adds a new struct without affecting existing code

Test Results

$ cargo test --package aws_lambda_events --features sns
test result: ok. 212 passed; 0 failed; 0 ignored

Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

…n 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<String>) since it's only present
  in Notification messages
- Adds `subscribe_url` field (Option<String>) for confirmation messages
- Adds `token` field (Option<String>) for confirmation messages

All fields use #[serde(default)] to handle missing fields gracefully.

Fixes aws#966

Signed-off-by: abhu85 <151518127+abhu85@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jlizen
Copy link
Collaborator

jlizen commented Feb 20, 2026

You are correct to state that SnsMessage is only for notifications:

sns_message_type: String The type of SNS message. For a lambda event, this should always be Notification

I didn't realize that it was possible to dispatch the (un)subscribe notifications to an lambda event sink in general. Could you share a little about your use case?

Anyway -
I don't think that the existing implementation should be considered a bug, as it is specified to behave as such, (though could be clearer, doc comment on the struct would be good). I would probably name it SnsNotification if I could wave a wand...

Rather than this breaking change, how about annotating the current type with a note that it is only for Notifications. And then add a separate type for the (un)subscription confirmations?

We could also have a #[non_exhaustive] enum that allows having a handler that can extract either, if that is useful? Certainly you could do it with an untagged enum, but I think it might also work with an internally tag enum containing a flattened tuple variant?

@abhu85
Copy link
Author

abhu85 commented Feb 20, 2026

Thanks for the feedback @jlizen! You're absolutely right about avoiding breaking changes.

Use Case

We're building Lambda functions that handle SNS subscription confirmations directly (e.g., when subscribing HTTP(S) endpoints to SNS topics programmatically). The Lambda needs to parse the subscription confirmation message to extract the subscribe_url and token to complete the subscription.

Proposed Non-Breaking Approach

I agree with your suggestion. Here's what I propose:

Option 1: Separate Types (Recommended)

// Keep existing for notifications (no changes)
pub struct SnsMessage { ... }

// New type for confirmations
pub struct SnsSubscriptionConfirmation {
    pub message: String,
    pub subscribe_url: Option<String>,
    pub unsubscribe_url: Option<String>, 
    pub token: String,
    // ... other common fields
}

// Optional: Enum wrapper
#[non_exhaustive]
pub enum SnsEvent {
    Notification(SnsMessage),
    SubscriptionConfirmation(SnsSubscriptionConfirmation),
    UnsubscribeConfirmation(SnsSubscriptionConfirmation),
}

Option 2: Separate Module

Move confirmation types to a new sns::subscription module to keep the API organized.

Would you prefer Option 1 with the enum wrapper, or would you like me to explore a different approach? I'm happy to revise the PR based on your preferred design.

Also, should I add documentation explaining which message types map to which structs based on the Type field in the SNS payload?

@jlizen
Copy link
Collaborator

jlizen commented Feb 20, 2026

I prefer #1. No need to separate out to a new module. SnsSubscriptionConfirmation doesn't need unsubscribe_url field, right?

I don't think we need the enum wrapper unless you have a usage for it. It sounds like you don't. I think lambdas handling subscription events will generally be fairly distinct from ones handling notification events.

Also, should I add documentation explaining which message types map to which structs based on the Type field in the SNS payload?

Is this with the enum? If you do need the enum, then yes you should document semantics. Otherwise, I think we just could use some clearer docs on the SnsMessage struct-level saying it is only notifications, and to see [SnsSubscriptionConfirmation] for (un)subscribe. (And if you do add the enum, mention that too.

For the enum, do keep in mind that you will need to handle getting serde to distinguish the two, so your example wouldn't work exactly as written (no serde handling). I would probably not bother unless you have a need for it (ie, you are receiving a mixed stream of event types).

@abhu85
Copy link
Author

abhu85 commented Feb 20, 2026

Thanks for the clarification @jlizen! I've drafted an implementation following your guidance. Here's the plan:

Proposed Implementation

1. Keep SnsMessage Unchanged (Documentation Update Only)

Add struct-level documentation clarifying it's for Notification messages only:

/// SnsMessage stores information about SNS **Notification** type messages only.
///
/// **Important**: This struct is designed specifically for handling SNS Notification messages
/// (where `Type` field equals "Notification"). For handling SubscriptionConfirmation or
/// UnsubscribeConfirmation messages, use [`SnsSubscriptionConfirmation`] instead.

✅ Zero breaking changes - all existing code continues to work.

2. Add New SnsSubscriptionConfirmation Struct

/// SnsSubscriptionConfirmation stores information about SNS SubscriptionConfirmation and
/// UnsubscribeConfirmation type messages.
///
/// Handles both `Type: "SubscriptionConfirmation"` and `Type: "UnsubscribeConfirmation"`.
#[non_exhaustive]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct SnsSubscriptionConfirmation {
    #[serde(rename = "Type")]
    pub message_type: String,

    pub message_id: String,
    pub topic_arn: String,
    pub message: String,
    pub timestamp: DateTime<Utc>,
    pub signature_version: String,
    pub signature: String,

    #[serde(alias = "SigningCertURL")]
    pub signing_cert_url: String,

    /// Present for SubscriptionConfirmation, None for UnsubscribeConfirmation
    #[serde(alias = "SubscribeURL")]
    #[serde(default)]
    pub subscribe_url: Option<String>,

    /// Present in both SubscriptionConfirmation and UnsubscribeConfirmation
    pub token: String,

    #[serde(default)]
    pub subject: Option<String>,

    #[serde(deserialize_with = "deserialize_lambda_map")]
    #[serde(default)]
    pub message_attributes: HashMap<String, MessageAttribute>,

    #[cfg(feature = "catch-all-fields")]
    #[serde(flatten)]
    pub other: serde_json::Map<String, Value>,
}

Key points:

  • subscribe_url: Option<String> - Some(url) for SubscriptionConfirmation, None for UnsubscribeConfirmation
  • ✅ No unsubscribe_url field (as you noted, that's only in Notification messages)
  • ✅ Single struct handles both confirmation types (simpler than two separate structs)
  • ✅ No enum wrapper (as you suggested - not needed for typical use cases)

3. Usage Example

use aws_lambda_events::event::sns::SnsSubscriptionConfirmation;

fn handle_confirmation(confirmation: SnsSubscriptionConfirmation) {
    if let Some(url) = confirmation.subscribe_url {
        // SubscriptionConfirmation - visit URL or use token to confirm
        println!("Visit {} to confirm", url);
        println!("Or use token: {}", confirmation.token);
    } else {
        // UnsubscribeConfirmation
        println!("Unsubscribe confirmed, token: {}", confirmation.token);
    }
}

4. Tests

Adding three comprehensive tests:

  • test_sns_subscription_confirmation() - verifies SubscriptionConfirmation deserialization, subscribe_url is Some(url)
  • test_sns_unsubscribe_confirmation() - verifies UnsubscribeConfirmation deserialization, subscribe_url is None
  • test_sns_message_is_notification_only() - verifies existing SnsMessage still works for Notifications

Questions

  1. Location: Add SnsSubscriptionConfirmation right after SnsMessage in the same file (no separate module)?
  2. Exports: Should I add it to any re-export lists?
  3. Naming: Is SnsSubscriptionConfirmation clear, or would you prefer SnsConfirmationMessage?

I can prepare the full PR update once you confirm this approach looks good. The fixture file (example-sns-subscription-confirmation.json) already exists from the previous attempt.

Let me know if you'd like me to adjust anything!

@jlizen
Copy link
Collaborator

jlizen commented Feb 20, 2026

Overall approach seems fine.

Location: Add SnsSubscriptionConfirmation right after SnsMessage in the same file (no separate module)?

That's fine

Exports: Should I add it to any re-export lists?

Follow existing conventions. I believe that is only to export the module, which we already have for sns.

Naming: Is SnsSubscriptionConfirmation clear, or would you prefer SnsConfirmationMessage?

I probably would prefer SnsSubscriptionMessage if anything, but I dno't care very much, all three are intuitive enough.

test_sns_message_is_notification_only() - verifies existing SnsMessage still works for Notifications

I think we probably already have a test covering this, but if not, this would be good too.

Add a separate SnsSubscriptionMessage struct for handling SubscriptionConfirmation
and UnsubscribeConfirmation SNS message types. This is a non-breaking change that
keeps the existing SnsMessage struct unchanged.

Changes:
- Add SnsSubscriptionMessage struct with subscribe_url (Option) and token fields
- Add documentation to SnsMessage/SnsMessageObj clarifying they are for Notification only
- Add test fixtures for both SubscriptionConfirmation and UnsubscribeConfirmation
- Add tests verifying deserialization of both confirmation types

The new struct distinguishes confirmation types by:
- sns_message_type field ("SubscriptionConfirmation" or "UnsubscribeConfirmation")
- subscribe_url: Some(url) for SubscriptionConfirmation, None for UnsubscribeConfirmation

Fixes aws#966

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@abhu85
Copy link
Author

abhu85 commented Feb 20, 2026

@jlizen I've implemented the changes based on your feedback!

Summary of Changes (commit 86cac61)

1. New SnsSubscriptionMessage Struct

  • Added right after SnsMessage in the same file (no separate module)
  • Uses SnsSubscriptionMessage naming (went with the more specific name since you said all three were intuitive)
  • subscribe_url: Option<String> - Some(url) for SubscriptionConfirmation, None for UnsubscribeConfirmation
  • No unsubscribe_url field (as you noted)
  • No enum wrapper

2. Documentation Only for Existing Structs

  • Added struct-level docs to SnsMessage clarifying it's for Notification only
  • Added cross-reference to SnsSubscriptionMessage
  • Same treatment for SnsMessageObj
  • Zero changes to existing struct fields

3. Tests

  • my_example_sns_subscription_confirmation - verifies SubscriptionConfirmation
  • my_example_sns_unsubscribe_confirmation - verifies UnsubscribeConfirmation

All 212 tests pass.

Let me know if you'd like any adjustments!

Copy link
Collaborator

@jlizen jlizen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall, one small change.

///
/// # Example
///
/// ```ignore
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need to be ignore, does it? It should compile as-is?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right! Fixed in 9a9a3c5 - removed the ignore attribute. Doc tests confirm it compiles correctly.

Copy link
Collaborator

@jlizen jlizen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whoops, meant to request the one change

The doc example compiles as-is, so the ignore attribute is unnecessary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SnsMessage struct fails with SubscriptionConfirmation types

2 participants

Comments