Skip to content

Commit 1a87ffd

Browse files
[UrlConnectionHttpClient] Wrap erroneous NPE from HttpURLConnection as IOException (#2572)
* [UrlConnectionHttpClient] Wrap erroneous NPE from HttpURLConnection as IOException ## Motivation `HttpURLConnection#getInputStream0` has been observed to intermittently throw `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 should bias towards propagating these as `IOException`s. ## Modifications * When interacting specifically with HttpURLConnection#getResponseCode, wrap any unexpected NullPointerExceptions as IOException * Add corresponding unit test ## Result * Transient errors will be retried and be less likely to surface to a user's application layer
1 parent 6cb36e1 commit 1a87ffd

File tree

4 files changed

+54
-3
lines changed

4 files changed

+54
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"contributor": "Bennett-Lynch",
4+
"type": "bugfix",
5+
"description": "[UrlConnectionHttpClient] Wrap erroneous NPE from HttpURLConnection as IOException"
6+
}

http-clients/url-connection-client/src/main/java/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClient.java

+19-1
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ public HttpExecuteResponse call() throws IOException {
207207
request.contentStreamProvider().ifPresent(provider ->
208208
invokeSafely(() -> IoUtils.copy(provider.newStream(), connection.getOutputStream())));
209209

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

227+
/**
228+
* {@link sun.net.www.protocol.http.HttpURLConnection#getInputStream0()} has been observed to intermittently throw
229+
* {@link NullPointerException}s for reasons that still require further investigation, but are assumed to be due to a
230+
* bug in the JDK. Propagating such NPEs is confusing for users and are not subject to being retried on by the default
231+
* retry policy configuration, so instead we bias towards propagating these as {@link IOException}s.
232+
* <p>
233+
* TODO: Determine precise root cause of intermittent NPEs, submit JDK bug report if applicable, and consider applying
234+
* this behavior only on unpatched JVM runtime versions.
235+
*/
236+
private static int getResponseCodeSafely(HttpURLConnection connection) throws IOException {
237+
Validate.paramNotNull(connection, "connection");
238+
try {
239+
return connection.getResponseCode();
240+
} catch (NullPointerException e) {
241+
throw new IOException("Unexpected NullPointerException when trying to read response from HttpURLConnection", e);
242+
}
243+
}
244+
227245
private Map<String, List<String>> extractHeaders(HttpURLConnection response) {
228246
return response.getHeaderFields().entrySet().stream()
229247
.filter(e -> e.getKey() != null)

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

+28-1
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,30 @@
1414
*/
1515
package software.amazon.awssdk.http.urlconnection;
1616

17+
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
18+
import static org.mockito.Mockito.doThrow;
19+
import static org.mockito.Mockito.spy;
1720
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
21+
import static software.amazon.awssdk.utils.FunctionalUtils.safeFunction;
1822

23+
import java.io.IOException;
1924
import java.net.HttpURLConnection;
25+
import java.util.function.Function;
2026
import org.junit.Ignore;
27+
import org.junit.Test;
2128
import software.amazon.awssdk.http.SdkHttpClient;
2229
import software.amazon.awssdk.http.SdkHttpClientTestSuite;
2330

2431
public final class UrlConnectionHttpClientWithCustomCreateWireMockTest extends SdkHttpClientTestSuite {
32+
33+
private Function<HttpURLConnection, HttpURLConnection> connectionInterceptor = Function.identity();
34+
2535
@Override
2636
protected SdkHttpClient createSdkHttpClient(SdkHttpClientOptions options) {
27-
return UrlConnectionHttpClient.create((uri) -> invokeSafely(() -> (HttpURLConnection) uri.toURL().openConnection()));
37+
return UrlConnectionHttpClient.create(uri -> invokeSafely(() -> {
38+
HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
39+
return connectionInterceptor.apply(connection);
40+
}));
2841
}
2942

3043
@Ignore // Not supported when using custom factory
@@ -41,4 +54,18 @@ public void testTrustAllWorks() {
4154
@Override
4255
public void testCustomTlsTrustManagerAndTrustAllFails() {
4356
}
57+
58+
@Test
59+
public void testGetResponseCodeNpeIsWrappedAsIo() throws Exception {
60+
connectionInterceptor = safeFunction(connection -> {
61+
connection = spy(connection);
62+
doThrow(new NullPointerException()).when(connection).getResponseCode();
63+
return connection;
64+
});
65+
66+
assertThatThrownBy(() -> testForResponseCode(HttpURLConnection.HTTP_OK))
67+
.isInstanceOf(IOException.class)
68+
.hasMessage("Unexpected NullPointerException when trying to read response from HttpURLConnection")
69+
.hasCauseInstanceOf(NullPointerException.class);
70+
}
4471
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ public void testCustomTlsTrustManagerAndTrustAllFails() throws Exception {
152152
assertThatThrownBy(() -> createSdkHttpClient(httpClientOptions)).isInstanceOf(IllegalArgumentException.class);
153153
}
154154

155-
private void testForResponseCode(int returnCode) throws Exception {
155+
protected void testForResponseCode(int returnCode) throws Exception {
156156
testForResponseCode(returnCode, SdkHttpMethod.POST);
157157
}
158158

0 commit comments

Comments
 (0)