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 PasswordAuthentication serverAuthentication;
167
168 private PasswordAuthentication proxyAuthentication;
169
170 JdkTransporter(
171 RepositorySystemSession session,
172 RemoteRepository repository,
173 int javaVersion,
174 ChecksumExtractor checksumExtractor,
175 PathProcessor pathProcessor)
176 throws NoTransporterException {
177 this.checksumExtractor = checksumExtractor;
178 this.pathProcessor = pathProcessor;
179 try {
180 URI uri = new URI(repository.getUrl()).parseServerAuthority();
181 if (uri.isOpaque()) {
182 throw new URISyntaxException(repository.getUrl(), "URL must not be opaque");
183 }
184 if (uri.getRawFragment() != null || uri.getRawQuery() != null) {
185 throw new URISyntaxException(repository.getUrl(), "URL must not have fragment or query");
186 }
187 String path = uri.getPath();
188 if (path == null) {
189 path = "/";
190 }
191 if (!path.startsWith("/")) {
192 path = "/" + path;
193 }
194 if (!path.endsWith("/")) {
195 path = path + "/";
196 }
197 this.baseUri = URI.create(uri.getScheme() + "://" + uri.getRawAuthority() + path);
198 } catch (URISyntaxException e) {
199 throw new NoTransporterException(repository, e.getMessage(), e);
200 }
201
202 HashMap<String, String> headers = new HashMap<>();
203 String userAgent = HttpTransporterUtils.getUserAgent(session, repository);
204 if (userAgent != null) {
205 headers.put(USER_AGENT, userAgent);
206 }
207 Map<String, String> configuredHeaders = HttpTransporterUtils.getHttpHeaders(session, repository);
208 if (configuredHeaders != null) {
209 headers.putAll(configuredHeaders);
210 }
211 headers.put(CACHE_CONTROL, "no-cache, no-store");
212
213 this.connectTimeout = HttpTransporterUtils.getHttpConnectTimeout(session, repository);
214 this.requestTimeout = HttpTransporterUtils.getHttpRequestTimeout(session, repository);
215 Optional<Boolean> expectContinue = HttpTransporterUtils.getHttpExpectContinue(session, repository);
216 if (javaVersion > 19) {
217 this.expectContinue = expectContinue.orElse(null);
218 } else {
219 this.expectContinue = null;
220 if (expectContinue.isPresent()) {
221 LOGGER.warn(
222 "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",
223 javaVersion);
224 }
225 }
226 final String httpsSecurityMode = HttpTransporterUtils.getHttpsSecurityMode(session, repository);
227 final boolean insecure = ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE.equals(httpsSecurityMode);
228
229 this.maxConcurrentRequests = new Semaphore(ConfigUtils.getInteger(
230 session,
231 DEFAULT_MAX_CONCURRENT_REQUESTS,
232 CONFIG_PROP_MAX_CONCURRENT_REQUESTS + "." + repository.getId(),
233 CONFIG_PROP_MAX_CONCURRENT_REQUESTS));
234
235 this.preemptiveAuth = HttpTransporterUtils.isHttpPreemptiveAuth(session, repository);
236 this.preemptivePutAuth = HttpTransporterUtils.isHttpPreemptivePutAuth(session, repository);
237
238 this.headers = headers;
239 this.client = createClient(session, repository, insecure);
240 }
241
242 private URI resolve(TransportTask task) {
243 return baseUri.resolve(task.getLocation());
244 }
245
246 private ConnectException enhance(ConnectException connectException) {
247 ConnectException result = new ConnectException("Connection to " + baseUri.toASCIIString() + " refused");
248 result.initCause(connectException);
249 return result;
250 }
251
252 @Override
253 protected void implPeek(PeekTask task) throws Exception {
254 HttpRequest.Builder request =
255 HttpRequest.newBuilder().uri(resolve(task)).method("HEAD", HttpRequest.BodyPublishers.noBody());
256 headers.forEach(request::setHeader);
257
258 prepare(request);
259 try {
260 HttpResponse<Void> response = send(request.build(), HttpResponse.BodyHandlers.discarding());
261 if (response.statusCode() >= MULTIPLE_CHOICES) {
262 throw new HttpTransporterException(response.statusCode());
263 }
264 } catch (ConnectException e) {
265 throw enhance(e);
266 }
267 }
268
269 @Override
270 protected void implGet(GetTask task) throws Exception {
271 boolean resume = task.getResumeOffset() > 0L && task.getDataPath() != null;
272 HttpResponse<InputStream> response = null;
273
274 try {
275 while (true) {
276 HttpRequest.Builder request =
277 HttpRequest.newBuilder().uri(resolve(task)).GET();
278 headers.forEach(request::setHeader);
279
280 if (resume) {
281 long resumeOffset = task.getResumeOffset();
282 long lastModified = pathProcessor.lastModified(task.getDataPath(), 0L);
283 request.header(RANGE, "bytes=" + resumeOffset + '-');
284 request.header(
285 IF_UNMODIFIED_SINCE,
286 RFC7231.format(Instant.ofEpochMilli(lastModified - MODIFICATION_THRESHOLD)));
287 request.header(ACCEPT_ENCODING, "identity");
288 }
289
290 prepare(request);
291 try {
292 response = send(request.build(), HttpResponse.BodyHandlers.ofInputStream());
293 if (response.statusCode() >= MULTIPLE_CHOICES) {
294 if (resume && response.statusCode() == PRECONDITION_FAILED) {
295 closeBody(response);
296 resume = false;
297 continue;
298 }
299 try {
300 JdkRFC9457Reporter.INSTANCE.generateException(response, (statusCode, reasonPhrase) -> {
301 throw new HttpTransporterException(statusCode);
302 });
303 } finally {
304 closeBody(response);
305 }
306 }
307 } catch (ConnectException e) {
308 closeBody(response);
309 throw enhance(e);
310 }
311 break;
312 }
313
314 long offset = 0L,
315 length = response.headers().firstValueAsLong(CONTENT_LENGTH).orElse(-1L);
316 if (resume) {
317 String range = response.headers().firstValue(CONTENT_RANGE).orElse(null);
318 if (range != null) {
319 Matcher m = CONTENT_RANGE_PATTERN.matcher(range);
320 if (!m.matches()) {
321 throw new IOException("Invalid Content-Range header for partial download: " + range);
322 }
323 offset = Long.parseLong(m.group(1));
324 length = Long.parseLong(m.group(2)) + 1L;
325 if (offset < 0L || offset >= length || (offset > 0L && offset != task.getResumeOffset())) {
326 throw new IOException("Invalid Content-Range header for partial download from offset "
327 + task.getResumeOffset() + ": " + range);
328 }
329 }
330 }
331
332 final boolean downloadResumed = offset > 0L;
333 final Path dataFile = task.getDataPath();
334 if (dataFile == null) {
335 try (InputStream is = response.body()) {
336 utilGet(task, is, true, length, downloadResumed);
337 }
338 } else {
339 try (PathProcessor.CollocatedTempFile tempFile = pathProcessor.newTempFile(dataFile)) {
340 task.setDataPath(tempFile.getPath(), downloadResumed);
341 if (downloadResumed && Files.isRegularFile(dataFile)) {
342 try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(dataFile))) {
343 Files.copy(inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING);
344 }
345 }
346 try (InputStream is = response.body()) {
347 utilGet(task, is, true, length, downloadResumed);
348 }
349 tempFile.move();
350 } finally {
351 task.setDataPath(dataFile);
352 }
353 }
354 if (task.getDataPath() != null) {
355 String lastModifiedHeader = response.headers()
356 .firstValue(LAST_MODIFIED)
357 .orElse(null);
358 if (lastModifiedHeader != null) {
359 try {
360 pathProcessor.setLastModified(
361 task.getDataPath(),
362 ZonedDateTime.parse(lastModifiedHeader, RFC7231)
363 .toInstant()
364 .toEpochMilli());
365 } catch (DateTimeParseException e) {
366
367 }
368 }
369 }
370 Map<String, String> checksums = checksumExtractor.extractChecksums(headerGetter(response));
371 if (checksums != null && !checksums.isEmpty()) {
372 checksums.forEach(task::setChecksum);
373 }
374 } finally {
375 closeBody(response);
376 }
377 }
378
379 private static Function<String, String> headerGetter(HttpResponse<?> response) {
380 return s -> response.headers().firstValue(s).orElse(null);
381 }
382
383 private void closeBody(HttpResponse<InputStream> streamHttpResponse) throws IOException {
384 if (streamHttpResponse != null) {
385 InputStream body = streamHttpResponse.body();
386 if (body != null) {
387 body.close();
388 }
389 }
390 }
391
392 @Override
393 protected void implPut(PutTask task) throws Exception {
394 HttpRequest.Builder request = HttpRequest.newBuilder().uri(resolve(task));
395 if (expectContinue != null) {
396 request = request.expectContinue(expectContinue);
397 }
398 headers.forEach(request::setHeader);
399
400 if (task.getDataLength() == 0L) {
401 request.PUT(HttpRequest.BodyPublishers.noBody());
402 } else {
403 request.PUT(HttpRequest.BodyPublishers.fromPublisher(
404 HttpRequest.BodyPublishers.ofInputStream(() -> {
405 try {
406 return new TransportListenerNotifyingInputStream(
407 task.newInputStream(), task.getListener(), task.getDataLength());
408 } catch (IOException e) {
409 throw new UncheckedIOException(e);
410 }
411 }),
412
413 task.getDataLength()));
414 }
415 prepare(request);
416 try {
417 HttpResponse<Void> response = send(request.build(), HttpResponse.BodyHandlers.discarding());
418 if (response.statusCode() >= MULTIPLE_CHOICES) {
419 throw new HttpTransporterException(response.statusCode());
420 }
421 } catch (ConnectException e) {
422 throw enhance(e);
423 } catch (IOException e) {
424
425 Throwable rootCause = getRootCause(e);
426 if (rootCause instanceof TransferCancelledException) {
427 throw (TransferCancelledException) rootCause;
428 }
429 throw e;
430 }
431 }
432
433 private void prepare(HttpRequest.Builder requestBuilder) {
434 if (preemptiveAuth
435 || (preemptivePutAuth && requestBuilder.build().method().equals("PUT"))) {
436 if (serverAuthentication != null) {
437
438 requestBuilder.setHeader(
439 "Authorization",
440 getBasicAuthValue(serverAuthentication.getUserName(), serverAuthentication.getPassword()));
441 }
442 if (proxyAuthentication != null) {
443 requestBuilder.setHeader(
444 "Proxy-Authorization",
445 getBasicAuthValue(proxyAuthentication.getUserName(), proxyAuthentication.getPassword()));
446 }
447 }
448 }
449
450 static String getBasicAuthValue(String username, char[] password) {
451
452 return "Basic "
453 + Base64.getEncoder().encodeToString((username + ':' + String.valueOf(password)).getBytes(ISO_8859_1));
454 }
455
456 private <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
457 throws Exception {
458 maxConcurrentRequests.acquire();
459 try {
460 return client.send(request, responseBodyHandler);
461 } finally {
462 maxConcurrentRequests.release();
463 }
464 }
465
466 @Override
467 protected void implClose() {
468 if (client != null) {
469 JdkTransporterCloser.closer(client).run();
470 }
471 }
472
473 private HttpClient createClient(RepositorySystemSession session, RemoteRepository repository, boolean insecure)
474 throws RuntimeException {
475
476 HashMap<Authenticator.RequestorType, PasswordAuthentication> authentications = new HashMap<>();
477 SSLContext sslContext = null;
478 try (AuthenticationContext repoAuthContext = AuthenticationContext.forRepository(session, repository)) {
479 if (repoAuthContext != null) {
480 sslContext = repoAuthContext.get(AuthenticationContext.SSL_CONTEXT, SSLContext.class);
481
482 String username = repoAuthContext.get(AuthenticationContext.USERNAME);
483 String password = repoAuthContext.get(AuthenticationContext.PASSWORD);
484 serverAuthentication = new PasswordAuthentication(username, password.toCharArray());
485 authentications.put(Authenticator.RequestorType.SERVER, serverAuthentication);
486 }
487 }
488
489 if (sslContext == null) {
490 try {
491 if (insecure) {
492 sslContext = SSLContext.getInstance("TLS");
493 X509ExtendedTrustManager tm = new X509ExtendedTrustManager() {
494 @Override
495 public void checkClientTrusted(X509Certificate[] chain, String authType) {}
496
497 @Override
498 public void checkServerTrusted(X509Certificate[] chain, String authType) {}
499
500 @Override
501 public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) {}
502
503 @Override
504 public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) {}
505
506 @Override
507 public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) {}
508
509 @Override
510 public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) {}
511
512 @Override
513 public X509Certificate[] getAcceptedIssuers() {
514 return null;
515 }
516 };
517 sslContext.init(null, new X509TrustManager[] {tm}, null);
518 } else {
519 sslContext = SSLContext.getDefault();
520 }
521 } catch (Exception e) {
522 if (e instanceof RuntimeException) {
523 throw (RuntimeException) e;
524 } else {
525 throw new IllegalStateException("SSL Context setup failure", e);
526 }
527 }
528 }
529
530 Methanol.Builder builder = Methanol.newBuilder()
531 .version(HttpClient.Version.valueOf(ConfigUtils.getString(
532 session,
533 DEFAULT_HTTP_VERSION,
534 CONFIG_PROP_HTTP_VERSION + "." + repository.getId(),
535 CONFIG_PROP_HTTP_VERSION)))
536 .followRedirects(HttpClient.Redirect.NORMAL)
537 .connectTimeout(Duration.ofMillis(connectTimeout))
538
539
540
541 .requestTimeout(Duration.ofMillis(requestTimeout))
542 .sslContext(sslContext);
543
544 if (insecure) {
545 SSLParameters sslParameters = sslContext.getDefaultSSLParameters();
546 sslParameters.setEndpointIdentificationAlgorithm(null);
547 builder.sslParameters(sslParameters);
548 }
549
550 setLocalAddress(
551 builder,
552 HttpTransporterUtils.getHttpLocalAddress(session, repository).orElse(null));
553
554 if (repository.getProxy() != null) {
555 InetSocketAddress proxyAddress = new InetSocketAddress(
556 repository.getProxy().getHost(), repository.getProxy().getPort());
557 if (proxyAddress.isUnresolved()) {
558 throw new IllegalStateException(
559 "Proxy host " + repository.getProxy().getHost() + " could not be resolved");
560 }
561 builder.proxy(ProxySelector.of(proxyAddress));
562 try (AuthenticationContext proxyAuthContext = AuthenticationContext.forProxy(session, repository)) {
563 if (proxyAuthContext != null) {
564 String username = proxyAuthContext.get(AuthenticationContext.USERNAME);
565 String password = proxyAuthContext.get(AuthenticationContext.PASSWORD);
566
567 proxyAuthentication = new PasswordAuthentication(username, password.toCharArray());
568 authentications.put(Authenticator.RequestorType.PROXY, proxyAuthentication);
569 }
570 }
571 }
572
573 if (!authentications.isEmpty()) {
574 builder.authenticator(new Authenticator() {
575 @Override
576 protected PasswordAuthentication getPasswordAuthentication() {
577 return authentications.get(getRequestorType());
578 }
579 });
580 }
581
582 configureRetryHandler(session, repository, builder);
583
584 return builder.build();
585 }
586
587 private static class RetryLoggingListener implements RetryInterceptor.Listener {
588 private final int maxNumRetries;
589
590 RetryLoggingListener(int maxNumRetries) {
591 this.maxNumRetries = maxNumRetries;
592 }
593
594 @Override
595 public void onRetry(Context<?> context, HttpRequest nextRequest, Duration delay) {
596 LOGGER.warn(
597 "{} request to {} failed (attempt {} of {}) due to {}. Retrying in {} ms...",
598 context.request().method(),
599 context.request().uri(),
600 context.retryCount() + 1,
601 maxNumRetries + 1,
602 getReason(context),
603 delay.toMillis());
604 }
605
606 String getReason(Context<?> context) {
607 if (context.exception().isPresent()) {
608 return context.exception().get().getMessage();
609 } else if (context.response().isPresent()) {
610 return "status " + context.response().get().statusCode();
611 }
612
613 throw new IllegalStateException("No exception or response present in retry context");
614 }
615 }
616
617 private static void configureRetryHandler(
618 RepositorySystemSession session, RemoteRepository repository, Methanol.Builder builder) {
619 int retryCount = HttpTransporterUtils.getHttpRetryHandlerCount(session, repository);
620 long retryInterval = HttpTransporterUtils.getHttpRetryHandlerInterval(session, repository);
621 long retryIntervalMax = HttpTransporterUtils.getHttpRetryHandlerIntervalMax(session, repository);
622 if (retryCount > 0) {
623 Methanol.Interceptor rateLimitingRetryInterceptor = RetryInterceptor.newBuilder()
624 .maxRetries(retryCount)
625 .onStatus(HttpTransporterUtils.getHttpServiceUnavailableCodes(session, repository)::contains)
626 .listener(new RetryLoggingListener(retryCount))
627 .backoff(RetryInterceptor.BackoffStrategy.linear(
628 Duration.ofMillis(retryInterval), Duration.ofMillis(retryIntervalMax)))
629 .build();
630 builder.interceptor(rateLimitingRetryInterceptor);
631 Methanol.Interceptor retryIoExceptionsInterceptor = RetryInterceptor.newBuilder()
632
633
634
635 .maxRetries(retryCount)
636 .onException(t -> {
637
638
639
640 Throwable rootCause = getRootCause(t);
641 return t instanceof IOException
642 && !NON_RETRIABLE_IO_EXCEPTIONS.contains(t.getClass())
643 && !(rootCause instanceof TransferCancelledException);
644 })
645 .listener(new RetryLoggingListener(retryCount))
646 .build();
647 builder.interceptor(retryIoExceptionsInterceptor);
648 }
649 }
650
651 private static void setLocalAddress(HttpClient.Builder builder, InetAddress address) {
652 if (address == null) {
653 return;
654 }
655 try {
656 final Method mtd = builder.getClass().getDeclaredMethod("localAddress", InetAddress.class);
657 if (!mtd.canAccess(builder)) {
658 mtd.setAccessible(true);
659 }
660 mtd.invoke(builder, address);
661 } catch (final NoSuchMethodException ignore) {
662
663 } catch (InvocationTargetException e) {
664 throw new IllegalStateException(e.getTargetException());
665 } catch (IllegalAccessException e) {
666 throw new IllegalStateException(e);
667 }
668 }
669
670 private static Throwable getRootCause(Throwable throwable) {
671 Objects.requireNonNull(throwable);
672 Throwable rootCause = throwable;
673 while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
674 rootCause = rootCause.getCause();
675 }
676 return rootCause;
677 }
678 }