Skip to content

Add configurable UUID conversion for non-AWS SQS-compatible services#1433

Open
jm0514 wants to merge 8 commits intoawspring:mainfrom
jm0514:gh-814
Open

Add configurable UUID conversion for non-AWS SQS-compatible services#1433
jm0514 wants to merge 8 commits intoawspring:mainfrom
jm0514:gh-814

Conversation

@jm0514
Copy link

@jm0514 jm0514 commented Aug 9, 2025

Add configurable UUID conversion for non-AWS SQS-compatible services

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

Added configurable support for SQS-compatible cloud services (like Yandex Message Queue) that use non-UUID MessageId formats. The framework now supports both UUID and non-UUID MessageId handling through a new configuration option.

Configuration:

# For AWS SQS (default behavior)
spring.cloud.aws.sqs.convert-message-id-to-uuid=true

# For non-AWS SQS-compatible services (Yandex, etc.)
spring.cloud.aws.sqs.convert-message-id-to-uuid=false

💡 Motivation and Context

Solves #814

💚 How did you test it?

  • Unit Tests: Added comprehensive tests for SqsHeaderMapper and MessageHeaderUtils

📝 Checklist

  • I reviewed submitted code
  • I added tests to verify changes
  • I updated reference documentation to reflect the change
  • All tests passing
  • No breaking changes

🔮 Next steps

accessor.setHeader(SqsHeaders.SQS_AWS_MESSAGE_ID_HEADER, source.messageId());
MessageHeaders messageHeaders = accessor.toMessageHeaders();
logger.trace("Mapped headers {} for message {}", messageHeaders, source.messageId());
return new MessagingMessageHeaders(messageHeaders, UUID.randomUUID());
Copy link
Contributor

Choose a reason for hiding this comment

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

Passing a random UUID does not feel right. Perhaps MessagingMessageHeaders can have id set as String instead of UUID? cc @tomazfernandes

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the suggestion! Updated to generate UUID from Message ID instead of random UUID.

@tomazfernandes
Copy link
Contributor

tomazfernandes commented Sep 2, 2025

Hey @jm0514, thanks for the PR!

Perhaps MessagingMessageHeaders can have id set as String instead of UUID? cc @tomazfernandes

@maciejwalkowiak, the issue there is that the MessagingMessageHeaders inherits from MessageHeaders from Spring, and that one requires an UUID type in the constructor.

Might be worth raising with the Spring Messaging team?

In the meantime, I think we can make the solution simpler if we leverage the fact that most ID-related operations are made through the MessageHeaderUtils#getId method, which already returns a String.

Instead of requiring a configuration change to support other ID types, we could simplify the logic by trying to parse the UUID, and if it fails we add the new header as suggested in the PR.

Then in the MessageHeaderUtils#getId method, we check for the header first, and return either that, or the Spring Message ID if the former is absent.

The caveat would be that for non UUID ids, the result from getting the ID header directly would be a random UUID, but I don't think there's a way around it with the existing MessageHeaders implementation, so properly documenting the behavior should suffice. And since we don't currently support that anyway, we wouldn't really need a breaking change.

What do you folks think?

@tomazfernandes tomazfernandes added the status: waiting-for-feedback Waiting for feedback from issuer label Oct 4, 2025
@github-actions github-actions bot added the type: documentation Documentation or Samples related issue label Feb 20, 2026
@jm0514
Copy link
Author

jm0514 commented Feb 20, 2026

  • Fail-fast: When convert-message-id-to-uuid=true (default), receiving a non-UUID message ID now throws a MessagingException with a clear error
    message and instructions.
  • Send-side parity: SqsTemplate now handles non-UUID message IDs in send responses. SendResult.messageId() returns a deterministic UUID, and the raw
    ID is available via SendResult.additionalInformation() under the rawMessageId key.
  • Naming consistency: Renamed Sqs_AWSMessageIdSqs_RawMessageId and getAwsMessageId()getRawMessageId() to be provider-neutral and consistent
    across receive/send sides.
  • Documentation: Added Non-UUID Message IDs section in sqs.adoc and property entry in _configprops.adoc.

Copy link
Contributor

@tomazfernandes tomazfernandes left a comment

Choose a reason for hiding this comment

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

Hey @jm0514, thanks for the updates. Overall direction looks solid.

I have a couple of suggestions to simplify the design and reduce duplication.

