Skip to content

Commit b2fcb7e

Browse files
authored
Fix ApacheHttpClient's handling of request bodies on DELETE, GET, HEAD & OPTIONS requests (aws#5704)
* Add tests * Fix ApacheHttpClient's handling of request bodies on DELETE, GET, HEAD & OPTIONS requests * Fix style * Handle HttpURLConnection switching GET->POST and not supporting PATCH * Update protocol test
1 parent 1509f58 commit b2fcb7e

File tree

5 files changed

+111
-35
lines changed

5 files changed

+111
-35
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "Xtansia",
5+
"description": "Fix ApacheHttpClient's handling of request bodies on DELETE, GET, HEAD & OPTIONS requests"
6+
}

http-clients/apache-client/src/main/java/software/amazon/awssdk/http/apache/internal/impl/ApacheHttpRequestFactory.java

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,7 @@
2424
import org.apache.http.HttpEntity;
2525
import org.apache.http.HttpHeaders;
2626
import org.apache.http.client.config.RequestConfig;
27-
import org.apache.http.client.methods.HttpDelete;
2827
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
29-
import org.apache.http.client.methods.HttpGet;
30-
import org.apache.http.client.methods.HttpHead;
31-
import org.apache.http.client.methods.HttpOptions;
32-
import org.apache.http.client.methods.HttpPatch;
33-
import org.apache.http.client.methods.HttpPost;
34-
import org.apache.http.client.methods.HttpPut;
3528
import org.apache.http.client.methods.HttpRequestBase;
3629
import software.amazon.awssdk.annotations.SdkInternalApi;
3730
import software.amazon.awssdk.http.HttpExecuteRequest;
@@ -116,23 +109,18 @@ private void addRequestConfig(final HttpRequestBase base,
116109

117110

118111
private HttpRequestBase createApacheRequest(HttpExecuteRequest request, URI uri) {
119-
switch (request.httpRequest().method()) {
112+
SdkHttpMethod method = request.httpRequest().method();
113+
switch (method) {
120114
case HEAD:
121-
return new HttpHead(uri);
122115
case GET:
123-
return new HttpGet(uri);
124116
case DELETE:
125-
return new HttpDelete(uri);
126117
case OPTIONS:
127-
return new HttpOptions(uri);
128118
case PATCH:
129-
return wrapEntity(request, new HttpPatch(uri));
130119
case POST:
131-
return wrapEntity(request, new HttpPost(uri));
132120
case PUT:
133-
return wrapEntity(request, new HttpPut(uri));
121+
return wrapEntity(request, new HttpRequestImpl(method, uri));
134122
default:
135-
throw new RuntimeException("Unknown HTTP method name: " + request.httpRequest().method());
123+
throw new RuntimeException("Unknown HTTP method name: " + method);
136124
}
137125
}
138126

@@ -192,4 +180,18 @@ private String getHostHeaderValue(SdkHttpRequest request) {
192180
? request.host() + ":" + request.port()
193181
: request.host();
194182
}
183+
184+
private static final class HttpRequestImpl extends HttpEntityEnclosingRequestBase {
185+
private final SdkHttpMethod method;
186+
187+
private HttpRequestImpl(SdkHttpMethod method, URI uri) {
188+
this.method = method;
189+
setURI(uri);
190+
}
191+
192+
@Override
193+
public String getMethod() {
194+
return this.method.toString();
195+
}
196+
}
195197
}

http-clients/url-connection-client/src/test/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClientDefaultWireMockTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@
1515

1616
package software.amazon.awssdk.http.urlconnection;
1717

18+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
19+
20+
import java.net.ProtocolException;
1821
import javax.net.ssl.HttpsURLConnection;
1922
import javax.net.ssl.SSLSocketFactory;
23+
import org.junit.Test;
2024
import org.junit.jupiter.api.AfterEach;
2125
import software.amazon.awssdk.http.SdkHttpClient;
2226
import software.amazon.awssdk.http.SdkHttpClientDefaultTestSuite;
27+
import software.amazon.awssdk.http.SdkHttpMethod;
2328

2429
public class UrlConnectionHttpClientDefaultWireMockTest extends SdkHttpClientDefaultTestSuite {
2530

@@ -32,4 +37,20 @@ protected SdkHttpClient createSdkHttpClient() {
3237
public void reset() {
3338
HttpsURLConnection.setDefaultSSLSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault());
3439
}
40+
41+
@Test
42+
@Override
43+
public void supportsRequestBodyOnGetRequest() throws Exception {
44+
// HttpURLConnection is hard-coded to switch GET requests with a body to POST requests, in #getOutputStream0.
45+
testForResponseCode(200, SdkHttpMethod.GET, SdkHttpMethod.POST, true);
46+
}
47+
48+
@Test
49+
@Override
50+
public void supportsRequestBodyOnPatchRequest() {
51+
// HttpURLConnection does not support PATCH requests.
52+
assertThatThrownBy(super::supportsRequestBodyOnPatchRequest)
53+
.hasRootCauseInstanceOf(ProtocolException.class)
54+
.hasRootCauseMessage("Invalid HTTP method: PATCH");
55+
}
3556
}

test/http-client-tests/src/main/java/software/amazon/awssdk/http/SdkHttpClientDefaultTestSuite.java

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public void supportsResponseCode200() throws Exception {
6161
@Test
6262
public void supportsResponseCode200Head() throws Exception {
6363
// HEAD is special due to closing of the connection immediately and streams are null
64-
testForResponseCode(HttpURLConnection.HTTP_FORBIDDEN, SdkHttpMethod.HEAD);
64+
testForResponseCode(HttpURLConnection.HTTP_FORBIDDEN, SdkHttpMethod.HEAD, false);
6565
}
6666

6767
@Test
@@ -76,7 +76,7 @@ public void supportsResponseCode403() throws Exception {
7676

7777
@Test
7878
public void supportsResponseCode403Head() throws Exception {
79-
testForResponseCode(HttpURLConnection.HTTP_FORBIDDEN, SdkHttpMethod.HEAD);
79+
testForResponseCode(HttpURLConnection.HTTP_FORBIDDEN, SdkHttpMethod.HEAD, false);
8080
}
8181

8282
@Test
@@ -98,45 +98,87 @@ public void supportsResponseCode500() throws Exception {
9898
public void validatesHttpsCertificateIssuer() {
9999
SdkHttpClient client = createSdkHttpClient();
100100

101-
SdkHttpFullRequest request = mockSdkRequest("https://localhost:" + mockServer.httpsPort(), SdkHttpMethod.POST);
101+
SdkHttpFullRequest request = mockSdkRequest("https://localhost:" + mockServer.httpsPort(), SdkHttpMethod.POST, true);
102102

103103
assertThatThrownBy(client.prepareRequest(HttpExecuteRequest.builder().request(request).build())::call)
104104
.isInstanceOf(SSLHandshakeException.class);
105105
}
106106

107+
@Test
108+
public void supportsRequestBodyOnGetRequest() throws Exception {
109+
testForResponseCode(200, SdkHttpMethod.GET, true);
110+
}
111+
112+
@Test
113+
public void supportsRequestBodyOnPostRequest() throws Exception {
114+
testForResponseCode(200, SdkHttpMethod.POST, true);
115+
}
116+
117+
@Test
118+
public void supportsRequestBodyOnPutRequest() throws Exception {
119+
testForResponseCode(200, SdkHttpMethod.PUT, true);
120+
}
121+
122+
@Test
123+
public void supportsRequestBodyOnDeleteRequest() throws Exception {
124+
testForResponseCode(200, SdkHttpMethod.DELETE, true);
125+
}
126+
127+
@Test
128+
public void supportsRequestBodyOnHeadRequest() throws Exception {
129+
testForResponseCode(200, SdkHttpMethod.HEAD, true);
130+
}
131+
132+
@Test
133+
public void supportsRequestBodyOnPatchRequest() throws Exception {
134+
testForResponseCode(200, SdkHttpMethod.PATCH, true);
135+
}
136+
137+
@Test
138+
public void supportsRequestBodyOnOptionsRequest() throws Exception {
139+
testForResponseCode(200, SdkHttpMethod.OPTIONS, true);
140+
}
141+
107142
private void testForResponseCode(int returnCode) throws Exception {
108-
testForResponseCode(returnCode, SdkHttpMethod.POST);
143+
testForResponseCode(returnCode, SdkHttpMethod.POST, true);
109144
}
110145

111-
private void testForResponseCode(int returnCode, SdkHttpMethod method) throws Exception {
146+
protected void testForResponseCode(int returnCode, SdkHttpMethod method, boolean includeBody) throws Exception {
147+
testForResponseCode(returnCode, method, method, includeBody);
148+
}
149+
150+
protected void testForResponseCode(int returnCode,
151+
SdkHttpMethod method,
152+
SdkHttpMethod expectedMethod,
153+
boolean includeBody) throws Exception {
112154
SdkHttpClient client = createSdkHttpClient();
113155

114156
stubForMockRequest(returnCode);
115157

116-
SdkHttpFullRequest req = mockSdkRequest("http://localhost:" + mockServer.port(), method);
158+
SdkHttpFullRequest req = mockSdkRequest("http://localhost:" + mockServer.port(), method, includeBody);
117159
HttpExecuteResponse rsp = client.prepareRequest(HttpExecuteRequest.builder()
118160
.request(req)
119161
.contentStreamProvider(req.contentStreamProvider()
120162
.orElse(null))
121163
.build())
122164
.call();
123165

124-
validateResponse(rsp, returnCode, method);
166+
validateResponse(rsp, returnCode, expectedMethod, includeBody);
125167
}
126168

127169
protected void testForResponseCodeUsingHttps(SdkHttpClient client, int returnCode) throws Exception {
128170
SdkHttpMethod sdkHttpMethod = SdkHttpMethod.POST;
129171
stubForMockRequest(returnCode);
130172

131-
SdkHttpFullRequest req = mockSdkRequest("https://localhost:" + mockServer.httpsPort(), sdkHttpMethod);
173+
SdkHttpFullRequest req = mockSdkRequest("https://localhost:" + mockServer.httpsPort(), sdkHttpMethod, true);
132174
HttpExecuteResponse rsp = client.prepareRequest(HttpExecuteRequest.builder()
133175
.request(req)
134176
.contentStreamProvider(req.contentStreamProvider()
135177
.orElse(null))
136178
.build())
137179
.call();
138180

139-
validateResponse(rsp, returnCode, sdkHttpMethod);
181+
validateResponse(rsp, returnCode, sdkHttpMethod, true);
140182
}
141183

142184
private void stubForMockRequest(int returnCode) {
@@ -151,17 +193,20 @@ private void stubForMockRequest(int returnCode) {
151193
mockServer.stubFor(any(urlPathEqualTo("/")).willReturn(responseBuilder));
152194
}
153195

154-
private void validateResponse(HttpExecuteResponse response, int returnCode, SdkHttpMethod method) throws IOException {
196+
private void validateResponse(HttpExecuteResponse response,
197+
int returnCode,
198+
SdkHttpMethod method,
199+
boolean expectBody) throws IOException {
155200
RequestMethod requestMethod = RequestMethod.fromString(method.name());
156201

157202
RequestPatternBuilder patternBuilder = RequestPatternBuilder.newRequestPattern(requestMethod, urlMatching("/"))
158203
.withHeader("Host", containing("localhost"))
159204
.withHeader("User-Agent", equalTo("hello-world!"));
160205

161-
if (method == SdkHttpMethod.HEAD) {
162-
patternBuilder.withRequestBody(absent());
163-
} else {
206+
if (expectBody) {
164207
patternBuilder.withRequestBody(equalTo("Body"));
208+
} else {
209+
patternBuilder.withRequestBody(absent());
165210
}
166211

167212
mockServer.verify(1, patternBuilder);
@@ -177,14 +222,14 @@ private void validateResponse(HttpExecuteResponse response, int returnCode, SdkH
177222
mockServer.resetMappings();
178223
}
179224

180-
private SdkHttpFullRequest mockSdkRequest(String uriString, SdkHttpMethod method) {
225+
private SdkHttpFullRequest mockSdkRequest(String uriString, SdkHttpMethod method, boolean includeBody) {
181226
URI uri = URI.create(uriString);
182227
SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder()
183228
.uri(uri)
184229
.method(method)
185230
.putHeader("Host", uri.getHost())
186231
.putHeader("User-Agent", "hello-world!");
187-
if (method != SdkHttpMethod.HEAD) {
232+
if (includeBody) {
188233
byte[] content = "Body".getBytes(StandardCharsets.UTF_8);
189234
requestBuilder.putHeader("Content-Length", Integer.toString(content.length));
190235
requestBuilder.contentStreamProvider(() -> new ByteArrayInputStream(content));

test/protocol-tests-core/src/main/resources/software/amazon/awssdk/protocol/suites/cases/rest-json-contenttype.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,11 @@
230230
"uri": "/no-payload",
231231
"method": "GET",
232232
"headers": {
233+
"contains": {
234+
"Content-Length": "0"
235+
},
233236
"doesNotContain": [
234-
"Content-Type",
235-
"Content-Length"
237+
"Content-Type"
236238
]
237239
},
238240
"body": {
@@ -258,11 +260,11 @@
258260
"method": "GET",
259261
"headers": {
260262
"contains": {
263+
"Content-Length": "0",
261264
"x-amz-test-id": "t-12345"
262265
},
263266
"doesNotContain": [
264-
"Content-Type",
265-
"Content-Length"
267+
"Content-Type"
266268
]
267269
},
268270
"body": {

0 commit comments

Comments
 (0)