diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-4fd89f7.json b/.changes/next-release/bugfix-AWSSDKforJavav2-4fd89f7.json new file mode 100644 index 000000000000..6b7d01cfafdb --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-4fd89f7.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Fixed an issue where NPE would be thrown if there was an empty event in the input for an event streaming operation." +} diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/handler/AwsClientHandlerUtils.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/handler/AwsClientHandlerUtils.java index d195e09774e8..f69b79469cce 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/handler/AwsClientHandlerUtils.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/handler/AwsClientHandlerUtils.java @@ -45,13 +45,15 @@ public static ByteBuffer encodeEventStreamRequestToByteBuffer(SdkHttpFullRequest Map headers = new LinkedHashMap<>(); request.forEachHeader((name, value) -> headers.put(name, HeaderValue.fromString(firstIfPresent(value)))); - byte[] payload = null; + byte[] payload; if (request.contentStreamProvider().isPresent()) { try { payload = IoUtils.toByteArray(request.contentStreamProvider().get().newStream()); } catch (IOException e) { throw new UncheckedIOException(e); } + } else { + payload = new byte[0]; } return new Message(headers, payload).toByteBuffer(); diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/handler/AwsClientHandlerUtilsTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/handler/AwsClientHandlerUtilsTest.java new file mode 100644 index 000000000000..4f3636c34cb9 --- /dev/null +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/handler/AwsClientHandlerUtilsTest.java @@ -0,0 +1,48 @@ +/* + * 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.awscore.client.handler; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.nio.ByteBuffer; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.utils.StringInputStream; + +public class AwsClientHandlerUtilsTest { + + @Test + void nonNullPayload_shouldEncodeToEmptyMessage() { + SdkHttpFullRequest request = SdkHttpFullRequest.builder() + .method(SdkHttpMethod.GET) + .uri(URI.create("http://localhost")) + .contentStreamProvider(() -> new StringInputStream("test")) + .build(); + ByteBuffer buffer = AwsClientHandlerUtils.encodeEventStreamRequestToByteBuffer(request); + assertThat(buffer).isNotNull(); + } + + @Test + void nullPayload_shouldEncodeToEmptyMessage() { + SdkHttpFullRequest request = SdkHttpFullRequest.builder() + .method(SdkHttpMethod.GET) + .uri(URI.create("http://localhost")).build(); + ByteBuffer buffer = AwsClientHandlerUtils.encodeEventStreamRequestToByteBuffer(request); + assertThat(buffer).isNotNull(); + } +} diff --git a/test/protocol-tests/src/main/resources/codegen-resources/restjson/contenttype/service-2.json b/test/protocol-tests/src/main/resources/codegen-resources/restjson/contenttype/service-2.json index 65d3bc977488..04415d6a3f00 100644 --- a/test/protocol-tests/src/main/resources/codegen-resources/restjson/contenttype/service-2.json +++ b/test/protocol-tests/src/main/resources/codegen-resources/restjson/contenttype/service-2.json @@ -223,6 +223,9 @@ "HeadersOnlyEvent": { "shape": "HeadersOnlyEvent" }, + "EndEvent": { + "shape": "EndEvent" + }, "ImplicitPayloadAndHeadersEvent": { "shape": "ImplicitPayloadAndHeadersEvent" } @@ -280,6 +283,12 @@ }, "event": true }, + "EndEvent":{ + "type":"structure", + "members":{ + }, + "event":true + }, "BlobPayloadMember":{"type":"blob"}, "EventStream": { "type": "structure", diff --git a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/RestJsonEventStreamProtocolTest.java b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/RestJsonEventStreamProtocolTest.java index 53a19f98f5ae..0b2ddebb80e8 100644 --- a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/RestJsonEventStreamProtocolTest.java +++ b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/RestJsonEventStreamProtocolTest.java @@ -15,10 +15,22 @@ package software.amazon.awssdk.protocol.tests; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.in; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import io.reactivex.Flowable; import java.net.URI; import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; @@ -27,6 +39,7 @@ import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.protocols.json.AwsJsonProtocol; import software.amazon.awssdk.protocols.json.AwsJsonProtocolFactory; +import software.amazon.awssdk.services.protocolrestjsoncontenttype.ProtocolRestJsonContentTypeAsyncClient; import software.amazon.awssdk.services.protocolrestjsoncontenttype.model.BlobAndHeadersEvent; import software.amazon.awssdk.services.protocolrestjsoncontenttype.model.HeadersOnlyEvent; import software.amazon.awssdk.services.protocolrestjsoncontenttype.model.ImplicitPayloadAndHeadersEvent; @@ -38,9 +51,19 @@ import software.amazon.awssdk.services.protocolrestjsoncontenttype.transform.ImplicitPayloadAndHeadersEventMarshaller; import software.amazon.awssdk.services.protocolrestjsoncontenttype.transform.StringAndHeadersEventMarshaller; +@WireMockTest public class RestJsonEventStreamProtocolTest { private static final String EVENT_CONTENT_TYPE_HEADER = ":content-type"; + private ProtocolRestJsonContentTypeAsyncClient client; + + @BeforeEach + void setup(WireMockRuntimeInfo info) { + client = ProtocolRestJsonContentTypeAsyncClient.builder() + .endpointOverride(URI.create("http://localhost:" + info.getHttpPort())) + .build(); + } + @Test public void implicitPayloadAndHeaders_payloadMemberPresent() { ImplicitPayloadAndHeadersEventMarshaller marshaller = new ImplicitPayloadAndHeadersEventMarshaller(protocolFactory()); @@ -91,6 +114,18 @@ public void blobAndHeadersEvent() { assertThat(content).isEqualTo("hello rest-json"); } + @Test + public void containsEmptyEvent_shouldEncodeSuccessfully() { + stubFor(any(anyUrl()).willReturn(aResponse().withStatus(200))); + client.testEventStream(b -> { + }, Flowable.fromArray(InputEventStream.stringAndHeadersEventBuilder().stringPayloadMember( + "test").build(), + InputEventStream.endEventBuilder().build())).join(); + + verify(postRequestedFor(anyUrl()) + .withHeader("Content-Type", equalTo("application/vnd.amazon.eventstream"))); + } + @Test public void stringAndHeadersEvent() { StringAndHeadersEventMarshaller marshaller = new StringAndHeadersEventMarshaller(protocolFactory());