From 73bb014cc897785d270fe78c646bca6c3b64fc41 Mon Sep 17 00:00:00 2001 From: jeongmin Date: Sat, 9 Aug 2025 16:08:47 +0900 Subject: [PATCH 1/6] Add an option to receive MessageId as String rather than UUID --- .../sqs/SqsAutoConfiguration.java | 20 +++++-- .../autoconfigure/sqs/SqsProperties.java | 10 ++++ .../cloud/sqs/MessageHeaderUtils.java | 19 +++++++ .../cloud/sqs/listener/SqsHeaders.java | 5 ++ .../cloud/sqs/operations/SqsTemplate.java | 14 ++--- .../support/converter/SqsHeaderMapper.java | 30 +++++++++-- .../cloud/sqs/MessageHeaderUtilsTest.java | 54 +++++++++++++++++++ .../converter/SqsHeaderMapperTests.java | 29 ++++++++++ 8 files changed, 166 insertions(+), 15 deletions(-) diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsAutoConfiguration.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsAutoConfiguration.java index c6c89301e3..45e4ad0d74 100644 --- a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsAutoConfiguration.java +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsAutoConfiguration.java @@ -32,6 +32,7 @@ import io.awspring.cloud.sqs.operations.SqsTemplate; import io.awspring.cloud.sqs.operations.SqsTemplateBuilder; import io.awspring.cloud.sqs.support.converter.MessagingMessageConverter; +import io.awspring.cloud.sqs.support.converter.SqsHeaderMapper; import io.awspring.cloud.sqs.support.converter.SqsMessagingMessageConverter; import io.awspring.cloud.sqs.support.converter.legacy.JacksonJsonMessageConverterMigration; import io.awspring.cloud.sqs.support.converter.legacy.JacksonMessageConverterMigration; @@ -159,10 +160,15 @@ private void configureProperties(SqsContainerOptionsBuilder options) { static class SqsJacksonConfiguration { @ConditionalOnMissingBean @Bean - public MessagingMessageConverter messageConverter(ObjectProvider jsonMapperProvider) { - JsonMapper jsonMapper = jsonMapperProvider.getIfAvailable(); - return jsonMapper != null ? new SqsMessagingMessageConverter(jsonMapper) + public MessagingMessageConverter messageConverter(ObjectProvider jsonMapperProvider, + SqsProperties sqsProperties) { + SqsMessagingMessageConverter converter = jsonMapperProvider.getIfAvailable() != null + ? new SqsMessagingMessageConverter(jsonMapperProvider.getIfAvailable()) : new SqsMessagingMessageConverter(); + SqsHeaderMapper headerMapper = new SqsHeaderMapper(); + headerMapper.setConvertMessageIdToUuid(sqsProperties.getConvertMessageIdToUuid()); + converter.setHeaderMapper(headerMapper); + return converter; } @Bean @@ -179,8 +185,12 @@ public JacksonMessageConverterMigration jsonMapperWrapper(ObjectProvider messageConverter() { - return new LegacyJackson2SqsMessagingMessageConverter(); + public MessagingMessageConverter messageConverter(SqsProperties sqsProperties) { + LegacyJackson2SqsMessagingMessageConverter converter = new LegacyJackson2SqsMessagingMessageConverter(); + SqsHeaderMapper headerMapper = new SqsHeaderMapper(); + headerMapper.setConvertMessageIdToUuid(sqsProperties.getConvertMessageIdToUuid()); + converter.setHeaderMapper(headerMapper); + return converter; } @Bean diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java index 509768d2cf..59b1c557ca 100644 --- a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java @@ -51,6 +51,16 @@ public void setListener(Listener listener) { private Boolean observationEnabled = false; + private Boolean convertMessageIdToUuid = true; + + public Boolean getConvertMessageIdToUuid() { + return convertMessageIdToUuid; + } + + public void setConvertMessageIdToUuid(Boolean convertMessageIdToUuid) { + this.convertMessageIdToUuid = convertMessageIdToUuid; + } + /** * Return the strategy to use if the queue is not found. * @return the {@link QueueNotFoundStrategy} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java index 5ee1e238c4..1486fcadc1 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java @@ -15,6 +15,7 @@ */ package io.awspring.cloud.sqs; +import io.awspring.cloud.sqs.listener.SqsHeaders; import io.awspring.cloud.sqs.support.converter.MessagingMessageHeaders; import java.util.Collection; import java.util.Map; @@ -150,4 +151,22 @@ public static Message removeHeaderIfPresent(Message message, String ke return new GenericMessage<>(message.getPayload(), newHeaders); } + /** + * Return the AWS message ID, falling back to Spring message ID if not present. + * @param message the message. + * @return the AWS ID or Spring ID. + */ + public static String getAwsMessageId(Message message) { + String awsMessageId = message.getHeaders().get(SqsHeaders.SQS_AWS_MESSAGE_ID_HEADER, String.class); + return awsMessageId != null ? awsMessageId : getId(message); + } + + /** + * Return the messages' AWS ID as a concatenated {@link String}. + * @param messages the messages. + * @return the AWS IDs. + */ + public static String getAwsMessageId(Collection> messages) { + return messages.stream().map(MessageHeaderUtils::getAwsMessageId).collect(Collectors.joining("; ")); + } } diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java index 41fcc166a5..095cb3a9c6 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java @@ -88,6 +88,11 @@ private SqsHeaders() { */ public static final String SQS_DEFAULT_TYPE_HEADER = "JavaType"; + /** + * Header for the original AWS MessageId when not using UUID conversion. + */ + public static final String SQS_AWS_MESSAGE_ID_HEADER = SQS_HEADER_PREFIX + "AWSMessageId"; + public static class MessageSystemAttributes { private MessageSystemAttributes() { diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java index dd577996c1..7d186b14b5 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java @@ -359,7 +359,7 @@ protected CompletableFuture> doSendBatchAsync(String end logger.debug("Sending messages {} to endpoint {}", messages, endpointName); return createSendMessageBatchRequest(endpointName, messages).thenCompose(this.sqsAsyncClient::sendMessageBatch) .thenApply(response -> createSendResultBatch(response, endpointName, - originalMessages.stream().collect(Collectors.toMap(MessageHeaderUtils::getId, msg -> msg)))); + originalMessages.stream().collect(Collectors.toMap(MessageHeaderUtils::getAwsMessageId, msg -> msg)))); } private SendResult.Batch createSendResultBatch(SendMessageBatchResponse response, String endpointName, @@ -540,7 +540,7 @@ private Map addMissingFifoReceiveHeaders(Map hea private CompletableFuture deleteMessages(String endpointName, Collection> messages) { logger.trace("Acknowledging in queue {} messages {}", endpointName, - MessageHeaderUtils.getId(addTypeToMessages(messages))); + MessageHeaderUtils.getAwsMessageId(addTypeToMessages(messages))); return getQueueAttributes(endpointName) .thenCompose(attributes -> this.sqsAsyncClient.deleteMessageBatch(DeleteMessageBatchRequest.builder() .queueUrl(attributes.getQueueUrl()).entries(createDeleteMessageEntries(messages)).build())) @@ -559,7 +559,7 @@ private Collection> getFailedAckMessage DeleteMessageBatchResponse response, Collection> messages, String endpointName) { return response.failed().stream().map(BatchResultErrorEntry::id) - .map(id -> messages.stream().filter(msg -> MessageHeaderUtils.getId(msg).equals(id)).findFirst() + .map(id -> messages.stream().filter(msg -> MessageHeaderUtils.getAwsMessageId(msg).equals(id)).findFirst() .orElseThrow(() -> new SqsAcknowledgementException( "Could not correlate ids for acknowledgement failure", Collections.emptyList(), messages, endpointName))) @@ -570,7 +570,7 @@ private Collection> getSuccessfulAckMes DeleteMessageBatchResponse response, Collection> messages, String endpointName) { return response.successful().stream().map(DeleteMessageBatchResultEntry::id) - .map(id -> messages.stream().filter(msg -> MessageHeaderUtils.getId(msg).equals(id)).findFirst() + .map(id -> messages.stream().filter(msg -> MessageHeaderUtils.getAwsMessageId(msg).equals(id)).findFirst() .orElseThrow(() -> new SqsAcknowledgementException( "Could not correlate ids for acknowledgement failure", Collections.emptyList(), messages, endpointName))) @@ -588,7 +588,7 @@ private void logAcknowledgement(String endpointName, Collection createDeleteMessageEntries( Collection> messages) { return messages.stream() - .map(message -> DeleteMessageBatchRequestEntry.builder().id(MessageHeaderUtils.getId(message)) + .map(message -> DeleteMessageBatchRequestEntry.builder().id(MessageHeaderUtils.getAwsMessageId(message)) .receiptHandle( MessageHeaderUtils.getHeaderAsString(message, SqsHeaders.SQS_RECEIPT_HANDLE_HEADER)) .build()) diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java index c9f4546163..3e369edc14 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java @@ -62,12 +62,18 @@ public class SqsHeaderMapper implements ContextAwareHeaderMapper { private BiFunction additionalHeadersFunction = ((message, accessor) -> accessor.toMessageHeaders()); + private boolean convertMessageIdToUuid = true; + public void setAdditionalHeadersFunction( BiFunction headerFunction) { Assert.notNull(headerFunction, "headerFunction cannot be null"); this.additionalHeadersFunction = headerFunction; } + public void setConvertMessageIdToUuid(boolean convertMessageIdToUuid) { + this.convertMessageIdToUuid = convertMessageIdToUuid; + } + @Override public Message fromHeaders(MessageHeaders headers) { Message.Builder builder = Message.builder(); @@ -157,9 +163,27 @@ public MessageHeaders toHeaders(Message source) { accessor.copyHeadersIfAbsent(getMessageAttributesAsHeaders(source)); accessor.copyHeadersIfAbsent(createDefaultHeaders(source)); accessor.copyHeadersIfAbsent(createAdditionalHeaders(source)); - MessageHeaders messageHeaders = accessor.toMessageHeaders(); - logger.trace("Mapped headers {} for message {}", messageHeaders, source.messageId()); - return new MessagingMessageHeaders(messageHeaders, UUID.fromString(source.messageId())); + + if (convertMessageIdToUuid && isValidUuid(source.messageId())) { + MessageHeaders messageHeaders = accessor.toMessageHeaders(); + logger.trace("Mapped headers {} for message {}", messageHeaders, source.messageId()); + return new MessagingMessageHeaders(messageHeaders, UUID.fromString(source.messageId())); + } else { + 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()); + } + + } + + private boolean isValidUuid(String messageId) { + try { + UUID.fromString(messageId); + return true; + } catch (IllegalArgumentException e) { + return false; + } } private MessageHeaders createAdditionalHeaders(Message source) { diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java index e156e9ec82..64b30ca203 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java @@ -17,10 +17,14 @@ import static org.assertj.core.api.Assertions.assertThat; +import io.awspring.cloud.sqs.listener.SqsHeaders; import org.junit.jupiter.api.Test; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; +import java.util.Collection; +import java.util.List; + /** * Tests for {@link MessageHeaderUtils}. * @@ -93,4 +97,54 @@ void shouldPreserveOtherHeaders() { assertThat(result.getHeaders().get("another-header")).isEqualTo("another-value"); assertThat(result.getHeaders().size()).isEqualTo(message.getHeaders().size() - 1); } + + @Test + void shouldReturnAwsMessageIdWhenHeaderPresent() { + // given + String awsMessageId = "92898073-7bd6a160-5797b060-54a7e539"; + Message message = MessageBuilder.withPayload("test-payload") + .setHeader(SqsHeaders.SQS_AWS_MESSAGE_ID_HEADER, awsMessageId) + .build(); + + // when + String result = MessageHeaderUtils.getAwsMessageId(message); + + // then + assertThat(result).isEqualTo(awsMessageId); + } + + @Test + void shouldFallbackToSpringMessageIdWhenAwsHeaderNotPresent() { + // given + Message message = MessageBuilder.withPayload("test-payload").build(); + String expectedId = message.getHeaders().getId().toString(); + + // when + String result = MessageHeaderUtils.getAwsMessageId(message); + + // then + assertThat(result).isEqualTo(expectedId); + } + + @Test + void shouldConcatenateAwsMessageIdsFromCollection() { + // given + String awsMessageId1 = "aws-id-1"; + String awsMessageId2 = "aws-id-2"; + + Message message1 = MessageBuilder.withPayload("payload1") + .setHeader(SqsHeaders.SQS_AWS_MESSAGE_ID_HEADER, awsMessageId1) + .build(); + Message message2 = MessageBuilder.withPayload("payload2") + .setHeader(SqsHeaders.SQS_AWS_MESSAGE_ID_HEADER, awsMessageId2) + .build(); + + Collection> messages = List.of(message1, message2); + + // when + String result = MessageHeaderUtils.getAwsMessageId(messages); + + // then + assertThat(result).isEqualTo("aws-id-1; aws-id-2"); + } } diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java index ffbe0683a1..437059071d 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java @@ -177,6 +177,35 @@ void createsMessageWithNumberHeader(String value, String type, Number expected) assertThat(headers.get(headerName)).isEqualTo(expected); } + @Test + void shouldConvertUuidMessageIdWhenConvertMessageIdToUuidIsTrue() { + SqsHeaderMapper mapper = new SqsHeaderMapper(); + mapper.setConvertMessageIdToUuid(true); + String uuidMessageId = "550e8400-e29b-41d4-a716-446655440000"; + Message message = Message.builder() + .body("payload") + .messageId(uuidMessageId) + .build(); + MessageHeaders headers = mapper.toHeaders(message); + assertThat(headers.getId()).isEqualTo(UUID.fromString(uuidMessageId)); + assertThat(headers.get(SqsHeaders.SQS_AWS_MESSAGE_ID_HEADER)).isNull(); + } + + @Test + void shouldStoreAwsMessageIdInHeaderWhenConvertMessageIdToUuidIsFalse() { + SqsHeaderMapper mapper = new SqsHeaderMapper(); + mapper.setConvertMessageIdToUuid(false); + String nonUuidMessageId = "92898073-7bd6a160-5797b060-54a7e539"; + Message message = Message.builder() + .body("payload") + .messageId(nonUuidMessageId) + .build(); + MessageHeaders headers = mapper.toHeaders(message); + assertThat(headers.get(SqsHeaders.SQS_AWS_MESSAGE_ID_HEADER)).isEqualTo(nonUuidMessageId); + assertThat(headers.getId()).isNotEqualTo(nonUuidMessageId); + assertThat(headers.getId()).isNotNull(); + } + private static Stream validArguments() { return Stream.of(Arguments.of("10", "Number", BigDecimal.valueOf(10)), Arguments.of("3", "Number.byte", (byte) 3), Arguments.of("3", "Number.Byte", (byte) 3), From 1b24d21aa645678f5599353aabe14be547a058bf Mon Sep 17 00:00:00 2001 From: jeongmin Date: Sat, 9 Aug 2025 17:51:05 +0900 Subject: [PATCH 2/6] Add Jeongmin Kim to project contributors --- .../awspring/cloud/autoconfigure/sqs/SqsAutoConfiguration.java | 1 + .../java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java | 1 + .../src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java | 1 + .../src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java | 1 + .../main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java | 1 + .../io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java | 1 + .../test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java | 1 + .../cloud/sqs/support/converter/SqsHeaderMapperTests.java | 1 + 8 files changed, 8 insertions(+) diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsAutoConfiguration.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsAutoConfiguration.java index 45e4ad0d74..7474074b57 100644 --- a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsAutoConfiguration.java +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsAutoConfiguration.java @@ -65,6 +65,7 @@ * @author Maciej Walkowiak * @author Wei Jiang * @author Dongha Kim + * @author Jeongmin Kim * @since 3.0 */ @AutoConfiguration diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java index 59b1c557ca..eba169de3d 100644 --- a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java @@ -26,6 +26,7 @@ * * @author Tomaz Fernandes * @author Wei Jiang + * @author Jeongmin Kim * @since 3.0 */ @ConfigurationProperties(prefix = SqsProperties.PREFIX) diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java index 1486fcadc1..84f32f6015 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java @@ -31,6 +31,7 @@ * Utility class for extracting {@link MessageHeaders} from a {@link Message}. * * @author Tomaz Fernandes + * @author Jeongmin Kim * @since 3.0 */ public class MessageHeaderUtils { diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java index 095cb3a9c6..e064a48ae2 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java @@ -23,6 +23,7 @@ * * @author Tomaz Fernandes * @author Artem Bilan + * @author Jeongmin Kim * * @since 3.0 * diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java index 7d186b14b5..3811e4f521 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java @@ -77,6 +77,7 @@ * @author Tomaz Fernandes * @author Zhong Xi Lu * @author Hyunggeol Lee + * @author Jeongmin Kim * * @since 3.0 */ diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java index 3e369edc14..135bc263e8 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java @@ -51,6 +51,7 @@ * @author Tomaz Fernandes * @author Alain Sahli * @author Maciej Walkowiak + * @author Jeongmin Kim * * @since 3.0 * @see LegacyJackson2SqsMessagingMessageConverter diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java index 64b30ca203..75bd6712c9 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java @@ -29,6 +29,7 @@ * Tests for {@link MessageHeaderUtils}. * * @author Tomaz Fernandes + * @author Jeongmin Kim */ class MessageHeaderUtilsTest { diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java index 437059071d..278fbe5121 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java @@ -37,6 +37,7 @@ * * @author Tomaz Fernandes * @author Maciej Walkowiak + * @author Jeongmin Kim */ class SqsHeaderMapperTests { From 787b42dcb88ff7d10cf30ebcb0394565b40b8f9b Mon Sep 17 00:00:00 2001 From: jeongmin Date: Tue, 26 Aug 2025 17:46:19 +0900 Subject: [PATCH 3/6] Generate consistent UUID from Message ID instead of random UUID --- .../support/converter/MessagingMessageHeaders.java | 13 +++++++++++++ .../sqs/support/converter/SqsHeaderMapper.java | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/MessagingMessageHeaders.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/MessagingMessageHeaders.java index 99ad15a0b6..883e419c47 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/MessagingMessageHeaders.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/MessagingMessageHeaders.java @@ -15,6 +15,7 @@ */ package io.awspring.cloud.sqs.support.converter; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.UUID; import org.jspecify.annotations.Nullable; @@ -23,6 +24,7 @@ /** * {@link MessageHeaders} implementation that allows providing an external {@link UUID}. * @author Tomaz Fernandes + * @author Jeongmin Kim * @since 3.0 */ public class MessagingMessageHeaders extends MessageHeaders { @@ -53,4 +55,15 @@ public MessagingMessageHeaders(@Nullable Map headers, @Nullable public MessagingMessageHeaders(@Nullable Map headers, @Nullable UUID id, @Nullable Long timestamp) { super(headers, id, timestamp); } + + /** + * Create an instance with String ID converted to consistent UUID + */ + public MessagingMessageHeaders(@Nullable Map headers, @Nullable String stringId) { + super(headers, stringId != null ? generateConsistentUuid(stringId) : null, null); + } + + private static UUID generateConsistentUuid(String stringId) { + return UUID.nameUUIDFromBytes(stringId.getBytes(StandardCharsets.UTF_8)); + } } diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java index 135bc263e8..529b24116f 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java @@ -173,7 +173,7 @@ public MessageHeaders toHeaders(Message source) { 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()); + return new MessagingMessageHeaders(messageHeaders, source.messageId()); } } From 0eab76dec6c00c94235a8b04515ee9c453ab6d0f Mon Sep 17 00:00:00 2001 From: jeongmin Date: Fri, 20 Feb 2026 21:38:28 +0900 Subject: [PATCH 4/6] Add fail-fast for non-UUID message IDs and handle non-UUID send responses --- .../cloud/sqs/operations/SqsTemplate.java | 59 +++++++++++++------ .../sqs/operations/SqsTemplateParameters.java | 5 ++ .../support/converter/SqsHeaderMapper.java | 17 ++++-- .../sqs/operations/SqsTemplateTests.java | 50 ++++++++++++++++ .../converter/SqsHeaderMapperTests.java | 26 ++++---- 5 files changed, 124 insertions(+), 33 deletions(-) diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java index 3811e4f521..bd790c5cd3 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java @@ -33,6 +33,7 @@ import io.awspring.cloud.sqs.support.converter.SqsMessagingMessageConverter; import io.awspring.cloud.sqs.support.converter.legacy.LegacyJackson2SqsMessagingMessageConverter; import io.awspring.cloud.sqs.support.observation.SqsTemplateObservation; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Collection; import java.util.Collections; @@ -328,16 +329,36 @@ private CompletableFuture handleAutoDeduplication(String endpointName) protected CompletableFuture> doSendAsync(String endpointName, Message message, org.springframework.messaging.Message originalMessage) { return createSendMessageRequest(endpointName, message).thenCompose(this.sqsAsyncClient::sendMessage) - .thenApply(response -> createSendResult(UUID.fromString(response.messageId()), - response.sequenceNumber(), endpointName, originalMessage)); + .thenApply(response -> createSendResult(response.messageId(), response.sequenceNumber(), endpointName, + originalMessage)); } - private SendResult createSendResult(UUID messageId, @Nullable String sequenceNumber, String endpointName, - org.springframework.messaging.Message originalMessage) { + private SendResult createSendResult(String rawMessageId, @Nullable String sequenceNumber, + String endpointName, org.springframework.messaging.Message originalMessage) { + Map additionalInfo = new HashMap<>(); + if (sequenceNumber != null) { + additionalInfo.put(SqsTemplateParameters.SEQUENCE_NUMBER_PARAMETER_NAME, sequenceNumber); + } + UUID messageId; + if (isValidUuid(rawMessageId)) { + messageId = UUID.fromString(rawMessageId); + } + else { + messageId = UUID.nameUUIDFromBytes(rawMessageId.getBytes(StandardCharsets.UTF_8)); + additionalInfo.put(SqsTemplateParameters.RAW_MESSAGE_ID_PARAMETER_NAME, rawMessageId); + } return new SendResult<>(messageId, endpointName, originalMessage, - sequenceNumber != null - ? Collections.singletonMap(SqsTemplateParameters.SEQUENCE_NUMBER_PARAMETER_NAME, sequenceNumber) - : Collections.emptyMap()); + additionalInfo.isEmpty() ? Collections.emptyMap() : additionalInfo); + } + + private static boolean isValidUuid(String value) { + try { + UUID.fromString(value); + return true; + } + catch (IllegalArgumentException e) { + return false; + } } private CompletableFuture createSendMessageRequest(String endpointName, Message message) { @@ -359,8 +380,8 @@ protected CompletableFuture> doSendBatchAsync(String end Collection messages, Collection> originalMessages) { logger.debug("Sending messages {} to endpoint {}", messages, endpointName); return createSendMessageBatchRequest(endpointName, messages).thenCompose(this.sqsAsyncClient::sendMessageBatch) - .thenApply(response -> createSendResultBatch(response, endpointName, - originalMessages.stream().collect(Collectors.toMap(MessageHeaderUtils::getAwsMessageId, msg -> msg)))); + .thenApply(response -> createSendResultBatch(response, endpointName, originalMessages.stream() + .collect(Collectors.toMap(MessageHeaderUtils::getRawMessageId, msg -> msg)))); } private SendResult.Batch createSendResultBatch(SendMessageBatchResponse response, String endpointName, @@ -380,10 +401,8 @@ private Collection> createSendResultFailed(SendMessageB private Collection> doCreateSendResultBatch(SendMessageBatchResponse response, String endpointName, Map> originalMessagesById) { - return response - .successful().stream().map(entry -> createSendResult(UUID.fromString(entry.messageId()), - entry.sequenceNumber(), endpointName, getOriginalMessage(originalMessagesById, entry))) - .toList(); + return response.successful().stream().map(entry -> createSendResult(entry.messageId(), entry.sequenceNumber(), + endpointName, getOriginalMessage(originalMessagesById, entry))).toList(); } private org.springframework.messaging.Message getOriginalMessage( @@ -541,7 +560,7 @@ private Map addMissingFifoReceiveHeaders(Map hea private CompletableFuture deleteMessages(String endpointName, Collection> messages) { logger.trace("Acknowledging in queue {} messages {}", endpointName, - MessageHeaderUtils.getAwsMessageId(addTypeToMessages(messages))); + MessageHeaderUtils.getRawMessageId(addTypeToMessages(messages))); return getQueueAttributes(endpointName) .thenCompose(attributes -> this.sqsAsyncClient.deleteMessageBatch(DeleteMessageBatchRequest.builder() .queueUrl(attributes.getQueueUrl()).entries(createDeleteMessageEntries(messages)).build())) @@ -560,7 +579,8 @@ private Collection> getFailedAckMessage DeleteMessageBatchResponse response, Collection> messages, String endpointName) { return response.failed().stream().map(BatchResultErrorEntry::id) - .map(id -> messages.stream().filter(msg -> MessageHeaderUtils.getAwsMessageId(msg).equals(id)).findFirst() + .map(id -> messages.stream().filter(msg -> MessageHeaderUtils.getRawMessageId(msg).equals(id)) + .findFirst() .orElseThrow(() -> new SqsAcknowledgementException( "Could not correlate ids for acknowledgement failure", Collections.emptyList(), messages, endpointName))) @@ -571,7 +591,8 @@ private Collection> getSuccessfulAckMes DeleteMessageBatchResponse response, Collection> messages, String endpointName) { return response.successful().stream().map(DeleteMessageBatchResultEntry::id) - .map(id -> messages.stream().filter(msg -> MessageHeaderUtils.getAwsMessageId(msg).equals(id)).findFirst() + .map(id -> messages.stream().filter(msg -> MessageHeaderUtils.getRawMessageId(msg).equals(id)) + .findFirst() .orElseThrow(() -> new SqsAcknowledgementException( "Could not correlate ids for acknowledgement failure", Collections.emptyList(), messages, endpointName))) @@ -589,7 +610,7 @@ private void logAcknowledgement(String endpointName, Collection createDeleteMessageEntries( Collection> messages) { return messages.stream() - .map(message -> DeleteMessageBatchRequestEntry.builder().id(MessageHeaderUtils.getAwsMessageId(message)) + .map(message -> DeleteMessageBatchRequestEntry.builder().id(MessageHeaderUtils.getRawMessageId(message)) .receiptHandle( MessageHeaderUtils.getHeaderAsString(message, SqsHeaders.SQS_RECEIPT_HANDLE_HEADER)) .build()) diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplateParameters.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplateParameters.java index 6c3e3662e5..801ad5ce34 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplateParameters.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplateParameters.java @@ -39,4 +39,9 @@ public class SqsTemplateParameters { */ public static final String ERROR_CODE_PARAMETER_NAME = "code"; + /** + * The raw provider message ID when it is not a valid UUID. + */ + public static final String RAW_MESSAGE_ID_PARAMETER_NAME = "rawMessageId"; + } diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java index 529b24116f..fa58a71119 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java @@ -165,12 +165,20 @@ public MessageHeaders toHeaders(Message source) { accessor.copyHeadersIfAbsent(createDefaultHeaders(source)); accessor.copyHeadersIfAbsent(createAdditionalHeaders(source)); - if (convertMessageIdToUuid && isValidUuid(source.messageId())) { + if (convertMessageIdToUuid) { + if (!isValidUuid(source.messageId())) { + 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.", + source.messageId(), SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER)); + } MessageHeaders messageHeaders = accessor.toMessageHeaders(); logger.trace("Mapped headers {} for message {}", messageHeaders, source.messageId()); return new MessagingMessageHeaders(messageHeaders, UUID.fromString(source.messageId())); - } else { - accessor.setHeader(SqsHeaders.SQS_AWS_MESSAGE_ID_HEADER, source.messageId()); + } + else { + accessor.setHeader(SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER, source.messageId()); MessageHeaders messageHeaders = accessor.toMessageHeaders(); logger.trace("Mapped headers {} for message {}", messageHeaders, source.messageId()); return new MessagingMessageHeaders(messageHeaders, source.messageId()); @@ -182,7 +190,8 @@ private boolean isValidUuid(String messageId) { try { UUID.fromString(messageId); return true; - } catch (IllegalArgumentException e) { + } + catch (IllegalArgumentException e) { return false; } } diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/operations/SqsTemplateTests.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/operations/SqsTemplateTests.java index a31d65fe14..bb297c702a 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/operations/SqsTemplateTests.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/operations/SqsTemplateTests.java @@ -1192,6 +1192,56 @@ void shouldReceiveBatchFifo() { } + @Test + void shouldHandleNonUuidMessageIdInSendResponse() { + String queue = "test-queue"; + GetQueueUrlResponse urlResponse = GetQueueUrlResponse.builder().queueUrl(queue).build(); + given(mockClient.getQueueUrl(any(GetQueueUrlRequest.class))) + .willReturn(CompletableFuture.completedFuture(urlResponse)); + mockQueueAttributes(mockClient, Map.of()); + String nonUuidMessageId = "92898073-7bd6a160-5797b060-54a7e539"; + SendMessageResponse response = SendMessageResponse.builder().messageId(nonUuidMessageId).build(); + given(mockClient.sendMessage(any(SendMessageRequest.class))) + .willReturn(CompletableFuture.completedFuture(response)); + SqsOperations template = SqsTemplate.newTemplate(mockClient); + String payload = "test-payload"; + SendResult result = template.send(to -> to.queue(queue).payload(payload)); + assertThat(result.messageId()) + .isEqualTo(UUID.nameUUIDFromBytes(nonUuidMessageId.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + assertThat(result.additionalInformation().get(SqsTemplateParameters.RAW_MESSAGE_ID_PARAMETER_NAME)) + .isEqualTo(nonUuidMessageId); + } + + @Test + void shouldHandleNonUuidMessageIdInBatchSendResponse() { + String queue = "test-queue"; + String payload1 = "test-payload-1"; + String payload2 = "test-payload-2"; + Message message1 = MessageBuilder.withPayload(payload1).build(); + Message message2 = MessageBuilder.withPayload(payload2).build(); + List> messages = List.of(message1, message2); + + GetQueueUrlResponse urlResponse = GetQueueUrlResponse.builder().queueUrl(queue).build(); + given(mockClient.getQueueUrl(any(GetQueueUrlRequest.class))) + .willReturn(CompletableFuture.completedFuture(urlResponse)); + mockQueueAttributes(mockClient, Map.of()); + String nonUuidMessageId1 = "92898073-7bd6a160-5797b060-54a7e539"; + String nonUuidMessageId2 = "a2898073-8bd6a160-6797b060-64a7e539"; + SendMessageBatchResponse response = SendMessageBatchResponse.builder() + .successful( + builder -> builder.id(message1.getHeaders().getId().toString()).messageId(nonUuidMessageId1), + builder -> builder.id(message2.getHeaders().getId().toString()).messageId(nonUuidMessageId2)) + .build(); + given(mockClient.sendMessageBatch(any(SendMessageBatchRequest.class))) + .willReturn(CompletableFuture.completedFuture(response)); + SqsOperations template = SqsTemplate.newSyncTemplate(mockClient); + SendResult.Batch results = template.sendMany(queue, messages); + assertThat(results.successful()).hasSize(2); + results.successful().forEach(result -> { + assertThat(result.additionalInformation()).containsKey(SqsTemplateParameters.RAW_MESSAGE_ID_PARAMETER_NAME); + }); + } + @Test void shouldPropagateTracingAsMessageSystemAttribute() { String queue = "test-queue"; diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java index 278fbe5121..5df507bd59 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java @@ -16,6 +16,7 @@ package io.awspring.cloud.sqs.support.converter; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import io.awspring.cloud.sqs.listener.SqsHeaders; import java.math.BigDecimal; @@ -27,6 +28,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.MessagingException; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.services.sqs.model.Message; import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; @@ -183,13 +185,20 @@ void shouldConvertUuidMessageIdWhenConvertMessageIdToUuidIsTrue() { SqsHeaderMapper mapper = new SqsHeaderMapper(); mapper.setConvertMessageIdToUuid(true); String uuidMessageId = "550e8400-e29b-41d4-a716-446655440000"; - Message message = Message.builder() - .body("payload") - .messageId(uuidMessageId) - .build(); + Message message = Message.builder().body("payload").messageId(uuidMessageId).build(); MessageHeaders headers = mapper.toHeaders(message); assertThat(headers.getId()).isEqualTo(UUID.fromString(uuidMessageId)); - assertThat(headers.get(SqsHeaders.SQS_AWS_MESSAGE_ID_HEADER)).isNull(); + assertThat(headers.get(SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER)).isNull(); + } + + @Test + void shouldThrowWhenConvertMessageIdToUuidIsTrueAndMessageIdIsNotValidUuid() { + SqsHeaderMapper mapper = new SqsHeaderMapper(); + mapper.setConvertMessageIdToUuid(true); + String nonUuidMessageId = "92898073-7bd6a160-5797b060-54a7e539"; + Message message = Message.builder().body("payload").messageId(nonUuidMessageId).build(); + assertThatThrownBy(() -> mapper.toHeaders(message)).isInstanceOf(MessagingException.class) + .hasMessageContaining("not a valid UUID").hasMessageContaining("convert-message-id-to-uuid"); } @Test @@ -197,12 +206,9 @@ void shouldStoreAwsMessageIdInHeaderWhenConvertMessageIdToUuidIsFalse() { SqsHeaderMapper mapper = new SqsHeaderMapper(); mapper.setConvertMessageIdToUuid(false); String nonUuidMessageId = "92898073-7bd6a160-5797b060-54a7e539"; - Message message = Message.builder() - .body("payload") - .messageId(nonUuidMessageId) - .build(); + Message message = Message.builder().body("payload").messageId(nonUuidMessageId).build(); MessageHeaders headers = mapper.toHeaders(message); - assertThat(headers.get(SqsHeaders.SQS_AWS_MESSAGE_ID_HEADER)).isEqualTo(nonUuidMessageId); + assertThat(headers.get(SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER)).isEqualTo(nonUuidMessageId); assertThat(headers.getId()).isNotEqualTo(nonUuidMessageId); assertThat(headers.getId()).isNotNull(); } From dce5a94a825a54901662763c7f88fda33abbb4e3 Mon Sep 17 00:00:00 2001 From: jeongmin Date: Fri, 20 Feb 2026 21:38:52 +0900 Subject: [PATCH 5/6] Add documentation for non-UUID message ID support --- docs/src/main/asciidoc/_configprops.adoc | 1 + docs/src/main/asciidoc/sqs.adoc | 21 +++++++++++++++++++ .../autoconfigure/sqs/SqsProperties.java | 4 ++++ 3 files changed, 26 insertions(+) diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index a3de5dc581..af96fab578 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -94,6 +94,7 @@ |spring.cloud.aws.sns.endpoint | | Overrides the default endpoint. |spring.cloud.aws.sns.region | | Overrides the default region. |spring.cloud.aws.sqs.dualstack-enabled | | Configure whether the AWS client should use the AWS dualstack endpoint. Note that not each AWS service supports dual-stack. For complete list check AWS services that support IPv6 +|spring.cloud.aws.sqs.convert-message-id-to-uuid | `+++true+++` | Whether to convert SQS message IDs to UUIDs. Set to `false` for SQS-compatible providers that return non-UUID message IDs. |spring.cloud.aws.sqs.enabled | `+++true+++` | Enables SQS integration. |spring.cloud.aws.sqs.endpoint | | Overrides the default endpoint. |spring.cloud.aws.sqs.listener.auto-startup | | Defines whether SQS listeners will start automatically or not. diff --git a/docs/src/main/asciidoc/sqs.adoc b/docs/src/main/asciidoc/sqs.adoc index bf120b162b..233bd7555e 100644 --- a/docs/src/main/asciidoc/sqs.adoc +++ b/docs/src/main/asciidoc/sqs.adoc @@ -319,6 +319,27 @@ If `SendBatchFailureStrategy#DO_NOT_THROW` is configured, no exception is thrown For convenience, the `additionalInformation` parameters can be found as constants in the `SqsTemplateParameters` class. +===== Non-UUID Message IDs + +By default, Spring Cloud AWS SQS expects the message ID returned by SQS to be a valid UUID. +If a non-UUID message ID is received, an error is thrown with instructions to enable non-UUID support. + +To enable non-UUID message ID support (e.g., for Yandex Message Queue or other SQS-compatible providers): + +[source,properties] +---- +spring.cloud.aws.sqs.convert-message-id-to-uuid=false +---- + +When disabled: + +* **Receive side**: The raw provider message ID is stored in the `Sqs_RawMessageId` header. + A deterministic UUID derived from the raw ID is used as the Spring `MessageHeaders.ID`. + Access the raw ID via `MessageHeaderUtils.getRawMessageId(message)`. +* **Send side**: If the send response contains a non-UUID message ID, + `SendResult.messageId()` returns a deterministic UUID and the raw ID is available + in `SendResult.additionalInformation()` under the `rawMessageId` key. + [[template-message-conversion]] ==== Template Message Conversion diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java index eba169de3d..fb37f35bb5 100644 --- a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java @@ -52,6 +52,10 @@ public void setListener(Listener listener) { private Boolean observationEnabled = false; + /** + * Whether to convert SQS message IDs to UUIDs. Set to {@code false} for SQS-compatible providers that return + * non-UUID message IDs. + */ private Boolean convertMessageIdToUuid = true; public Boolean getConvertMessageIdToUuid() { From 56cb5ec3aba9d2212801af89d87d478e967e313c Mon Sep 17 00:00:00 2001 From: jeongmin Date: Fri, 20 Feb 2026 21:39:28 +0900 Subject: [PATCH 6/6] Rename AWSMessageId to RawMessageId for consistency --- .../cloud/sqs/MessageHeaderUtils.java | 18 +++++----- .../cloud/sqs/listener/SqsHeaders.java | 4 +-- .../cloud/sqs/MessageHeaderUtilsTest.java | 36 +++++++++---------- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java index 84f32f6015..6f167157cd 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java @@ -153,21 +153,21 @@ public static Message removeHeaderIfPresent(Message message, String ke } /** - * Return the AWS message ID, falling back to Spring message ID if not present. + * Return the raw provider message ID, falling back to Spring message ID if not present. * @param message the message. - * @return the AWS ID or Spring ID. + * @return the raw provider ID or Spring ID. */ - public static String getAwsMessageId(Message message) { - String awsMessageId = message.getHeaders().get(SqsHeaders.SQS_AWS_MESSAGE_ID_HEADER, String.class); - return awsMessageId != null ? awsMessageId : getId(message); + public static String getRawMessageId(Message message) { + String rawMessageId = message.getHeaders().get(SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER, String.class); + return rawMessageId != null ? rawMessageId : getId(message); } /** - * Return the messages' AWS ID as a concatenated {@link String}. + * Return the messages' raw provider IDs as a concatenated {@link String}. * @param messages the messages. - * @return the AWS IDs. + * @return the raw provider IDs. */ - public static String getAwsMessageId(Collection> messages) { - return messages.stream().map(MessageHeaderUtils::getAwsMessageId).collect(Collectors.joining("; ")); + public static String getRawMessageId(Collection> messages) { + return messages.stream().map(MessageHeaderUtils::getRawMessageId).collect(Collectors.joining("; ")); } } diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java index e064a48ae2..87101b1f23 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java @@ -90,9 +90,9 @@ private SqsHeaders() { public static final String SQS_DEFAULT_TYPE_HEADER = "JavaType"; /** - * Header for the original AWS MessageId when not using UUID conversion. + * Header for the raw provider message ID when not using UUID conversion. */ - public static final String SQS_AWS_MESSAGE_ID_HEADER = SQS_HEADER_PREFIX + "AWSMessageId"; + public static final String SQS_RAW_MESSAGE_ID_HEADER = SQS_HEADER_PREFIX + "RawMessageId"; public static class MessageSystemAttributes { diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java index 75bd6712c9..784bbf3012 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java @@ -18,13 +18,12 @@ import static org.assertj.core.api.Assertions.assertThat; import io.awspring.cloud.sqs.listener.SqsHeaders; +import java.util.Collection; +import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; -import java.util.Collection; -import java.util.List; - /** * Tests for {@link MessageHeaderUtils}. * @@ -100,52 +99,49 @@ void shouldPreserveOtherHeaders() { } @Test - void shouldReturnAwsMessageIdWhenHeaderPresent() { + void shouldReturnRawMessageIdWhenHeaderPresent() { // given - String awsMessageId = "92898073-7bd6a160-5797b060-54a7e539"; + String rawMessageId = "92898073-7bd6a160-5797b060-54a7e539"; Message message = MessageBuilder.withPayload("test-payload") - .setHeader(SqsHeaders.SQS_AWS_MESSAGE_ID_HEADER, awsMessageId) - .build(); + .setHeader(SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER, rawMessageId).build(); // when - String result = MessageHeaderUtils.getAwsMessageId(message); + String result = MessageHeaderUtils.getRawMessageId(message); // then - assertThat(result).isEqualTo(awsMessageId); + assertThat(result).isEqualTo(rawMessageId); } @Test - void shouldFallbackToSpringMessageIdWhenAwsHeaderNotPresent() { + void shouldFallbackToSpringMessageIdWhenRawHeaderNotPresent() { // given Message message = MessageBuilder.withPayload("test-payload").build(); String expectedId = message.getHeaders().getId().toString(); // when - String result = MessageHeaderUtils.getAwsMessageId(message); + String result = MessageHeaderUtils.getRawMessageId(message); // then assertThat(result).isEqualTo(expectedId); } @Test - void shouldConcatenateAwsMessageIdsFromCollection() { + void shouldConcatenateRawMessageIdsFromCollection() { // given - String awsMessageId1 = "aws-id-1"; - String awsMessageId2 = "aws-id-2"; + String rawMessageId1 = "raw-id-1"; + String rawMessageId2 = "raw-id-2"; Message message1 = MessageBuilder.withPayload("payload1") - .setHeader(SqsHeaders.SQS_AWS_MESSAGE_ID_HEADER, awsMessageId1) - .build(); + .setHeader(SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER, rawMessageId1).build(); Message message2 = MessageBuilder.withPayload("payload2") - .setHeader(SqsHeaders.SQS_AWS_MESSAGE_ID_HEADER, awsMessageId2) - .build(); + .setHeader(SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER, rawMessageId2).build(); Collection> messages = List.of(message1, message2); // when - String result = MessageHeaderUtils.getAwsMessageId(messages); + String result = MessageHeaderUtils.getRawMessageId(messages); // then - assertThat(result).isEqualTo("aws-id-1; aws-id-2"); + assertThat(result).isEqualTo("raw-id-1; raw-id-2"); } }