Skip to content

Conversation

@lmammino
Copy link
Contributor

@lmammino lmammino commented Nov 24, 2025

📬 Issue #, if available:

✍️ Description of changes

This PR improves the ergonomics of working with SqsBatchResponse and
KinesisEventResponse.

Since v1.0, these structs have been marked as #[non_exhaustive], which means you
can no longer construct them and provide their content in a single expression.

Note

This is also the case for the related structs SqsBatchItemFailure and
KinesisBatchItemFailure.


❌ Previous (pre-1.0, now broken) API

The following code used to be valid before 1.0, but no longer compiles:

use aws_lambda_events::event::sqs::{SqsEvent, SqsBatchResponse,
SqsBatchItemFailure};

async fn function_handler(
    event: LambdaEvent<SqsEvent>
) -> Result<SqsBatchResponse, Error> {
    let mut batch_item_failures = Vec::new();

    for record in event.payload.records {
        let message_id = record.message_id.clone().unwrap_or_default();

        // Try to process the message
        if let Err(e) = process_record(&record).await {
            println!("Failed to process message {}: {}", message_id, e);
            // Add to failures list so it will be retried
            batch_item_failures.push(SqsBatchItemFailure {
                item_identifier: message_id,
            });
        }
    }

    // Return the list of failed messages
    Ok(SqsBatchResponse {
        batch_item_failures,
    })
}

// ...

⚠️ Current (working but verbose) pattern

With #[non_exhaustive], you now need something slightly more verbose, similar to
the official example in the repo
([advanced-sqs-partial-batch-failures](https://github.com/aws/aws-lambda-rust-runti
me/blob/main/examples/advanced-sqs-partial-batch-failures/src/main.rs)):

use aws_lambda_events::event::sqs::{SqsEvent, SqsBatchResponse,
SqsBatchItemFailure};

async fn function_handler(
    event: LambdaEvent<SqsEvent>
) -> Result<SqsBatchResponse, Error> {
    let mut batch_item_failures = Vec::new();

    for record in event.payload.records {
        let message_id = record.message_id.clone().unwrap_or_default();

        // Try to process the message
        if let Err(e) = process_record(&record).await {
            println!("Failed to process message {}: {}", message_id, e);

            // Build failure using Default because SqsBatchItemFailure is
#[non_exhaustive]
            let mut failure = SqsBatchItemFailure::default();
            failure.item_identifier = message_id;

            // Add to failures list so it will be retried
            batch_item_failures.push(failure);
        }
    }

    // Return the list of failed messages
    let mut response = SqsBatchResponse::default();
    response.batch_item_failures = batch_item_failures;

    Ok(response)
}

// ...

This works, but it is more verbose and a bit less ergonomic for a very common usage
pattern.


✅ Proposed (this PR): ergonomic helper API

This PR adds two helper methods to both SqsBatchResponse and
KinesisEventResponse:

  1. add_failure(item_identifier) - Add a single failure to the response
  2. set_failures(item_identifiers) - Set multiple failures at once (replaces
    any existing failures)

Both methods accept flexible input types via impl Into<String>, so you can pass
&str, String, or any other compatible type.

SQS Example using add_failure:

use aws_lambda_events::event::sqs::{SqsEvent, SqsBatchResponse};

async fn function_handler(
    event: LambdaEvent<SqsEvent>,
) -> Result<SqsBatchResponse, Error> {
    let mut response = SqsBatchResponse::default();

    for record in event.payload.records {
        let message_id = record.message_id.clone().unwrap_or_default();

        if let Err(e) = process_record(&record).await {
            println!("Failed to process message {}: {}", message_id, e);
            response.add_failure(message_id);
        }
    }

    Ok(response)
}

// ...

SQS Example using set_failures:

use aws_lambda_events::event::sqs::{SqsEvent, SqsBatchResponse};

async fn function_handler(
    event: LambdaEvent<SqsEvent>,
) -> Result<SqsBatchResponse, Error> {
    let mut failed_ids = Vec::new();

    for record in event.payload.records {
        let message_id = record.message_id.clone().unwrap_or_default();

        if let Err(e) = process_record(&record).await {
            println!("Failed to process message {}: {}", message_id, e);
            failed_ids.push(message_id);
        }
    }

    let mut response = SqsBatchResponse::default();
    response.set_failures(failed_ids);

    Ok(response)
}

// ...

Kinesis Example using add_failure:

use aws_lambda_events::event::kinesis::KinesisEvent;
use aws_lambda_events::event::streams::KinesisEventResponse;

async fn function_handler(
    event: LambdaEvent<KinesisEvent>,
) -> Result<KinesisEventResponse, Error> {
    let mut response = KinesisEventResponse::default();

    for record in event.payload.records {
        let sequence_number = record.kinesis.sequence_number.clone();

        if let Err(e) = process_record(&record).await {
            println!("Failed to process record {}: {}", sequence_number, e);
            response.add_failure(sequence_number);
        }
    }

    Ok(response)
}

