23
23
24
24
import java .io .IOException ;
25
25
import java .io .InputStream ;
26
+ import java .io .OutputStream ;
27
+ import java .io .UncheckedIOException ;
26
28
import java .net .HttpURLConnection ;
27
29
import java .net .URI ;
28
30
import java .security .KeyManagementException ;
31
33
import java .time .Duration ;
32
34
import java .util .List ;
33
35
import java .util .Map ;
36
+ import java .util .Objects ;
37
+ import java .util .Optional ;
38
+ import java .util .function .Supplier ;
34
39
import java .util .stream .Collectors ;
35
40
import javax .net .ssl .HostnameVerifier ;
36
41
import javax .net .ssl .HttpsURLConnection ;
42
47
import javax .net .ssl .X509TrustManager ;
43
48
import software .amazon .awssdk .annotations .SdkPublicApi ;
44
49
import software .amazon .awssdk .http .AbortableInputStream ;
50
+ import software .amazon .awssdk .http .ContentStreamProvider ;
45
51
import software .amazon .awssdk .http .ExecutableHttpRequest ;
46
52
import software .amazon .awssdk .http .HttpExecuteRequest ;
47
53
import software .amazon .awssdk .http .HttpExecuteResponse ;
@@ -195,10 +201,20 @@ private SSLContext getSslContext(AttributeMap options) {
195
201
}
196
202
197
203
private static class RequestCallable implements ExecutableHttpRequest {
198
-
199
204
private final HttpURLConnection connection ;
200
205
private final HttpExecuteRequest request ;
201
206
207
+ /**
208
+ * Whether we encountered the 'bug' in the way the HttpURLConnection handles 'Expect: 100-continue' cases. See
209
+ * {@link #getAndHandle100Bug} for more information.
210
+ */
211
+ private boolean expect100BugEncountered = false ;
212
+
213
+ /**
214
+ * Result cache for {@link #responseHasNoContent()}.
215
+ */
216
+ private Boolean responseHasNoContent ;
217
+
202
218
private RequestCallable (HttpURLConnection connection , HttpExecuteRequest request ) {
203
219
this .connection = connection ;
204
220
this .request = request ;
@@ -208,14 +224,19 @@ private RequestCallable(HttpURLConnection connection, HttpExecuteRequest request
208
224
public HttpExecuteResponse call () throws IOException {
209
225
connection .connect ();
210
226
211
- request .contentStreamProvider ().ifPresent (provider ->
212
- invokeSafely (() -> IoUtils .copy (provider .newStream (), connection .getOutputStream ())));
227
+ Optional <ContentStreamProvider > requestContent = request .contentStreamProvider ();
228
+
229
+ if (requestContent .isPresent ()) {
230
+ Optional <OutputStream > outputStream = tryGetOutputStream ();
231
+ if (outputStream .isPresent ()) {
232
+ IoUtils .copy (requestContent .get ().newStream (), outputStream .get ());
233
+ }
234
+ }
213
235
214
236
int responseCode = getResponseCodeSafely (connection );
215
237
boolean isErrorResponse = HttpStatusFamily .of (responseCode ).isOneOf (CLIENT_ERROR , SERVER_ERROR );
216
- InputStream content = !isErrorResponse ? connection .getInputStream () : connection .getErrorStream ();
217
- AbortableInputStream responseBody = content != null ?
218
- AbortableInputStream .create (content ) : null ;
238
+ Optional <InputStream > responseContent = isErrorResponse ? tryGetErrorStream () : tryGetInputStream ();
239
+ AbortableInputStream responseBody = responseContent .map (AbortableInputStream ::create ).orElse (null );
219
240
220
241
return HttpExecuteResponse .builder ()
221
242
.response (SdkHttpResponse .builder ()
@@ -228,6 +249,93 @@ public HttpExecuteResponse call() throws IOException {
228
249
.build ();
229
250
}
230
251
252
+ private Optional <OutputStream > tryGetOutputStream () {
253
+ return getAndHandle100Bug (() -> invokeSafely (connection ::getOutputStream ), false );
254
+ }
255
+
256
+ private Optional <InputStream > tryGetInputStream () {
257
+ return getAndHandle100Bug (() -> invokeSafely (connection ::getInputStream ), true );
258
+ }
259
+
260
+ private Optional <InputStream > tryGetErrorStream () {
261
+ InputStream result = invokeSafely (connection ::getErrorStream );
262
+ if (result == null && expect100BugEncountered ) {
263
+ log .debug (() -> "The response payload has been dropped because of a limitation of the JDK's URL Connection "
264
+ + "HTTP client, resulting in a less descriptive SDK exception error message. Using "
265
+ + "the Apache HTTP client removes this limitation." );
266
+ }
267
+ return Optional .ofNullable (result );
268
+ }
269
+
270
+ /**
271
+ * This handles a bug in {@link HttpURLConnection#getOutputStream()} and {@link HttpURLConnection#getInputStream()}
272
+ * where these methods will throw a ProtocolException if we sent an "Expect: 100-continue" header, and the
273
+ * service responds with something other than a 100.
274
+ *
275
+ * HttpUrlConnection still gives us access to the response code and headers when this bug is encountered, so our
276
+ * handling of the bug is:
277
+ * <ol>
278
+ * <li>If the service returned a response status or content length that indicates there was no response payload,
279
+ * we ignore that we couldn't read the response payload, and just return the response with what we have.</li>
280
+ * <li>If the service returned a payload and we can't read it because of the bug, we throw an exception for
281
+ * non-failure cases (2xx, 3xx) or log and return the response without the payload for failure cases (4xx or 5xx)
282
+ * .</li>
283
+ * </ol>
284
+ */
285
+ private <T > Optional <T > getAndHandle100Bug (Supplier <T > supplier , boolean failOn100Bug ) {
286
+ try {
287
+ return Optional .ofNullable (supplier .get ());
288
+ } catch (RuntimeException e ) {
289
+ if (!exceptionCausedBy100HandlingBug (e )) {
290
+ throw e ;
291
+ }
292
+
293
+ if (responseHasNoContent ()) {
294
+ return Optional .empty ();
295
+ }
296
+
297
+ expect100BugEncountered = true ;
298
+
299
+ if (!failOn100Bug ) {
300
+ return Optional .empty ();
301
+ }
302
+
303
+ int responseCode = invokeSafely (connection ::getResponseCode );
304
+ String message = "Unable to read response payload, because service returned response code "
305
+ + responseCode + " to an Expect: 100-continue request. Using another HTTP client "
306
+ + "implementation (e.g. Apache) removes this limitation." ;
307
+ throw new UncheckedIOException (new IOException (message , e ));
308
+ }
309
+ }
310
+
311
+ private boolean exceptionCausedBy100HandlingBug (RuntimeException e ) {
312
+ return requestWasExpect100Continue () &&
313
+ e .getMessage () != null &&
314
+ e .getMessage ().startsWith ("java.net.ProtocolException: Server rejected operation" );
315
+ }
316
+
317
+ private Boolean requestWasExpect100Continue () {
318
+ return request .httpRequest ()
319
+ .firstMatchingHeader ("Expect" )
320
+ .map (expect -> expect .equalsIgnoreCase ("100-continue" ))
321
+ .orElse (false );
322
+ }
323
+
324
+ private boolean responseHasNoContent () {
325
+ // We cannot account for chunked encoded responses, because we only have access to headers and response code here,
326
+ // so we assume chunked encoded responses DO have content.
327
+ if (responseHasNoContent == null ) {
328
+ responseHasNoContent = responseNeverHasPayload (invokeSafely (connection ::getResponseCode )) ||
329
+ Objects .equals (connection .getHeaderField ("Content-Length" ), "0" ) ||
330
+ Objects .equals (connection .getRequestMethod (), "HEAD" );
331
+ }
332
+ return responseHasNoContent ;
333
+ }
334
+
335
+ private boolean responseNeverHasPayload (int responseCode ) {
336
+ return responseCode == 204 || responseCode == 304 || (responseCode >= 100 && responseCode < 200 );
337
+ }
338
+
231
339
/**
232
340
* {@link sun.net.www.protocol.http.HttpURLConnection#getInputStream0()} has been observed to intermittently throw
233
341
* {@link NullPointerException}s for reasons that still require further investigation, but are assumed to be due to a
0 commit comments