Skip to content

Commit 3170d9f

Browse files
authored
Support zero-length chunked-encoded responses from XML services. (#2964)
The current code currently only considers responses with a content-length of zero as being empty. That isn't sufficient for chunk-encoded responses, which do not have a content-length. This change updates the logic to instead read a single byte off of the response stream to see if it's empty. This works for both zero content-length and empty chunked encoding streams.
1 parent 6b4dace commit 3170d9f

File tree

3 files changed

+82
-16
lines changed

3 files changed

+82
-16
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"contributor": "",
4+
"type": "bugfix",
5+
"description": "Do not fail with a parsing error when receiving 0-length chunk-encoded responses for XML services."
6+
}

core/protocols/aws-xml-protocol/src/main/java/software/amazon/awssdk/protocols/xml/internal/unmarshall/XmlResponseParserUtils.java

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515

1616
package software.amazon.awssdk.protocols.xml.internal.unmarshall;
1717

18-
import static software.amazon.awssdk.http.Header.CONTENT_LENGTH;
19-
18+
import java.io.BufferedInputStream;
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.io.UncheckedIOException;
2022
import java.util.Optional;
2123
import software.amazon.awssdk.annotations.SdkInternalApi;
2224
import software.amazon.awssdk.core.SdkField;
@@ -45,27 +47,42 @@ private XmlResponseParserUtils() {
4547
* @return A parsed XML document or an empty XML document if no payload/contents were found in the response.
4648
*/
4749
public static XmlElement parse(SdkPojo sdkPojo, SdkHttpFullResponse response) {
48-
4950
try {
5051
Optional<AbortableInputStream> responseContent = response.content();
5152

52-
// In some cases the responseContent is present but empty, so when we are not expecting a body we should
53-
// not attempt to parse it even if the body appears to be present.
54-
if ((!response.isSuccessful() || hasPayloadMembers(sdkPojo)) && responseContent.isPresent() &&
55-
!contentLengthZero(response) && !getBlobTypePayloadMemberToUnmarshal(sdkPojo).isPresent()) {
56-
return XmlDomParser.parse(responseContent.get());
57-
} else {
53+
if (!responseContent.isPresent() ||
54+
(response.isSuccessful() && !hasPayloadMembers(sdkPojo)) ||
55+
getBlobTypePayloadMemberToUnmarshal(sdkPojo).isPresent()) {
56+
return XmlElement.empty();
57+
}
58+
59+
// Make sure there is content in the stream before passing it to the parser.
60+
InputStream content = ensureMarkSupported(responseContent.get());
61+
content.mark(2);
62+
if (content.read() == -1) {
5863
return XmlElement.empty();
5964
}
65+
content.reset();
66+
67+
return XmlDomParser.parse(content);
68+
} catch (IOException e) {
69+
throw new UncheckedIOException(e);
6070
} catch (RuntimeException e) {
6171
if (response.isSuccessful()) {
6272
throw e;
6373
}
64-
6574
return XmlElement.empty();
6675
}
6776
}
6877

78+
private static InputStream ensureMarkSupported(AbortableInputStream content) {
79+
if (content.markSupported()) {
80+
return content;
81+
}
82+
83+
return new BufferedInputStream(content);
84+
}
85+
6986
/**
7087
* Gets the Member which is a Payload and which is of Blob Type.
7188
* @param sdkPojo
@@ -85,10 +102,4 @@ private static boolean hasPayloadMembers(SdkPojo sdkPojo) {
85102
return sdkPojo.sdkFields().stream()
86103
.anyMatch(f -> f.location() == MarshallLocation.PAYLOAD);
87104
}
88-
89-
private static boolean contentLengthZero(SdkHttpFullResponse response) {
90-
return response.firstMatchingHeader(CONTENT_LENGTH).map(l -> Long.parseLong(l) == 0).orElse(false);
91-
}
92-
93-
94105
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.services.s3.functionaltests;
17+
18+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
19+
import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
20+
import static com.github.tomakehurst.wiremock.client.WireMock.get;
21+
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
22+
23+
import com.github.tomakehurst.wiremock.junit.WireMockRule;
24+
import java.net.URI;
25+
import org.junit.Rule;
26+
import org.junit.Test;
27+
import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider;
28+
import software.amazon.awssdk.regions.Region;
29+
import software.amazon.awssdk.services.s3.S3Client;
30+
31+
public class EmptyResponseTest {
32+
@Rule
33+
public WireMockRule mockServer = new WireMockRule(0);
34+
35+
@Test
36+
public void emptyChunkedEncodingResponseWorks() {
37+
stubFor(get(anyUrl())
38+
.willReturn(aResponse().withStatus(200)
39+
.withHeader("Transfer-Encoding", "chunked")));
40+
41+
S3Client client = S3Client.builder()
42+
.endpointOverride(URI.create("http://localhost:" + mockServer.port()))
43+
.region(Region.US_WEST_2)
44+
.credentialsProvider(AnonymousCredentialsProvider.create())
45+
.build();
46+
47+
client.listBuckets(); // Should not fail
48+
}
49+
}

0 commit comments

Comments
 (0)