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,15 @@ 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
+
202
213
private RequestCallable (HttpURLConnection connection , HttpExecuteRequest request ) {
203
214
this .connection = connection ;
204
215
this .request = request ;
@@ -208,14 +219,19 @@ private RequestCallable(HttpURLConnection connection, HttpExecuteRequest request
208
219
public HttpExecuteResponse call () throws IOException {
209
220
connection .connect ();
210
221
211
- request .contentStreamProvider ().ifPresent (provider ->
212
- invokeSafely (() -> IoUtils .copy (provider .newStream (), connection .getOutputStream ())));
222
+ Optional <ContentStreamProvider > requestContent = request .contentStreamProvider ();
223
+
224
+ if (requestContent .isPresent ()) {
225
+ Optional <OutputStream > outputStream = tryGetOutputStream ();
226
+ if (outputStream .isPresent ()) {
227
+ IoUtils .copy (requestContent .get ().newStream (), outputStream .get ());
228
+ }
229
+ }
213
230
214
231
int responseCode = getResponseCodeSafely (connection );
215
232
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 ;
233
+ Optional <InputStream > responseContent = isErrorResponse ? tryGetErrorStream () : tryGetInputStream ();
234
+ AbortableInputStream responseBody = responseContent .map (AbortableInputStream ::create ).orElse (null );
219
235
220
236
return HttpExecuteResponse .builder ()
221
237
.response (SdkHttpResponse .builder ()
@@ -228,6 +244,90 @@ public HttpExecuteResponse call() throws IOException {
228
244
.build ();
229
245
}
230
246
247
+ private Optional <OutputStream > tryGetOutputStream () {
248
+ return getAndHandle100Bug (() -> invokeSafely (connection ::getOutputStream ), false );
249
+ }
250
+
251
+ private Optional <InputStream > tryGetInputStream () {
252
+ return getAndHandle100Bug (() -> invokeSafely (connection ::getInputStream ), true );
253
+ }
254
+
255
+ private Optional <InputStream > tryGetErrorStream () {
256
+ InputStream result = invokeSafely (connection ::getErrorStream );
257
+ if (result == null && expect100BugEncountered ) {
258
+ log .debug (() -> "The response payload has been dropped because of a limitation of the JDK's URL Connection "
259
+ + "HTTP client, resulting in a less descriptive SDK exception error message. Using "
260
+ + "the Apache HTTP client removes this limitation." );
261
+ }
262
+ return Optional .ofNullable (result );
263
+ }
264
+
265
+ /**
266
+ * This handles a bug in {@link HttpURLConnection#getOutputStream()} and {@link HttpURLConnection#getInputStream()}
267
+ * where these methods will throw a ProtocolException if we sent an "Expect: 100-continue" header, and the
268
+ * service responds with something other than a 100.
269
+ *
270
+ * HttpUrlConnection still gives us access to the response code and headers when this bug is encountered, so our
271
+ * handling of the bug is:
272
+ * <ol>
273
+ * <li>If the service returned a response status or content length that indicates there was no response payload,
274
+ * we ignore that we couldn't read the response payload, and just return the response with what we have.</li>
275
+ * <li>If the service returned a payload and we can't read it because of the bug, we throw an exception for
276
+ * non-failure cases (2xx, 3xx) or log and return the response without the payload for failure cases (4xx or 5xx)
277
+ * .</li>
278
+ * </ol>
279
+ */
280
+ private <T > Optional <T > getAndHandle100Bug (Supplier <T > supplier , boolean failOn100Bug ) {
281
+ try {
282
+ return Optional .ofNullable (supplier .get ());
283
+ } catch (RuntimeException e ) {
284
+ if (!exceptionCausedBy100HandlingBug (e )) {
285
+ throw e ;
286
+ }
287
+
288
+ expect100BugEncountered = true ;
289
+
290
+ if (!failOn100Bug ) {
291
+ return Optional .empty ();
292
+ }
293
+
294
+ if (responseHasNoContent ()) {
295
+ return Optional .empty ();
296
+ }
297
+
298
+ int responseCode = invokeSafely (connection ::getResponseCode );
299
+ String message = "Unable to read response payload, because service returned response code "
300
+ + responseCode + " to an Expect: 100-continue request. Using another HTTP client "
301
+ + "implementation (e.g. Apache) removes this limitation." ;
302
+ throw new UncheckedIOException (new IOException (message , e ));
303
+ }
304
+ }
305
+
306
+ private boolean exceptionCausedBy100HandlingBug (RuntimeException e ) {
307
+ return requestWasExpect100Continue () &&
308
+ e .getMessage () != null &&
309
+ e .getMessage ().startsWith ("java.net.ProtocolException: Server rejected operation" );
310
+ }
311
+
312
+ private Boolean requestWasExpect100Continue () {
313
+ return request .httpRequest ()
314
+ .firstMatchingHeader ("Expect" )
315
+ .map (expect -> expect .equalsIgnoreCase ("100-continue" ))
316
+ .orElse (false );
317
+ }
318
+
319
+ private boolean responseHasNoContent () {
320
+ // We cannot account for chunked encoded responses, because we only have access to headers and response code here,
321
+ // so we assume chunked encoded responses DO have content.
322
+ return responseNeverHasPayload (invokeSafely (connection ::getResponseCode )) ||
323
+ Objects .equals (connection .getHeaderField ("Content-Length" ), "0" ) ||
324
+ Objects .equals (connection .getRequestMethod (), "HEAD" );
325
+ }
326
+
327
+ private boolean responseNeverHasPayload (int responseCode ) {
328
+ return responseCode == 204 || responseCode == 304 || (responseCode >= 100 && responseCode < 200 );
329
+ }
330
+
231
331
/**
232
332
* {@link sun.net.www.protocol.http.HttpURLConnection#getInputStream0()} has been observed to intermittently throw
233
333
* {@link NullPointerException}s for reasons that still require further investigation, but are assumed to be due to a
0 commit comments