1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.aether.transport.jdk;
20
21 import javax.net.ssl.SSLContext;
22 import javax.net.ssl.SSLEngine;
23 import javax.net.ssl.SSLException;
24 import javax.net.ssl.SSLParameters;
25 import javax.net.ssl.X509ExtendedTrustManager;
26 import javax.net.ssl.X509TrustManager;
27
28 import java.io.BufferedInputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.InterruptedIOException;
32 import java.io.UncheckedIOException;
33 import java.lang.reflect.InvocationTargetException;
34 import java.lang.reflect.Method;
35 import java.net.Authenticator;
36 import java.net.ConnectException;
37 import java.net.InetAddress;
38 import java.net.InetSocketAddress;
39 import java.net.NoRouteToHostException;
40 import java.net.PasswordAuthentication;
41 import java.net.ProxySelector;
42 import java.net.Socket;
43 import java.net.URI;
44 import java.net.URISyntaxException;
45 import java.net.UnknownHostException;
46 import java.net.http.HttpClient;
47 import java.net.http.HttpRequest;
48 import java.net.http.HttpResponse;
49 import java.nio.file.Files;
50 import java.nio.file.Path;
51 import java.nio.file.StandardCopyOption;
52 import java.security.cert.X509Certificate;
53 import java.time.Duration;
54 import java.time.Instant;
55 import java.time.ZoneId;
56 import java.time.ZonedDateTime;
57 import java.time.format.DateTimeFormatter;
58 import java.time.format.DateTimeParseException;
59 import java.util.Base64;
60 import java.util.HashMap;
61 import java.util.Locale;
62 import java.util.Map;
63 import java.util.Objects;
64 import java.util.Optional;
65 import java.util.Set;
66 import java.util.concurrent.Semaphore;
67 import java.util.function.Function;
68 import java.util.regex.Matcher;
69
70 import com.github.mizosoft.methanol.Methanol;
71 import com.github.mizosoft.methanol.RetryInterceptor;
72 import com.github.mizosoft.methanol.RetryInterceptor.Context;
73 import org.eclipse.aether.ConfigurationProperties;
74 import org.eclipse.aether.RepositorySystemSession;
75 import org.eclipse.aether.repository.AuthenticationContext;
76 import org.eclipse.aether.repository.RemoteRepository;
77 import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
78 import org.eclipse.aether.spi.connector.transport.GetTask;
79 import org.eclipse.aether.spi.connector.transport.PeekTask;
80 import org.eclipse.aether.spi.connector.transport.PutTask;
81 import org.eclipse.aether.spi.connector.transport.TransportListenerNotifyingInputStream;
82 import org.eclipse.aether.spi.connector.transport.TransportTask;
83 import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor;
84 import org.eclipse.aether.spi.connector.transport.http.HttpTransporter;
85 import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException;
86 import org.eclipse.aether.spi.io.PathProcessor;
87 import org.eclipse.aether.transfer.NoTransporterException;
88 import org.eclipse.aether.transfer.TransferCancelledException;
89 import org.eclipse.aether.util.ConfigUtils;
90 import org.eclipse.aether.util.connector.transport.http.HttpTransporterUtils;
91 import org.slf4j.Logger;
92 import org.slf4j.LoggerFactory;
93
94 import static java.nio.charset.StandardCharsets.ISO_8859_1;
95 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.ACCEPT_ENCODING;
96 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CACHE_CONTROL;
97 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CONTENT_LENGTH;
98 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CONTENT_RANGE;
99 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CONTENT_RANGE_PATTERN;
100 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.IF_UNMODIFIED_SINCE;
101 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.LAST_MODIFIED;
102 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.MULTIPLE_CHOICES;
103 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.PRECONDITION_FAILED;
104 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.RANGE;
105 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.USER_AGENT;
106 import static org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.CONFIG_PROP_HTTP_VERSION;
107 import static org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.CONFIG_PROP_MAX_CONCURRENT_REQUESTS;
108 import static org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.DEFAULT_HTTP_VERSION;
109 import static org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.DEFAULT_MAX_CONCURRENT_REQUESTS;
110
111
112
113
114
115
116
117
118
119
120
121
122
123 final class JdkTransporter extends AbstractTransporter implements HttpTransporter {
124 private static final Logger LOGGER = LoggerFactory.getLogger(JdkTransporter.class);
125
126 private static final DateTimeFormatter RFC7231 = DateTimeFormatter.ofPattern(
127 "EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH)
128 .withZone(ZoneId.of("GMT"));
129
130 private static final long MODIFICATION_THRESHOLD = 60L * 1000L;
131
132
133
134
135
136
137 private static final Set<Class<? extends IOException>> NON_RETRIABLE_IO_EXCEPTIONS = Set.of(
138 InterruptedIOException.class,
139 UnknownHostException.class,
140 ConnectException.class,
141 NoRouteToHostException.class,
142 SSLException.class);
143
144 private final ChecksumExtractor checksumExtractor;
145
146 private final PathProcessor pathProcessor;
147
148 private final URI baseUri;
149
150 private final HttpClient client;
151
152 private final Map<String, String> headers;
153
154 private final int connectTimeout;
155
156 private final int requestTimeout;
157
158 private final Boolean expectContinue;
159
160 private final Semaphore maxConcurrentRequests;
161
162 private final boolean preemptivePutAuth;
163
164 private final boolean preemptiveAuth;
165
166 private final boolean sendRfc9457Accept;
167
168 private PasswordAuthentication serverAuthentication;
169
170 private PasswordAuthentication proxyAuthentication;
171
172 JdkTransporter(
173 RepositorySystemSession session,
174 RemoteRepository repository,
175 int javaVersion,
176 ChecksumExtractor checksumExtractor,
177 PathProcessor pathProcessor)
178 throws NoTransporterException {
179 this.checksumExtractor = checksumExtractor;
180 this.pathProcessor = pathProcessor;
181 try {
182 this.baseUri = HttpTransporterUtils.getBaseUri(repository);
183 } catch (URISyntaxException e) {
184 throw new NoTransporterException(repository, e.getMessage(), e);
185 }
186
187 HashMap<String, String> headers = new HashMap<>();
188 String userAgent = HttpTransporterUtils.getUserAgent(session, repository);
189 if (userAgent != null) {
190 headers.put(USER_AGENT, userAgent);
191 }
192 Map<String, String> configuredHeaders = HttpTransporterUtils.getHttpHeaders(session, repository);
193 if (configuredHeaders != null) {
194 headers.putAll(configuredHeaders);
195 }
196 headers.put(CACHE_CONTROL, "no-cache, no-store");
197
198 this.connectTimeout = HttpTransporterUtils.getHttpConnectTimeout(session, repository);
199 this.requestTimeout = HttpTransporterUtils.getHttpRequestTimeout(session, repository);
200 Optional<Boolean> expectContinue = HttpTransporterUtils.getHttpExpectContinue(session, repository);
201 if (javaVersion > 19) {
202 this.expectContinue = expectContinue.orElse(null);
203 } else {
204 this.expectContinue = null;
205 if (expectContinue.isPresent()) {
206 LOGGER.warn(
207 "Configuration for Expect-Continue set but is ignored on Java versions below 20 (current java version is {}) due https://bugs.openjdk.org/browse/JDK-8286171",
208 javaVersion);
209 }
210 }
211 final String httpsSecurityMode = HttpTransporterUtils.getHttpsSecurityMode(session, repository);
212 final boolean insecure = ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE.equals(httpsSecurityMode);
213
214 this.maxConcurrentRequests = new Semaphore(ConfigUtils.getInteger(
215 session,
216 DEFAULT_MAX_CONCURRENT_REQUESTS,
217 CONFIG_PROP_MAX_CONCURRENT_REQUESTS + "." + repository.getId(),
218 CONFIG_PROP_MAX_CONCURRENT_REQUESTS));
219
220 this.preemptiveAuth = HttpTransporterUtils.isHttpPreemptiveAuth(session, repository);
221 this.preemptivePutAuth = HttpTransporterUtils.isHttpPreemptivePutAuth(session, repository);
222 this.sendRfc9457Accept = HttpTransporterUtils.isHttpSendRfc9457Accept(session, repository);
223
224 this.headers = headers;
225 this.client = createClient(session, repository, insecure);
226 }
227
228 private URI resolve(TransportTask task) {
229 return baseUri.resolve(task.getLocation());
230 }
231
232 private ConnectException enhance(ConnectException connectException) {
233 ConnectException result = new ConnectException("Connection to " + baseUri.toASCIIString() + " refused");
234 result.initCause(connectException);
235 return result;
236 }
237
238 @Override
239 protected void implPeek(PeekTask task) throws Exception {
240 HttpRequest.Builder request =
241 HttpRequest.newBuilder().uri(resolve(task)).method("HEAD", HttpRequest.BodyPublishers.noBody());
242 headers.forEach(request::setHeader);
243
244 prepare(request);
245 try {
246 HttpResponse<Void> response = send(request.build(), HttpResponse.BodyHandlers.discarding());
247 if (response.statusCode() >= MULTIPLE_CHOICES) {
248 throw new HttpTransporterException(response.statusCode());
249 }
250 } catch (ConnectException e) {
251 throw enhance(e);
252 }
253 }
254
255 @Override
256 protected void implGet(GetTask task) throws Exception {
257 boolean resume = task.getResumeOffset() > 0L && task.getDataPath() != null;
258 HttpResponse<InputStream> response = null;
259
260 try {
261 while (true) {
262 HttpRequest.Builder request =
263 HttpRequest.newBuilder().uri(resolve(task)).GET();
264 headers.forEach(request::setHeader);
265 if (sendRfc9457Accept) {
266 JdkRFC9457Reporter.INSTANCE.prepareRequest(request);
267 }
268
269 if (resume) {
270 long resumeOffset = task.getResumeOffset();
271 long lastModified = pathProcessor.lastModified(task.getDataPath(), 0L);
272 request.header(RANGE, "bytes=" + resumeOffset + '-');
273 request.header(
274 IF_UNMODIFIED_SINCE,
275 RFC7231.format(Instant.ofEpochMilli(lastModified - MODIFICATION_THRESHOLD)));
276 request.header(ACCEPT_ENCODING, "identity");
277 }
278
279 prepare(request);
280 try {
281 response = send(request.build(), HttpResponse.BodyHandlers.ofInputStream());
282 if (response.statusCode() >= MULTIPLE_CHOICES) {
283 if (resume && response.statusCode() == PRECONDITION_FAILED) {
284 closeBody(response);
285 resume = false;
286 continue;
287 }
288 try {
289 JdkRFC9457Reporter.INSTANCE.generateException(response, (statusCode, reasonPhrase) -> {
290 throw new HttpTransporterException(statusCode);
291 });
292 } finally {
293 closeBody(response);
294 }
295 }
296 } catch (ConnectException e) {
297 closeBody(response);
298 throw enhance(e);
299 }
300 break;
301 }
302
303 long offset = 0L,
304 length = response.headers().firstValueAsLong(CONTENT_LENGTH).orElse(-1L);
305 if (resume) {
306 String range = response.headers().firstValue(CONTENT_RANGE).orElse(null);
307 if (range != null) {
308 Matcher m = CONTENT_RANGE_PATTERN.matcher(range);
309 if (!m.matches()) {
310 throw new IOException("Invalid Content-Range header for partial download: " + range);
311 }
312 offset = Long.parseLong(m.group(1));
313 length = Long.parseLong(m.group(2)) + 1L;
314 if (offset < 0L || offset >= length || (offset > 0L && offset != task.getResumeOffset())) {
315 throw new IOException("Invalid Content-Range header for partial download from offset "
316 + task.getResumeOffset() + ": " + range);
317 }
318 }
319 }
320
321 final boolean downloadResumed = offset > 0L;
322 final Path dataFile = task.getDataPath();
323 if (dataFile == null) {
324 try (InputStream is = response.body()) {
325 utilGet(task, is, true, length, downloadResumed);
326 }
327 } else {
328 try (PathProcessor.CollocatedTempFile tempFile = pathProcessor.newTempFile(dataFile)) {
329 task.setDataPath(tempFile.getPath(), downloadResumed);
330 if (downloadResumed && Files.isRegularFile(dataFile)) {
331 try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(dataFile))) {
332 Files.copy(inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING);
333 }
334 }
335 try (InputStream is = response.body()) {
336 utilGet(task, is, true, length, downloadResumed);
337 }
338 tempFile.move();
339 } finally {
340 task.setDataPath(dataFile);
341 }
342 }
343 if (task.getDataPath() != null) {
344 String lastModifiedHeader = response.headers()
345 .firstValue(LAST_MODIFIED)
346 .orElse(null);
347 if (lastModifiedHeader != null) {
348 try {
349 pathProcessor.setLastModified(
350 task.getDataPath(),
351 ZonedDateTime.parse(lastModifiedHeader, RFC7231)
352 .toInstant()
353 .toEpochMilli());
354 } catch (DateTimeParseException e) {
355
356 }
357 }
358 }
359 Map<String, String> checksums = checksumExtractor.extractChecksums(headerGetter(response));
360 if (checksums != null && !checksums.isEmpty()) {
361 checksums.forEach(task::setChecksum);
362 }
363 } finally {
364 closeBody(response);
365 }
366 }
367
368 private static Function<String, String> headerGetter(HttpResponse<?> response) {
369 return s -> response.headers().firstValue(s).orElse(null);
370 }
371
372 private void closeBody(HttpResponse<InputStream> streamHttpResponse) throws IOException {
373 if (streamHttpResponse != null) {
374 InputStream body = streamHttpResponse.body();
375 if (body != null) {
376 body.close();
377 }
378 }
379 }
380
381 @Override
382 protected void implPut(PutTask task) throws Exception {
383 HttpRequest.Builder request = HttpRequest.newBuilder().uri(resolve(task));
384 if (expectContinue != null) {
385 request = request.expectContinue(expectContinue);
386 }
387 headers.forEach(request::setHeader);
388 if (sendRfc9457Accept) {
389 JdkRFC9457Reporter.INSTANCE.prepareRequest(request);
390 }
391
392 if (task.getDataLength() == 0L) {
393 request.PUT(HttpRequest.BodyPublishers.noBody());
394 } else {
395 request.PUT(HttpRequest.BodyPublishers.fromPublisher(
396 HttpRequest.BodyPublishers.ofInputStream(() -> {
397 try {
398 return new TransportListenerNotifyingInputStream(
399 task.newInputStream(), task.getListener(), task.getDataLength());
400 } catch (IOException e) {
401 throw new UncheckedIOException(e);
402 }
403 }),
404
405 task.getDataLength()));
406 }
407 prepare(request);
408 try {
409 HttpResponse<InputStream> response = send(request.build(), HttpResponse.BodyHandlers.ofInputStream());
410 if (response.statusCode() >= MULTIPLE_CHOICES) {
411 try {
412 JdkRFC9457Reporter.INSTANCE.generateException(response, (statusCode, reasonPhrase) -> {
413 throw new HttpTransporterException(statusCode);
414 });
415 } finally {
416 closeBody(response);
417 }
418 }
419 } catch (ConnectException e) {
420 throw enhance(e);
421 } catch (IOException e) {
422
423 Throwable rootCause = getRootCause(e);
424 if (rootCause instanceof TransferCancelledException) {
425 throw (TransferCancelledException) rootCause;
426 }
427 throw e;
428 }
429 }
430
431 private void prepare(HttpRequest.Builder requestBuilder) {
432 if (preemptiveAuth
433 || (preemptivePutAuth && requestBuilder.build().method().equals("PUT"))) {
434 if (serverAuthentication != null) {
435
436 requestBuilder.setHeader(
437 "Authorization",
438 getBasicAuthValue(serverAuthentication.getUserName(), serverAuthentication.getPassword()));
439 }
440 if (proxyAuthentication != null) {
441 requestBuilder.setHeader(
442 "Proxy-Authorization",
443 getBasicAuthValue(proxyAuthentication.getUserName(), proxyAuthentication.getPassword()));
444 }
445 }
446 }
447
448 static String getBasicAuthValue(String username, char[] password) {
449
450 return "Basic "
451 + Base64.getEncoder().encodeToString((username + ':' + String.valueOf(password)).getBytes(ISO_8859_1));
452 }
453
454 private <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
455 throws Exception {
456 maxConcurrentRequests.acquire();
457 try {
458 return client.send(request, responseBodyHandler);
459 } finally {
460 maxConcurrentRequests.release();
461 }
462 }
463
464 @Override
465 protected void implClose() {
466 if (client != null) {
467 JdkTransporterCloser.closer(client).run();
468 }
469 }
470
471 private HttpClient createClient(RepositorySystemSession session, RemoteRepository repository, boolean insecure)
472 throws RuntimeException {
473
474 HashMap<Authenticator.RequestorType, PasswordAuthentication> authentications = new HashMap<>();
475 SSLContext sslContext = null;
476 try (AuthenticationContext repoAuthContext = AuthenticationContext.forRepository(session, repository)) {
477 if (repoAuthContext != null) {
478 sslContext = repoAuthContext.get(AuthenticationContext.SSL_CONTEXT, SSLContext.class);
479
480 String username = repoAuthContext.get(AuthenticationContext.USERNAME);
481 String password = repoAuthContext.get(AuthenticationContext.PASSWORD);
482 serverAuthentication = new PasswordAuthentication(username, password.toCharArray());
483 authentications.put(Authenticator.RequestorType.SERVER, serverAuthentication);
484 }
485 }
486
487 if (sslContext == null) {
488 try {
489 if (insecure) {
490 sslContext = SSLContext.getInstance("TLS");
491 X509ExtendedTrustManager tm = new X509ExtendedTrustManager() {
492 @Override
493 public void checkClientTrusted(X509Certificate[] chain, String authType) {}
494
495 @Override
496 public void checkServerTrusted(X509Certificate[] chain, String authType) {}
497
498 @Override
499 public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) {}
500
501 @Override
502 public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) {}
503
504 @Override
505 public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) {}
506
507 @Override
508 public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) {}
509
510 @Override
511 public X509Certificate[] getAcceptedIssuers() {
512 return null;
513 }
514 };
515 sslContext.init(null, new X509TrustManager[] {tm}, null);
516 } else {
517 sslContext = SSLContext.getDefault();
518 }
519 } catch (Exception e) {
520 if (e instanceof RuntimeException) {
521 throw (RuntimeException) e;
522 } else {
523 throw new IllegalStateException("SSL Context setup failure", e);
524 }
525 }
526 }
527
528 Methanol.Builder builder = Methanol.newBuilder()
529 .version(HttpClient.Version.valueOf(ConfigUtils.getString(
530 session,
531 DEFAULT_HTTP_VERSION,
532 CONFIG_PROP_HTTP_VERSION + "." + repository.getId(),
533 CONFIG_PROP_HTTP_VERSION)))
534 .followRedirects(HttpClient.Redirect.NORMAL)
535 .connectTimeout(Duration.ofMillis(connectTimeout))
536
537
538
539 .requestTimeout(Duration.ofMillis(requestTimeout))
540 .sslContext(sslContext);
541
542 if (insecure) {
543 SSLParameters sslParameters = sslContext.getDefaultSSLParameters();
544 sslParameters.setEndpointIdentificationAlgorithm(null);
545 builder.sslParameters(sslParameters);
546 }
547
548 setLocalAddress(
549 builder,
550 HttpTransporterUtils.getHttpLocalAddress(session, repository).orElse(null));
551
552 if (repository.getProxy() != null) {
553 InetSocketAddress proxyAddress = new InetSocketAddress(
554 repository.getProxy().getHost(), repository.getProxy().getPort());
555 if (proxyAddress.isUnresolved()) {
556 throw new IllegalStateException(
557 "Proxy host " + repository.getProxy().getHost() + " could not be resolved");
558 }
559 builder.proxy(ProxySelector.of(proxyAddress));
560 try (AuthenticationContext proxyAuthContext = AuthenticationContext.forProxy(session, repository)) {
561 if (proxyAuthContext != null) {
562 String username = proxyAuthContext.get(AuthenticationContext.USERNAME);
563 String password = proxyAuthContext.get(AuthenticationContext.PASSWORD);
564
565 proxyAuthentication = new PasswordAuthentication(username, password.toCharArray());
566 authentications.put(Authenticator.RequestorType.PROXY, proxyAuthentication);
567 }
568 }
569 }
570
571 if (!authentications.isEmpty()) {
572 builder.authenticator(new Authenticator() {
573 @Override
574 protected PasswordAuthentication getPasswordAuthentication() {
575 return authentications.get(getRequestorType());
576 }
577 });
578 }
579
580 configureRetryHandler(session, repository, builder);
581
582 return builder.build();
583 }
584
585 private static class RetryLoggingListener implements RetryInterceptor.Listener {
586 private final int maxNumRetries;
587
588 RetryLoggingListener(int maxNumRetries) {
589 this.maxNumRetries = maxNumRetries;
590 }
591
592 @Override
593 public void onRetry(Context<?> context, HttpRequest nextRequest, Duration delay) {
594 LOGGER.warn(
595 "{} request to {} failed (attempt {} of {}) due to {}. Retrying in {} ms...",
596 context.request().method(),
597 context.request().uri(),
598 context.retryCount() + 1,
599 maxNumRetries + 1,
600 getReason(context),
601 delay.toMillis());
602 }
603
604 String getReason(Context<?> context) {
605 if (context.exception().isPresent()) {
606 return context.exception().get().getMessage();
607 } else if (context.response().isPresent()) {
608 return "status " + context.response().get().statusCode();
609 }
610
611 throw new IllegalStateException("No exception or response present in retry context");
612 }
613 }
614
615 private static void configureRetryHandler(
616 RepositorySystemSession session, RemoteRepository repository, Methanol.Builder builder) {
617 int retryCount = HttpTransporterUtils.getHttpRetryHandlerCount(session, repository);
618 long retryInterval = HttpTransporterUtils.getHttpRetryHandlerInterval(session, repository);
619 long retryIntervalMax = HttpTransporterUtils.getHttpRetryHandlerIntervalMax(session, repository);
620 if (retryCount > 0) {
621 Methanol.Interceptor rateLimitingRetryInterceptor = RetryInterceptor.newBuilder()
622 .maxRetries(retryCount)
623 .onStatus(HttpTransporterUtils.getHttpServiceUnavailableCodes(session, repository)::contains)
624 .listener(new RetryLoggingListener(retryCount))
625 .backoff(RetryInterceptor.BackoffStrategy.linear(
626 Duration.ofMillis(retryInterval), Duration.ofMillis(retryIntervalMax)))
627 .build();
628 builder.interceptor(rateLimitingRetryInterceptor);
629 Methanol.Interceptor retryIoExceptionsInterceptor = RetryInterceptor.newBuilder()
630
631
632
633 .maxRetries(retryCount)
634 .onException(t -> {
635
636
637
638 Throwable rootCause = getRootCause(t);
639 return t instanceof IOException
640 && !NON_RETRIABLE_IO_EXCEPTIONS.contains(t.getClass())
641 && !(rootCause instanceof TransferCancelledException);
642 })
643 .listener(new RetryLoggingListener(retryCount))
644 .build();
645 builder.interceptor(retryIoExceptionsInterceptor);
646 }
647 }
648
649 private static void setLocalAddress(HttpClient.Builder builder, InetAddress address) {
650 if (address == null) {
651 return;
652 }
653 try {
654 final Method mtd = builder.getClass().getDeclaredMethod("localAddress", InetAddress.class);
655 if (!mtd.canAccess(builder)) {
656 mtd.setAccessible(true);
657 }
658 mtd.invoke(builder, address);
659 } catch (final NoSuchMethodException ignore) {
660
661 } catch (InvocationTargetException e) {
662 throw new IllegalStateException(e.getTargetException());
663 } catch (IllegalAccessException e) {
664 throw new IllegalStateException(e);
665 }
666 }
667
668 private static Throwable getRootCause(Throwable throwable) {
669 Objects.requireNonNull(throwable);
670 Throwable rootCause = throwable;
671 while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
672 rootCause = rootCause.getCause();
673 }
674 return rootCause;
675 }
676 }