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