Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix ApacheHttpClient's handling of request bodies on DELETE, GET, HEAD & OPTIONS requests #5704

Merged
merged 5 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/bugfix-AWSSDKforJavav2-c212259.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "bugfix",
"category": "AWS SDK for Java v2",
"contributor": "Xtansia",
"description": "Fix ApacheHttpClient's handling of request bodies on DELETE, GET, HEAD & OPTIONS requests"
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,7 @@
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.http.HttpExecuteRequest;
Expand Down Expand Up @@ -116,23 +109,18 @@ private void addRequestConfig(final HttpRequestBase base,


private HttpRequestBase createApacheRequest(HttpExecuteRequest request, URI uri) {
switch (request.httpRequest().method()) {
SdkHttpMethod method = request.httpRequest().method();
switch (method) {
case HEAD:
return new HttpHead(uri);
case GET:
return new HttpGet(uri);
case DELETE:
return new HttpDelete(uri);
case OPTIONS:
return new HttpOptions(uri);
davidh44 marked this conversation as resolved.
Show resolved Hide resolved
case PATCH:
return wrapEntity(request, new HttpPatch(uri));
case POST:
return wrapEntity(request, new HttpPost(uri));
case PUT:
return wrapEntity(request, new HttpPut(uri));
return wrapEntity(request, new HttpRequestImpl(method, uri));
default:
throw new RuntimeException("Unknown HTTP method name: " + request.httpRequest().method());
throw new RuntimeException("Unknown HTTP method name: " + method);
}
}

Expand Down Expand Up @@ -192,4 +180,18 @@ private String getHostHeaderValue(SdkHttpRequest request) {
? request.host() + ":" + request.port()
: request.host();
}

private static final class HttpRequestImpl extends HttpEntityEnclosingRequestBase {
private final SdkHttpMethod method;

private HttpRequestImpl(SdkHttpMethod method, URI uri) {
this.method = method;
setURI(uri);
}

@Override
public String getMethod() {
return this.method.toString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@

package software.amazon.awssdk.http.urlconnection;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.net.ProtocolException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import org.junit.Test;
import org.junit.jupiter.api.AfterEach;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.SdkHttpClientDefaultTestSuite;
import software.amazon.awssdk.http.SdkHttpMethod;

public class UrlConnectionHttpClientDefaultWireMockTest extends SdkHttpClientDefaultTestSuite {

Expand All @@ -32,4 +37,20 @@ protected SdkHttpClient createSdkHttpClient() {
public void reset() {
HttpsURLConnection.setDefaultSSLSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault());
}

@Test
@Override
public void supportsRequestBodyOnGetRequest() throws Exception {
// HttpURLConnection is hard-coded to switch GET requests with a body to POST requests, in #getOutputStream0.
testForResponseCode(200, SdkHttpMethod.GET, SdkHttpMethod.POST, true);
}

@Test
@Override
public void supportsRequestBodyOnPatchRequest() {
// HttpURLConnection does not support PATCH requests.
assertThatThrownBy(super::supportsRequestBodyOnPatchRequest)
.hasRootCauseInstanceOf(ProtocolException.class)
.hasRootCauseMessage("Invalid HTTP method: PATCH");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public void supportsResponseCode200() throws Exception {
@Test
public void supportsResponseCode200Head() throws Exception {
// HEAD is special due to closing of the connection immediately and streams are null
testForResponseCode(HttpURLConnection.HTTP_FORBIDDEN, SdkHttpMethod.HEAD);
testForResponseCode(HttpURLConnection.HTTP_FORBIDDEN, SdkHttpMethod.HEAD, false);
}

@Test
Expand All @@ -76,7 +76,7 @@ public void supportsResponseCode403() throws Exception {

@Test
public void supportsResponseCode403Head() throws Exception {
testForResponseCode(HttpURLConnection.HTTP_FORBIDDEN, SdkHttpMethod.HEAD);
testForResponseCode(HttpURLConnection.HTTP_FORBIDDEN, SdkHttpMethod.HEAD, false);
}

@Test
Expand All @@ -98,45 +98,87 @@ public void supportsResponseCode500() throws Exception {
public void validatesHttpsCertificateIssuer() {
SdkHttpClient client = createSdkHttpClient();

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

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

@Test
public void supportsRequestBodyOnGetRequest() throws Exception {
testForResponseCode(200, SdkHttpMethod.GET, true);
}

@Test
public void supportsRequestBodyOnPostRequest() throws Exception {
testForResponseCode(200, SdkHttpMethod.POST, true);
}

@Test
public void supportsRequestBodyOnPutRequest() throws Exception {
testForResponseCode(200, SdkHttpMethod.PUT, true);
}

@Test
public void supportsRequestBodyOnDeleteRequest() throws Exception {
testForResponseCode(200, SdkHttpMethod.DELETE, true);
}

@Test
public void supportsRequestBodyOnHeadRequest() throws Exception {
testForResponseCode(200, SdkHttpMethod.HEAD, true);
}

@Test
public void supportsRequestBodyOnPatchRequest() throws Exception {
testForResponseCode(200, SdkHttpMethod.PATCH, true);
}

@Test
public void supportsRequestBodyOnOptionsRequest() throws Exception {
testForResponseCode(200, SdkHttpMethod.OPTIONS, true);
}

private void testForResponseCode(int returnCode) throws Exception {
testForResponseCode(returnCode, SdkHttpMethod.POST);
testForResponseCode(returnCode, SdkHttpMethod.POST, true);
}

private void testForResponseCode(int returnCode, SdkHttpMethod method) throws Exception {
protected void testForResponseCode(int returnCode, SdkHttpMethod method, boolean includeBody) throws Exception {
testForResponseCode(returnCode, method, method, includeBody);
}

protected void testForResponseCode(int returnCode,
SdkHttpMethod method,
SdkHttpMethod expectedMethod,
boolean includeBody) throws Exception {
SdkHttpClient client = createSdkHttpClient();

stubForMockRequest(returnCode);

SdkHttpFullRequest req = mockSdkRequest("http://localhost:" + mockServer.port(), method);
SdkHttpFullRequest req = mockSdkRequest("http://localhost:" + mockServer.port(), method, includeBody);
HttpExecuteResponse rsp = client.prepareRequest(HttpExecuteRequest.builder()
.request(req)
.contentStreamProvider(req.contentStreamProvider()
.orElse(null))
.build())
.call();

validateResponse(rsp, returnCode, method);
validateResponse(rsp, returnCode, expectedMethod, includeBody);
}

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

SdkHttpFullRequest req = mockSdkRequest("https://localhost:" + mockServer.httpsPort(), sdkHttpMethod);
SdkHttpFullRequest req = mockSdkRequest("https://localhost:" + mockServer.httpsPort(), sdkHttpMethod, true);
HttpExecuteResponse rsp = client.prepareRequest(HttpExecuteRequest.builder()
.request(req)
.contentStreamProvider(req.contentStreamProvider()
.orElse(null))
.build())
.call();

validateResponse(rsp, returnCode, sdkHttpMethod);
validateResponse(rsp, returnCode, sdkHttpMethod, true);
}

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

private void validateResponse(HttpExecuteResponse response, int returnCode, SdkHttpMethod method) throws IOException {
private void validateResponse(HttpExecuteResponse response,
int returnCode,
SdkHttpMethod method,
boolean expectBody) throws IOException {
RequestMethod requestMethod = RequestMethod.fromString(method.name());

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

if (method == SdkHttpMethod.HEAD) {
patternBuilder.withRequestBody(absent());
} else {
if (expectBody) {
patternBuilder.withRequestBody(equalTo("Body"));
} else {
patternBuilder.withRequestBody(absent());
}

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

private SdkHttpFullRequest mockSdkRequest(String uriString, SdkHttpMethod method) {
private SdkHttpFullRequest mockSdkRequest(String uriString, SdkHttpMethod method, boolean includeBody) {
URI uri = URI.create(uriString);
SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder()
.uri(uri)
.method(method)
.putHeader("Host", uri.getHost())
.putHeader("User-Agent", "hello-world!");
if (method != SdkHttpMethod.HEAD) {
if (includeBody) {
byte[] content = "Body".getBytes(StandardCharsets.UTF_8);
requestBuilder.putHeader("Content-Length", Integer.toString(content.length));
requestBuilder.contentStreamProvider(() -> new ByteArrayInputStream(content));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,11 @@
"uri": "/no-payload",
"method": "GET",
"headers": {
"contains": {
"Content-Length": "0"
},
"doesNotContain": [
"Content-Type",
"Content-Length"
"Content-Type"
]
},
"body": {
Expand All @@ -258,11 +260,11 @@
"method": "GET",
"headers": {
"contains": {
"Content-Length": "0",
"x-amz-test-id": "t-12345"
},
"doesNotContain": [
"Content-Type",
"Content-Length"
"Content-Type"
]
},
"body": {
Expand Down
Loading