// ...

This keeps the types #[non_exhaustive] while restoring a convenient and
discoverable way to build partial batch responses.


🔏 By submitting this pull request

  • I confirm that I've run cargo +nightly fmt.
  • I confirm that I've run cargo clippy --fix.
  • I confirm that I've made a best-effort attempt to update all relevant
    documentation.
  • I confirm that my contribution is made under the terms of the Apache 2.0
    license.

@lmammino lmammino changed the title feat: Improve ergonomics of SqsBatchResponse feat(‎lambda-events): Improve ergonomics of SqsBatchResponse Nov 24, 2025
@lmammino lmammino changed the title feat(‎lambda-events): Improve ergonomics of SqsBatchResponse feat(‎lambda-events): Improve ergonomics of SqsBatchResponse Nov 24, 2025
@lmammino lmammino changed the title feat(‎lambda-events): Improve ergonomics of SqsBatchResponse feat(‎lambda-events): Improve ergonomics of SqsBatchResponse and KinesisEventResponse Nov 25, 2025
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.

The external API seems fine to me. Though, need to get consensus with maintainers about both having codegenned builders and these sort of extra-ergonomic constructors as discussed in #1060

Left some feedback around making this more maintainable using ..Default::default() constructor syntax since we are crate-local.

We probably should also explicitly state in doc comments that all fields besides the one being set, will use the defaults for the struct (which will generally be None, an allocation-free empty string or map, etc, though that doesn't need to go in doc comments).

///
/// # Example
///
/// ```rust,no_run
Copy link
Collaborator

Choose a reason for hiding this comment

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

minor: any reason this needs to be no_run?

///
/// # Example
///
/// ```rust,no_run
Copy link
Collaborator

Choose a reason for hiding this comment

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

minor: any reason this needs to be no_run?

/// }
/// ```
pub fn add_failure(&mut self, message_id: impl Into<String>) {
self.batch_item_failures.push(BatchItemFailure {
Copy link
Collaborator

@jlizen jlizen Dec 2, 2025

Choose a reason for hiding this comment

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

Is there a reason not to use ..Default::default() here to reduce churn in this function as the struct evolves? We shouldn't be limited by #[non_exhaustive] locally to the crate?

Ie:

self.batch_item_failures.push(BatchItemFailure {
    item_identifier: message_id.into(),
    ...Default::default()
};

We also should state in the doc comment that defaults will be used for all fields besides item_identifier.

{
self.batch_item_failures = message_ids
.into_iter()
.map(|id| BatchItemFailure {
Copy link
Collaborator

@jlizen jlizen Dec 2, 2025

Choose a reason for hiding this comment

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

Same feedback using ..Default::default() for future proofing, mentioning that defaults are used for other fields.

/// to be enabled in your Lambda function's Kinesis event source mapping configuration.
/// Without this setting, Lambda will retry the entire batch on any failure.
pub fn add_failure(&mut self, item_identifier: impl Into<String>) {
self.batch_item_failures.push(KinesisBatchItemFailure {
Copy link
Collaborator

@jlizen jlizen Dec 2, 2025

Choose a reason for hiding this comment

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

Same feedback using ..Default::default() for future proofing, mentioning that defaults are used for other fields.

/// **Important**: This feature requires `FunctionResponseTypes: ReportBatchItemFailures`
/// to be enabled in your Lambda function's Kinesis event source mapping configuration.
/// Without this setting, Lambda will retry the entire batch on any failure.
pub fn set_failures<I, S>(&mut self, item_identifiers: I)
Copy link
Collaborator

@jlizen jlizen Dec 2, 2025

Choose a reason for hiding this comment

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

Same feedback using ..Default::default() for future proofing, mentioning that defaults are used for other fields.

@jlizen
Copy link
Collaborator

jlizen commented Dec 2, 2025

Update - I spoke with other maintainers, and we're happy to accept this contribution with the above comments addressed! Thanks for this :)

@PartiallyUntyped will follow up in #1060 about the broader plans around codegenned builders, how they relate to these fine-tuned constructors, etc, but we don't need to let that block this PR.

@jlizen
Copy link
Collaborator

jlizen commented Dec 2, 2025

Looks like some CI failures as well due to the new doc tests. I don't mind adding lambda_runtime and tokio to dev-dependencies to get the doctests compiling. I'd prefer that to marking them no-compile since that doesn't age well.

It's also fine to rip out the usage samples if you prefer, though I do think they are pretty useful.

I think it will keep requiring that I approve the CI runs for you, which I am happy to, but you should be able to repro these failures on your fork as well to validate a fix.

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.

2 participants