From 337a82b911adfe3092ea233697a50f592c58c2f8 Mon Sep 17 00:00:00 2001 From: anuragg-saxenaa Date: Sun, 12 Apr 2026 12:00:52 -0400 Subject: [PATCH 1/2] fix: prevent NPE in validateDouble/validateFloat when input is null (#6639) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Double.isNaN() and Double.isFinite() accept primitive double — Java auto-unboxes the wrapper Double, which throws NullPointerException if the wrapper is null. This crash surfaces when a Map or List stored in DynamoDB contains an explicit null value. Add a null guard at the top of both validateDouble() and validateFloat() that returns early; null is a valid DynamoDB NULL attribute value and the converters already handle null separately via transformFrom/transformTo. Also add ConverterUtilsTest covering null, NaN, infinite, and finite inputs for both methods. Fixes #6639 Signed-off-by: anuragg-saxenaa --- .../internal/converter/ConverterUtils.java | 10 ++- .../converter/ConverterUtilsTest.java | 63 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtilsTest.java diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java index 0f6f20583187..663a03249cb2 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtils.java @@ -36,18 +36,26 @@ private ConverterUtils() { /** * Validates that a given Double input is a valid double supported by {@link DoubleAttributeConverter}. + * Null values are accepted (null will be stored as NULL in DynamoDB and round-trips correctly). * @param input */ public static void validateDouble(Double input) { + if (input == null) { + return; + } Validate.isTrue(!Double.isNaN(input), "NaN is not supported by the default converters."); Validate.isTrue(Double.isFinite(input), "Infinite numbers are not supported by the default converters."); } /** - * Validates that a given Float input is a valid double supported by {@link FloatAttributeConverter}. + * Validates that a given Float input is a valid float supported by {@link FloatAttributeConverter}. + * Null values are accepted (null will be stored as NULL in DynamoDB and round-trips correctly). * @param input */ public static void validateFloat(Float input) { + if (input == null) { + return; + } Validate.isTrue(!Float.isNaN(input), "NaN is not supported by the default converters."); Validate.isTrue(Float.isFinite(input), "Infinite numbers are not supported by the default converters."); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtilsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtilsTest.java new file mode 100644 index 000000000000..901d1ff11fd2 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/converter/ConverterUtilsTest.java @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.internal.converter; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ConverterUtilsTest { + + @Test + void validateDouble_nullDoesNotThrow() { + assertThatCode(() -> ConverterUtils.validateDouble(null)) + .doesNotThrow(); + } + + @Test + void validateDouble_finiteValueDoesNotThrow() { + assertThatCode(() -> ConverterUtils.validateDouble(3.14)) + .doesNotThrow(); + } + + @Test + void validateDouble_nanThrows() { + assertThatThrownBy(() -> ConverterUtils.validateDouble(Double.NaN)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("NaN"); + } + + @Test + void validateDouble_infiniteThrows() { + assertThatThrownBy(() -> ConverterUtils.validateDouble(Double.POSITIVE_INFINITY)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Infinite"); + } + + @Test + void validateFloat_nullDoesNotThrow() { + assertThatCode(() -> ConverterUtils.validateFloat(null)) + .doesNotThrow(); + } + + @Test + void validateFloat_nanThrows() { + assertThatThrownBy(() -> ConverterUtils.validateFloat(Float.NaN)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("NaN"); + } +} From 124bde141f79ecabb0540e64ee9aeb3a52851863 Mon Sep 17 00:00:00 2001 From: anuragg-saxenaa Date: Sun, 12 Apr 2026 12:05:21 -0400 Subject: [PATCH 2/2] fix: catch UncheckedIOException in RetryableStage to allow retry (#6578) RetryableStage caught SdkException and IOException but not UncheckedIOException (which extends RuntimeException, not IOException). When the HTTP client throws UncheckedIOException, it bypassed the catch block entirely and was never offered to the retry strategy predicates, so retryOnException(UncheckedIOException.class) had no effect. Add UncheckedIOException to the multi-catch so it is forwarded to RetryableStageHelper.tryRefreshToken and evaluated against user-supplied retry predicates like any other retriable exception. Fixes #6578 Signed-off-by: anuragg-saxenaa --- .../core/internal/http/pipeline/stages/RetryableStage.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/RetryableStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/RetryableStage.java index a545cb4b088e..7482b0c63f6d 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/RetryableStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/RetryableStage.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.core.internal.http.pipeline.stages; import java.io.IOException; +import java.io.UncheckedIOException; import java.time.Duration; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -56,7 +57,7 @@ public Response execute(SdkHttpFullRequest request, RequestExecutionCon Response response = executeRequest(retryableStageHelper, context); retryableStageHelper.recordAttemptSucceeded(); return response; - } catch (SdkExceptionWithRetryAfterHint | SdkException | IOException e) { + } catch (SdkExceptionWithRetryAfterHint | SdkException | IOException | UncheckedIOException e) { Throwable throwable = e; if (e instanceof SdkExceptionWithRetryAfterHint) { SdkExceptionWithRetryAfterHint wrapper = (SdkExceptionWithRetryAfterHint) e;