Skip to content

Commit 46e49ba

Browse files
Used retry hint from CRT before converting HTTPException to IOException. (#3575)
* Used retry hint from CRT before converting HTTPException to IOException. (#3540) * Used retry hint from CRT before converting HTTPException to IOException. * Add branch for handling http exceptions in CrtRequestExecutor. Updated test to use a mock http request that doesn't require a builder. * Fix checkstyle errors and test * Add more tests and fix existing tests Co-authored-by: Jonathan M. Henson <[email protected]>
1 parent ad97cfb commit 46e49ba

File tree

6 files changed

+169
-44
lines changed

6 files changed

+169
-44
lines changed

http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/CrtRequestExecutor.java

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import software.amazon.awssdk.crt.CrtRuntimeException;
2929
import software.amazon.awssdk.crt.http.HttpClientConnection;
3030
import software.amazon.awssdk.crt.http.HttpClientConnectionManager;
31+
import software.amazon.awssdk.crt.http.HttpException;
3132
import software.amazon.awssdk.crt.http.HttpManagerMetrics;
3233
import software.amazon.awssdk.crt.http.HttpRequest;
3334
import software.amazon.awssdk.crt.http.HttpStreamResponseHandler;
@@ -84,18 +85,7 @@ public CompletableFuture<Void> execute(CrtRequestContext executionContext) {
8485
metricCollector.reportMetric(CONCURRENCY_ACQUIRE_DURATION, acquireTimeTaken);
8586
}
8687

87-
HttpRequest crtRequest = CrtRequestAdapter.toCrtRequest(executionContext);
88-
HttpStreamResponseHandler crtResponseHandler =
89-
CrtResponseAdapter.toCrtResponseHandler(crtConn, requestFuture, asyncRequest.responseHandler());
90-
91-
// Submit the request on the connection
92-
try {
93-
crtConn.makeRequest(crtRequest, crtResponseHandler).activate();
94-
} catch (IllegalStateException | CrtRuntimeException e) {
95-
reportFailure(new IOException("An exception occurred when making the request", e),
96-
requestFuture,
97-
asyncRequest.responseHandler());
98-
}
88+
executeRequest(executionContext, requestFuture, crtConn, asyncRequest);
9989
});
10090

10191
requestFuture.whenComplete((obj, err) -> {
@@ -113,6 +103,35 @@ public CompletableFuture<Void> execute(CrtRequestContext executionContext) {
113103
return requestFuture;
114104
}
115105

106+
private void executeRequest(CrtRequestContext executionContext,
107+
CompletableFuture<Void> requestFuture,
108+
HttpClientConnection crtConn,
109+
AsyncExecuteRequest asyncRequest) {
110+
HttpRequest crtRequest = CrtRequestAdapter.toCrtRequest(executionContext);
111+
HttpStreamResponseHandler crtResponseHandler =
112+
CrtResponseAdapter.toCrtResponseHandler(crtConn, requestFuture, asyncRequest.responseHandler());
113+
114+
// Submit the request on the connection
115+
try {
116+
crtConn.makeRequest(crtRequest, crtResponseHandler).activate();
117+
} catch (HttpException e) {
118+
Throwable toThrow = e;
119+
if (HttpClientConnection.isErrorRetryable(e)) {
120+
// IOExceptions get retried, and if the CRT says this error is retryable,
121+
// it's semantically an IOException anyway.
122+
toThrow = new IOException(e);
123+
}
124+
reportFailure(toThrow,
125+
requestFuture,
126+
asyncRequest.responseHandler());
127+
} catch (IllegalStateException | CrtRuntimeException e) {
128+
// CRT throws IllegalStateException if the connection is closed
129+
reportFailure(new IOException("An exception occurred when making the request", e),
130+
requestFuture,
131+
asyncRequest.responseHandler());
132+
}
133+
}
134+
116135
/**
117136
* Create the execution future and set up the cancellation logic.
118137
* @return The created execution future.
@@ -137,7 +156,7 @@ private CompletableFuture<Void> createExecutionFuture(AsyncExecuteRequest reques
137156
/**
138157
* Notify the provided response handler and future of the failure.
139158
*/
140-
private void reportFailure(IOException cause,
159+
private void reportFailure(Throwable cause,
141160
CompletableFuture<Void> executeFuture,
142161
SdkAsyncHttpResponseHandler responseHandler) {
143162
try {

http-clients/aws-crt-client/src/main/java/software/amazon/awssdk/http/crt/internal/response/CrtResponseAdapter.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,16 @@ private void onSuccessfulResponseComplete(HttpStream stream) {
129129
private void onFailedResponseComplete(HttpStream stream, HttpException error) {
130130
log.debug(() -> "HTTP response encountered an error.", error);
131131

132-
IOException wrappedError = new IOException(error);
133-
responsePublisher.error(wrappedError);
134-
failResponseHandlerAndFuture(stream, wrappedError);
132+
Throwable toThrow = error;
133+
134+
if (HttpClientConnection.isErrorRetryable(error)) {
135+
// IOExceptions get retried, and if the CRT says this error is retryable,
136+
// it's semantically an IOException anyway.
137+
toThrow = new IOException(error);
138+
}
139+
140+
responsePublisher.error(toThrow);
141+
failResponseHandlerAndFuture(stream, toThrow);
135142
}
136143

137144
private void failResponseHandlerAndFuture(HttpStream stream, Throwable error) {

http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientSpiVerificationTest.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
import java.net.URI;
3636
import java.nio.ByteBuffer;
3737
import java.time.Duration;
38+
import java.util.Collections;
39+
import java.util.HashMap;
40+
import java.util.LinkedList;
41+
import java.util.List;
42+
import java.util.Map;
3843
import java.util.Random;
3944
import java.util.concurrent.CompletableFuture;
4045
import java.util.concurrent.TimeUnit;
@@ -48,8 +53,6 @@
4853
import org.reactivestreams.Subscription;
4954
import software.amazon.awssdk.crt.CrtResource;
5055
import software.amazon.awssdk.crt.http.HttpException;
51-
import software.amazon.awssdk.crt.io.EventLoopGroup;
52-
import software.amazon.awssdk.crt.io.HostResolver;
5356
import software.amazon.awssdk.http.RecordingResponseHandler;
5457
import software.amazon.awssdk.http.SdkHttpMethod;
5558
import software.amazon.awssdk.http.SdkHttpRequest;
@@ -72,8 +75,6 @@ public class AwsCrtHttpClientSpiVerificationTest {
7275

7376
@BeforeClass
7477
public static void setup() throws Exception {
75-
CrtResource.waitForNoResources();
76-
7778
client = AwsCrtAsyncHttpClient.builder()
7879
.connectionHealthChecksConfiguration(b -> b.minThroughputInBytesPerSecond(4068L)
7980
.allowableThroughputFailureInterval(Duration.ofSeconds(3)))
@@ -83,8 +84,6 @@ public static void setup() throws Exception {
8384
@AfterClass
8485
public static void tearDown() {
8586
client.close();
86-
EventLoopGroup.closeStaticDefault();
87-
HostResolver.closeStaticDefault();
8887
CrtResource.waitForNoResources();
8988
}
9089

@@ -132,6 +131,28 @@ public void requestFailed_connectionTimeout_shouldWrapException() {
132131
}
133132
}
134133

134+
@Test
135+
public void requestFailed_notRetryable_shouldNotWrapException() {
136+
try (SdkAsyncHttpClient client = AwsCrtAsyncHttpClient.builder().build()) {
137+
URI uri = URI.create("http://localhost:" + mockServer.port());
138+
// make it invalid by doing a non-zero content length with no request body...
139+
Map<String, List<String>> headers = new HashMap<>();
140+
headers.put("host", Collections.singletonList(uri.getHost()));
141+
142+
List<String> contentLengthValues = new LinkedList<>();
143+
contentLengthValues.add("1");
144+
headers.put("content-length", contentLengthValues);
145+
146+
SdkHttpRequest request = createRequest(uri).toBuilder().headers(headers).build();
147+
148+
RecordingResponseHandler recorder = new RecordingResponseHandler();
149+
client.execute(AsyncExecuteRequest.builder().request(request).requestContentPublisher(new EmptyPublisher()).responseHandler(recorder).build());
150+
// invalid request should have returned an HttpException and not an IOException.
151+
assertThatThrownBy(() -> recorder.completeFuture().get(5, TimeUnit.SECONDS))
152+
.hasCauseInstanceOf(HttpException.class).hasMessageContaining("does not match the previously declared length");
153+
}
154+
}
155+
135156
@Test
136157
public void callsOnStreamForEmptyResponseContent() throws Exception {
137158
stubFor(any(urlEqualTo("/")).willReturn(aResponse().withStatus(204).withHeader("foo", "bar")));

http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/AwsCrtHttpClientWireMockTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public class AwsCrtHttpClientWireMockTest {
4848

4949
@BeforeClass
5050
public static void setup() {
51-
System.setProperty("aws.crt.debugnative", "true");
51+
System.setProperty("aws.crt.debugnative", "false");
5252
}
5353

5454
@AfterClass

http-clients/aws-crt-client/src/test/java/software/amazon/awssdk/http/crt/internal/CrtRequestExecutorTest.java

Lines changed: 98 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package software.amazon.awssdk.http.crt.internal;
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1920
import static software.amazon.awssdk.http.HttpTestUtils.createProvider;
2021
import static software.amazon.awssdk.http.crt.CrtHttpClientTestUtils.createRequest;
2122

@@ -33,12 +34,14 @@
3334
import software.amazon.awssdk.crt.CrtRuntimeException;
3435
import software.amazon.awssdk.crt.http.HttpClientConnection;
3536
import software.amazon.awssdk.crt.http.HttpClientConnectionManager;
37+
import software.amazon.awssdk.crt.http.HttpException;
3638
import software.amazon.awssdk.crt.http.HttpRequest;
3739
import software.amazon.awssdk.http.SdkCancellationException;
3840
import software.amazon.awssdk.http.SdkHttpFullRequest;
3941
import software.amazon.awssdk.http.async.AsyncExecuteRequest;
4042
import software.amazon.awssdk.http.async.SdkAsyncHttpResponseHandler;
4143
import software.amazon.awssdk.http.crt.internal.response.CrtResponseAdapter;
44+
import software.amazon.awssdk.utils.CompletableFutureUtils;
4245

4346
@RunWith(MockitoJUnitRunner.class)
4447
public class CrtRequestExecutorTest {
@@ -91,16 +94,7 @@ public void acquireConnectionThrowException_shouldInvokeOnError() {
9194
@Test
9295
public void makeRequestThrowException_shouldInvokeOnError() {
9396
CrtRuntimeException exception = new CrtRuntimeException("");
94-
SdkHttpFullRequest request = createRequest(URI.create("http://localhost"));
95-
CrtRequestContext context = CrtRequestContext.builder()
96-
.readBufferSize(2000)
97-
.crtConnPool(connectionManager)
98-
.request(AsyncExecuteRequest.builder()
99-
.request(request)
100-
.requestContentPublisher(createProvider(""))
101-
.responseHandler(responseHandler)
102-
.build())
103-
.build();
97+
CrtRequestContext context = crtRequestContext();
10498
CompletableFuture<HttpClientConnection> completableFuture = new CompletableFuture<>();
10599

106100
Mockito.when(connectionManager.acquireConnection()).thenReturn(completableFuture);
@@ -122,16 +116,7 @@ public void makeRequestThrowException_shouldInvokeOnError() {
122116

123117
@Test
124118
public void makeRequest_success() {
125-
SdkHttpFullRequest request = createRequest(URI.create("http://localhost"));
126-
CrtRequestContext context = CrtRequestContext.builder()
127-
.readBufferSize(2000)
128-
.crtConnPool(connectionManager)
129-
.request(AsyncExecuteRequest.builder()
130-
.request(request)
131-
.requestContentPublisher(createProvider(""))
132-
.responseHandler(responseHandler)
133-
.build())
134-
.build();
119+
CrtRequestContext context = crtRequestContext();
135120
CompletableFuture<HttpClientConnection> completableFuture = new CompletableFuture<>();
136121
Mockito.when(connectionManager.acquireConnection()).thenReturn(completableFuture);
137122
completableFuture.complete(httpClientConnection);
@@ -162,4 +147,97 @@ public void cancelRequest_shouldInvokeOnError() {
162147
assertThat(actualException).hasMessageContaining("The request was cancelled");
163148
assertThat(actualException).isInstanceOf(SdkCancellationException.class);
164149
}
150+
151+
@Test
152+
public void execute_AcquireConnectionFailure_shouldAlwaysWrapIOException() {
153+
CrtRequestContext context = crtRequestContext();
154+
RuntimeException exception = new RuntimeException("some failure");
155+
CompletableFuture<HttpClientConnection> completableFuture = CompletableFutureUtils.failedFuture(exception);
156+
157+
Mockito.when(connectionManager.acquireConnection()).thenReturn(completableFuture);
158+
159+
CompletableFuture<Void> executeFuture = requestExecutor.execute(context);
160+
assertThatThrownBy(executeFuture::join).hasCauseInstanceOf(IOException.class).hasRootCause(exception);
161+
}
162+
163+
@Test
164+
public void executeRequest_failedOfIllegalStateException_shouldWrapIOException() {
165+
IllegalStateException exception = new IllegalStateException("connection closed");
166+
CrtRequestContext context = crtRequestContext();
167+
CompletableFuture<HttpClientConnection> completableFuture = new CompletableFuture<>();
168+
169+
Mockito.when(connectionManager.acquireConnection()).thenReturn(completableFuture);
170+
completableFuture.complete(httpClientConnection);
171+
172+
Mockito.when(httpClientConnection.makeRequest(Mockito.any(HttpRequest.class), Mockito.any(CrtResponseAdapter.class)))
173+
.thenThrow(exception);
174+
175+
CompletableFuture<Void> executeFuture = requestExecutor.execute(context);
176+
177+
ArgumentCaptor<Exception> argumentCaptor = ArgumentCaptor.forClass(Exception.class);
178+
Mockito.verify(responseHandler).onError(argumentCaptor.capture());
179+
180+
Exception actualException = argumentCaptor.getValue();
181+
assertThat(actualException).hasMessageContaining("An exception occurred when making the request").hasCause(exception);
182+
assertThatThrownBy(executeFuture::join).hasCauseInstanceOf(IOException.class).hasRootCause(exception);
183+
}
184+
185+
@Test
186+
public void executeRequest_failedOfRetryableHttpException_shouldWrapIOException() {
187+
HttpException exception = new HttpException(0x080a); // AWS_ERROR_HTTP_CONNECTION_CLOSED
188+
CrtRequestContext context = crtRequestContext();
189+
CompletableFuture<HttpClientConnection> completableFuture = new CompletableFuture<>();
190+
191+
Mockito.when(connectionManager.acquireConnection()).thenReturn(completableFuture);
192+
completableFuture.complete(httpClientConnection);
193+
194+
Mockito.when(httpClientConnection.makeRequest(Mockito.any(HttpRequest.class), Mockito.any(CrtResponseAdapter.class)))
195+
.thenThrow(exception);
196+
197+
CompletableFuture<Void> executeFuture = requestExecutor.execute(context);
198+
199+
ArgumentCaptor<Exception> argumentCaptor = ArgumentCaptor.forClass(Exception.class);
200+
Mockito.verify(responseHandler).onError(argumentCaptor.capture());
201+
202+
Exception actualException = argumentCaptor.getValue();
203+
assertThat(actualException).hasCause(exception);
204+
assertThatThrownBy(executeFuture::join).hasCauseInstanceOf(IOException.class).hasRootCause(exception);
205+
}
206+
207+
208+
@Test
209+
public void executeRequest_failedOfNonRetryableHttpException_shouldNotWrapIOException() {
210+
HttpException exception = new HttpException(0x0801); // AWS_ERROR_HTTP_HEADER_NOT_FOUND
211+
CrtRequestContext context = crtRequestContext();
212+
CompletableFuture<HttpClientConnection> completableFuture = new CompletableFuture<>();
213+
214+
Mockito.when(connectionManager.acquireConnection()).thenReturn(completableFuture);
215+
completableFuture.complete(httpClientConnection);
216+
217+
Mockito.when(httpClientConnection.makeRequest(Mockito.any(HttpRequest.class), Mockito.any(CrtResponseAdapter.class)))
218+
.thenThrow(exception);
219+
220+
CompletableFuture<Void> executeFuture = requestExecutor.execute(context);
221+
222+
ArgumentCaptor<Exception> argumentCaptor = ArgumentCaptor.forClass(Exception.class);
223+
Mockito.verify(responseHandler).onError(argumentCaptor.capture());
224+
225+
Exception actualException = argumentCaptor.getValue();
226+
assertThat(actualException).isEqualTo(exception);
227+
assertThatThrownBy(executeFuture::join).hasCause(exception);
228+
}
229+
230+
private CrtRequestContext crtRequestContext() {
231+
SdkHttpFullRequest request = createRequest(URI.create("http://localhost"));
232+
return CrtRequestContext.builder()
233+
.readBufferSize(2000)
234+
.crtConnPool(connectionManager)
235+
.request(AsyncExecuteRequest.builder()
236+
.request(request)
237+
.requestContentPublisher(createProvider(""))
238+
.responseHandler(responseHandler)
239+
.build())
240+
.build();
241+
}
242+
165243
}

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
<rxjava.version>2.2.21</rxjava.version>
115115
<commons-codec.verion>1.10</commons-codec.verion>
116116
<jmh.version>1.29</jmh.version>
117-
<awscrt.version>0.19.6</awscrt.version>
117+
<awscrt.version>0.19.9</awscrt.version>
118118

119119
<!--Test dependencies -->
120120
<junit5.version>5.8.1</junit5.version>

0 commit comments

Comments
 (0)