Skip to content
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-AWSCRTHTTPClient-1ad80f1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "bugfix",
"category": "AWS CRT HTTP Client",
"contributor": "",
"description": "Preserve the underlying CRT HttpException (including the CRT error code) as the cause of the SSLHandshakeException and ConnectException surfaced for TLS negotiation failures and socket timeouts, so callers can differentiate transient failures from persistent ones by inspecting the exception cause chain."
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,9 @@
<!-- ResponseHandlerHelper has helper method closeConnection() which handles safe closing of connection -->
<suppress id="NoCrtStreamCancel"
files=".*ResponseHandlerHelper\.java$"/>

<!-- Shared HTTP client test suites live in src/main/java so client modules can consume them;
allow test-style underscore method names. -->
<suppress checks="MethodName"
files=".*[\\/]http-client-tests[\\/]src[\\/]main[\\/]java[\\/].*TestSuite\.java$"/>
</suppressions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.http.apache;

import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.SdkHttpClientSslHandshakeBehaviorTestSuite;

public class ApacheSslHandshakeBehaviorTest extends SdkHttpClientSslHandshakeBehaviorTestSuite {
@Override
protected SdkHttpClient createSdkHttpClient() {
return ApacheHttpClient.builder().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.http.apache5;

import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.SdkHttpClientSslHandshakeBehaviorTestSuite;

public class Apache5SslHandshakeBehaviorTest extends SdkHttpClientSslHandshakeBehaviorTestSuite {
@Override
protected SdkHttpClient createSdkHttpClient() {
return Apache5HttpClient.builder().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,15 @@ public static Throwable wrapCrtException(Throwable throwable) {
int httpErrorCode = httpException.getErrorCode();

if (httpErrorCode == CRT_TLS_NEGOTIATION_ERROR_CODE) {
return new SSLHandshakeException(httpException.getMessage());
SSLHandshakeException sslHandshakeException = new SSLHandshakeException(httpException.getMessage());
sslHandshakeException.initCause(httpException);
return sslHandshakeException;
}

if (httpErrorCode == CRT_SOCKET_TIMEOUT) {
return new ConnectException(httpException.getMessage());
ConnectException connectException = new ConnectException(httpException.getMessage());
connectException.initCause(httpException);
return connectException;
}

return wrapWithIoExceptionIfRetryable((HttpException) throwable);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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.http.crt;

import java.time.Duration;
import software.amazon.awssdk.http.SdkAsyncHttpClientSslHandshakeBehaviorTestSuite;
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;

public class CrtAsyncSslHandshakeBehaviorTest extends SdkAsyncHttpClientSslHandshakeBehaviorTestSuite {
@Override
protected SdkAsyncHttpClient createSdkAsyncHttpClient() {
// CRT negotiates TLS during connection acquisition; the default 10s acquisition timeout can
// expire first and surface an acquisition-timeout exception instead of SSLHandshakeException.
return AwsCrtAsyncHttpClient.builder()
.connectionAcquisitionTimeout(Duration.ofSeconds(30))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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.http.crt;

import java.time.Duration;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.SdkHttpClientSslHandshakeBehaviorTestSuite;

public class CrtSslHandshakeBehaviorTest extends SdkHttpClientSslHandshakeBehaviorTestSuite {
@Override
protected SdkHttpClient createSdkHttpClient() {
// CRT negotiates TLS during connection acquisition; the default 10s acquisition timeout can
// expire first and surface an acquisition-timeout exception instead of SSLHandshakeException.
return AwsCrtHttpClient.builder()
.connectionAcquisitionTimeout(Duration.ofSeconds(30))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ public void execute_httpException_mapsToCorrectException(Entry<Integer, Class<?
.thenReturn(completableFuture);

CompletableFuture<Void> executeFuture = requestExecutor.execute(context);
assertThatThrownBy(executeFuture::join).hasCauseInstanceOf(expectedExceptionClass);
assertThatThrownBy(executeFuture::join).hasCauseInstanceOf(expectedExceptionClass)
.hasCauseInstanceOf(IOException.class)
.hasRootCause(exception);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ public void execute_httpException_mapsToCorrectException(Entry<Integer, Class<?
.thenReturn(completableFuture);

CompletableFuture<SdkHttpFullResponse> executeFuture = requestExecutor.execute(context);
assertThatThrownBy(executeFuture::join).hasCauseInstanceOf(expectedExceptionClass);
assertThatThrownBy(executeFuture::join).hasCauseInstanceOf(expectedExceptionClass)
.hasCauseInstanceOf(IOException.class)
.hasRootCause(exception);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.net.ConnectException;
import javax.net.ssl.SSLHandshakeException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import software.amazon.awssdk.crt.http.HttpException;
Expand Down Expand Up @@ -70,4 +73,37 @@ public void wrapWithIoExceptionIfRetryable_nonRetryableCode_returnsHttpException

assertThat(result).isSameAs(httpException);
}

/**
* TLS negotiation failures must chain the original {@link HttpException} (with its CRT error code)
* as cause, so callers can differentiate transient from persistent failures.
*/
@Test
public void wrapCrtException_tlsNegotiationError_wrapsInSslHandshakeExceptionWithCause() {
HttpException httpException = new HttpException(CrtUtils.CRT_TLS_NEGOTIATION_ERROR_CODE);

Throwable result = CrtUtils.wrapCrtException(httpException);

assertThat(result).isInstanceOf(SSLHandshakeException.class)
.hasMessage(httpException.getMessage())
.hasCause(httpException);
assertThat(((HttpException) result.getCause()).getErrorCode())
.isEqualTo(CrtUtils.CRT_TLS_NEGOTIATION_ERROR_CODE);
}

/**
* Socket timeouts must chain the original {@link HttpException} as cause.
*/
@Test
public void wrapCrtException_socketTimeout_wrapsInConnectExceptionWithCause() {
HttpException httpException = new HttpException(CrtUtils.CRT_SOCKET_TIMEOUT);

Throwable result = CrtUtils.wrapCrtException(httpException);

assertThat(result).isInstanceOf(ConnectException.class)
.hasMessage(httpException.getMessage())
.hasCause(httpException);
assertThat(((HttpException) result.getCause()).getErrorCode())
.isEqualTo(CrtUtils.CRT_SOCKET_TIMEOUT);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.http.nio.netty;

import software.amazon.awssdk.http.SdkAsyncHttpClientSslHandshakeBehaviorTestSuite;
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;

public class NettySslHandshakeBehaviorTest extends SdkAsyncHttpClientSslHandshakeBehaviorTestSuite {
@Override
protected SdkAsyncHttpClient createSdkAsyncHttpClient() {
return NettyNioAsyncHttpClient.builder().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.http.urlconnection;

import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.SdkHttpClientSslHandshakeBehaviorTestSuite;

public class UrlConnectionSslHandshakeBehaviorTest extends SdkHttpClientSslHandshakeBehaviorTestSuite {
@Override
protected SdkHttpClient createSdkHttpClient() {
return UrlConnectionHttpClient.builder().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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.http;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;

import com.github.tomakehurst.wiremock.WireMockServer;
import java.util.concurrent.CompletionException;
import javax.net.ssl.SSLHandshakeException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;

/**
* Validates that a TLS handshake failure against an untrusted (self-signed) certificate surfaces as
* {@link SSLHandshakeException} with a cause chain, so callers can differentiate transient
* interruptions from persistent certificate failures.
*/
public abstract class SdkAsyncHttpClientSslHandshakeBehaviorTestSuite {
private WireMockServer selfSignedServer;

protected abstract SdkAsyncHttpClient createSdkAsyncHttpClient();

@BeforeEach
public void setup() {
selfSignedServer = HttpTestUtils.createSelfSignedServer();
selfSignedServer.start();
}

@AfterEach
public void teardown() {
if (selfSignedServer != null) {
selfSignedServer.stop();
selfSignedServer = null;
}
}

@Test
public void sslHandshakeFailure_surfacesAsSslHandshakeException() {
Throwable thrown = executeRequestAgainstUntrustedServer();

assertThat(thrown).isInstanceOf(SSLHandshakeException.class);
}

@Test
public void sslHandshakeFailure_exceptionCarriesDiagnosableCause() {
Throwable thrown = executeRequestAgainstUntrustedServer();

assertThat(thrown).isInstanceOf(SSLHandshakeException.class);
assertThat(thrown.getCause())
.withFailMessage("SSLHandshakeException must chain the underlying failure as its cause "
+ "so callers can differentiate transient from persistent TLS failures. Was: %s", thrown)
.isNotNull();
}

private Throwable executeRequestAgainstUntrustedServer() {
try (SdkAsyncHttpClient client = createSdkAsyncHttpClient()) {
Throwable thrown = catchThrowable(
() -> HttpTestUtils.sendGetRequest(selfSignedServer.httpsPort(), client, true).join());
Comment thread
alextwoods marked this conversation as resolved.
return thrown instanceof CompletionException ? thrown.getCause() : thrown;
}
}
}
Loading
Loading