Skip to content

[UrlConnectionHttpClient] Wrap erroneous NPE from HttpURLConnection as IOException #2572

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

Merged
merged 4 commits into from
Jul 1, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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-a3c92c2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"category": "AWS SDK for Java v2",
"contributor": "Bennett-Lynch",
"type": "bugfix",
"description": "[UrlConnectionHttpClient] Wrap erroneous NPE from HttpURLConnection as IOException"
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ public HttpExecuteResponse call() throws IOException {
request.contentStreamProvider().ifPresent(provider ->
invokeSafely(() -> IoUtils.copy(provider.newStream(), connection.getOutputStream())));

int responseCode = connection.getResponseCode();
int responseCode = getResponseCodeSafely(connection);
boolean isErrorResponse = HttpStatusFamily.of(responseCode).isOneOf(CLIENT_ERROR, SERVER_ERROR);
InputStream content = !isErrorResponse ? connection.getInputStream() : connection.getErrorStream();
AbortableInputStream responseBody = content != null ?
Expand All @@ -224,6 +224,24 @@ public HttpExecuteResponse call() throws IOException {
.build();
}

/**
* {@link sun.net.www.protocol.http.HttpURLConnection#getInputStream0()} has been observed to intermittently throw
* {@link NullPointerException}s for reasons that still require further investigation, but are assumed to be due to a
* bug in the JDK. Propagating such NPEs is confusing for users and are not subject to being retried on by the default
* retry policy configuration, so instead we bias towards propagating these as {@link IOException}s.
* <p>
* TODO: Determine precise root cause of intermittent NPEs, submit JDK bug report if applicable, and consider applying
* this behavior only on unpatched JVM runtime versions.
*/
private static int getResponseCodeSafely(HttpURLConnection connection) throws IOException {
Validate.paramNotNull(connection, "connection");
try {
return connection.getResponseCode();
} catch (NullPointerException e) {
throw new IOException("Unexpected NullPointerException when trying to read response from HttpURLConnection", e);
}
}

private Map<String, List<String>> extractHeaders(HttpURLConnection response) {
return response.getHeaderFields().entrySet().stream()
.filter(e -> e.getKey() != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,30 @@
*/
package software.amazon.awssdk.http.urlconnection;

import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.spy;
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
import static software.amazon.awssdk.utils.FunctionalUtils.safeFunction;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.function.Function;
import org.junit.Ignore;
import org.junit.Test;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.SdkHttpClientTestSuite;

public final class UrlConnectionHttpClientWithCustomCreateWireMockTest extends SdkHttpClientTestSuite {

private Function<HttpURLConnection, HttpURLConnection> connectionInterceptor = Function.identity();

@Override
protected SdkHttpClient createSdkHttpClient(SdkHttpClientOptions options) {
return UrlConnectionHttpClient.create((uri) -> invokeSafely(() -> (HttpURLConnection) uri.toURL().openConnection()));
return UrlConnectionHttpClient.create(uri -> invokeSafely(() -> {
HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
return connectionInterceptor.apply(connection);
}));
}

@Ignore // Not supported when using custom factory
Expand All @@ -41,4 +54,18 @@ public void testTrustAllWorks() {
@Override
public void testCustomTlsTrustManagerAndTrustAllFails() {
}

@Test
public void testGetResponseCodeNpeIsWrappedAsIo() throws Exception {
connectionInterceptor = safeFunction(connection -> {
connection = spy(connection);
doThrow(new NullPointerException()).when(connection).getResponseCode();
return connection;
});

assertThatThrownBy(() -> testForResponseCode(HttpURLConnection.HTTP_OK))
.isInstanceOf(IOException.class)
.hasMessage("Unexpected NullPointerException when trying to read response from HttpURLConnection")
.hasCauseInstanceOf(NullPointerException.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ public void testCustomTlsTrustManagerAndTrustAllFails() throws Exception {
assertThatThrownBy(() -> createSdkHttpClient(httpClientOptions)).isInstanceOf(IllegalArgumentException.class);
}

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

Expand Down