public MessagingMessageConverter<Message> messageConverter(ObjectProvider<JsonMapper> jsonMapperProvider) {
JsonMapper jsonMapper = jsonMapperProvider.getIfAvailable();
return jsonMapper != null ? new SqsMessagingMessageConverter(jsonMapper)
public MessagingMessageConverter<Message> messageConverter(ObjectProvider<JsonMapper> jsonMapperProvider,
Copy link
Contributor

Choose a reason for hiding this comment

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

Currently the auto-config manually creates a SqsHeaderMapper and sets the flag on it. Instead, let's add convertMessageIdToUuid as a property on both SqsContainerOptions and SqsTemplateOptions, so auto-config just does:

  factory.configure(options -> options.convertMessageIdToUuid(sqsProperties.getConvertMessageIdToUuid()));
  builder.configure(options -> options.convertMessageIdToUuid(sqsProperties.getConvertMessageIdToUuid()));

This keeps the configuration surface consistent between container and template, removes the manual SqsHeaderMapper creation from auto-config, and gives programmatic users (without auto-config) a clean way to set it through the standard options API.

Copy link
Author

@jm0514 jm0514 Mar 14, 2026

Choose a reason for hiding this comment

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

Thanks for the great suggestion! This makes the configuration much cleaner and consistent with the existing patterns.

Applied in 1d32b0c. Moved convertMessageIdToUuid to both SqsContainerOptions and SqsTemplateOptions, so auto-config now uses factory.configure() and builder.configure() instead of manually creating SqsHeaderMapper.
Added getHeaderMapper() to AbstractMessagingMessageConverter to propagate the option to the header mapper.

MessageHeaders messageHeaders = accessor.toMessageHeaders();
logger.trace("Mapped headers {} for message {}", messageHeaders, source.messageId());
return new MessagingMessageHeaders(messageHeaders, UUID.fromString(source.messageId()));

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's consolidate this logic so we get consistent behavior across both listener and template.

My suggestion would be to create a SqsMessageIdResolver utility class with a method such as:

 public static MessageHeaders resolveAndAddMessageId(String messageId, MessageHeaders headers,
          boolean convertMessageIdToUuid) {
      MessageHeaders withRawId = MessageHeaderUtils.addHeaderIfAbsent(
          headers, SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER, messageId);
      if (isValidUuid(messageId)) {
          return new MessagingMessageHeaders(withRawId, UUID.fromString(messageId));
      }
      if (convertMessageIdToUuid) {
          throw new MessagingException(String.format(
              "Message ID '%s' is not a valid UUID. To support non-UUID message IDs, "
                  + "set 'spring.cloud.aws.sqs.convert-message-id-to-uuid=false'. "
                  + "The raw message ID will be available via the '%s' header.",
              messageId, SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER));
      }
      return new MessagingMessageHeaders(withRawId,
          UUID.nameUUIDFromBytes(messageId.getBytes(StandardCharsets.UTF_8)));
  }

 private static boolean isValidUuid(String value) {
          try {
              UUID.fromString(value);
              return true;
          }
          catch (IllegalArgumentException e) {
              return false;
          }
      }

Then we'd use it in both SqsHeaderMapper and SqsTemplate.

Open to suggestions if you have a different idea.

Copy link
Author

Choose a reason for hiding this comment

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

Your suggestion was clear and well thought out, so I followed it as proposed.

Applied in f1d7a50. Created SqsMessageIdResolver with resolveAndAddMessageId() for the listener side and resolveUuid() / isValidUuid() for the template side.
Both SqsHeaderMapper and SqsTemplate now delegate to this utility class.

Copy link
Contributor

@tomazfernandes tomazfernandes left a comment

Choose a reason for hiding this comment

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

@jm0514, thanks for the updates. I left a few comments, we're getting closer.

The main point is that there's a mismatch between what Spring Messaging exposes, which is a UUID-based Message, and the SQS contract, which uses String ids.

Therefore we need a consistent message-id translation layer exposed consistently to users regardless of whether they're using SQS directly or another compatible implementation.

This also makes it easier to move between SQS and SQS-compatible services over time.

Also, please add @since 4.1.0 to the new classes.

if (sequenceNumber != null) {
additionalInfo.put(SqsTemplateParameters.SEQUENCE_NUMBER_PARAMETER_NAME, sequenceNumber);
}
UUID messageId = SqsMessageIdResolver.resolveUuid(rawMessageId);
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd suggest always adding the new header to originalMessage through resolveAndAddMessageId, then reading the UUID value back from the resulting message.

That keeps the behavior consistent between receive and send paths and avoids silently handling the UUID conversion when not explicitly configured.

That also allows us to make the remaining methods of SqsMessageIdResolver private.

/**
* The raw provider message ID when it is not a valid UUID.
*/
public static final String RAW_MESSAGE_ID_PARAMETER_NAME = "rawMessageId";
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's name it "SQS_RAW_MESSAGE_ID_PARAMETER_NAME" for consistency with the header.

}
UUID messageId = SqsMessageIdResolver.resolveUuid(rawMessageId);
if (!SqsMessageIdResolver.isValidUuid(rawMessageId)) {
additionalInfo.put(SqsTemplateParameters.RAW_MESSAGE_ID_PARAMETER_NAME, rawMessageId);
Copy link
Contributor

Choose a reason for hiding this comment

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

If this id is already added to the MessageHeaders, we may not need to also expose it through additionalInfo.

If we keep this parameter, we should always add it so users can rely on it regardless of whether they are using SQS or another implementation.

* {@link software.amazon.awssdk.services.sqs.model.Message} instances.
* @return the header mapper instance.
*/
public HeaderMapper<S> getHeaderMapper() {
Copy link
Contributor

Choose a reason for hiding this comment

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

We could change to a configuration-style API here, for example configureHeaderMapper(Consumer<HeaderMapper<S>> configurer).

That preserves encapsulation and keeps the API aligned with the configuration patterns used in the project.

configureHeaderMapper(sqsContainerOptions);
}

private void configureHeaderMapper(SqsContainerOptions sqsContainerOptions) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's move the configuration logic to SqsMessageIdResolver and centralize all message id resolution logic there and keep behavior consistent between the template and message source.

I suggest something along these lines:

 public static void configureMessageIdResolution(
          MessagingMessageConverter<Message> converter, boolean convertMessageIdToUuid)`.

/**
* Sqs specific options for the {@link SqsTemplate}.
*
* @author Jeongmin Kim
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add me as an author here as well, along with @since 3.0.0. I missed it when the class was originally created.

@tomazfernandes tomazfernandes removed the status: waiting-for-feedback Waiting for feedback from issuer label Mar 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: sqs SQS integration related issue type: documentation Documentation or Samples related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants