summaryrefslogtreecommitdiffstats
path: root/vespajlib/src/main/java/ai/vespa
diff options
context:
space:
mode:
Diffstat (limited to 'vespajlib/src/main/java/ai/vespa')
-rw-r--r--vespajlib/src/main/java/ai/vespa/util/http/hc4/VespaHttpClientBuilder.java145
-rw-r--r--vespajlib/src/main/java/ai/vespa/util/http/hc4/package-info.java5
-rw-r--r--vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelaySupplier.java44
-rw-r--r--vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelayedConnectionLevelRetryHandler.java126
-rw-r--r--vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelayedResponseLevelRetryHandler.java125
-rw-r--r--vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryConsumer.java16
-rw-r--r--vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryFailedConsumer.java14
-rw-r--r--vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryPredicate.java13
-rw-r--r--vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/Sleeper.java26
-rw-r--r--vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/package-info.java5
-rw-r--r--vespajlib/src/main/java/ai/vespa/util/http/hc5/HttpToHttpsRoutePlanner.java33
-rw-r--r--vespajlib/src/main/java/ai/vespa/util/http/hc5/VespaAsyncHttpClientBuilder.java71
-rw-r--r--vespajlib/src/main/java/ai/vespa/util/http/hc5/VespaHttpClientBuilder.java93
-rw-r--r--vespajlib/src/main/java/ai/vespa/util/http/hc5/package-info.java5
14 files changed, 721 insertions, 0 deletions
diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/VespaHttpClientBuilder.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/VespaHttpClientBuilder.java
new file mode 100644
index 00000000000..53bf7b866af
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/VespaHttpClientBuilder.java
@@ -0,0 +1,145 @@
+// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.util.http.hc4;
+
+import com.yahoo.security.tls.MixedMode;
+import com.yahoo.security.tls.TlsContext;
+import com.yahoo.security.tls.TransportSecurityUtils;
+import org.apache.http.HttpException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.config.Registry;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.conn.HttpClientConnectionManager;
+import org.apache.http.conn.UnsupportedSchemeException;
+import org.apache.http.conn.routing.HttpRoute;
+import org.apache.http.conn.routing.HttpRoutePlanner;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
+import org.apache.http.impl.conn.DefaultSchemePortResolver;
+import org.apache.http.protocol.HttpContext;
+
+import javax.net.ssl.SSLParameters;
+import java.net.InetAddress;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Http client builder for internal Vespa communications over http/https.
+ *
+ * Notes:
+ * - hostname verification is not enabled - CN/SAN verification is assumed to be handled by the underlying x509 trust manager.
+ * - custom connection managers must be configured through {@link #createBuilder(ConnectionManagerFactory)}. Do not call {@link HttpClientBuilder#setConnectionManager(HttpClientConnectionManager)}.
+ *
+ * @author bjorncs
+ */
+public class VespaHttpClientBuilder {
+
+ private static final Logger log = Logger.getLogger(VespaHttpClientBuilder.class.getName());
+
+ public interface ConnectionManagerFactory {
+ HttpClientConnectionManager create(Registry<ConnectionSocketFactory> socketFactoryRegistry);
+ }
+
+ private VespaHttpClientBuilder() {}
+
+ /**
+ * Create a client builder with default connection manager.
+ */
+ public static HttpClientBuilder create() {
+ return createBuilder(null);
+ }
+
+ /**
+ * Create a client builder with a user specified connection manager.
+ */
+ public static HttpClientBuilder create(ConnectionManagerFactory connectionManagerFactory) {
+ return createBuilder(connectionManagerFactory);
+ }
+
+ /**
+ * Creates a client builder with a {@link BasicHttpClientConnectionManager} configured.
+ * This connection manager uses a single connection for all requests. See Javadoc for details.
+ */
+ public static HttpClientBuilder createWithBasicConnectionManager() {
+ return createBuilder(BasicHttpClientConnectionManager::new);
+ }
+
+ private static HttpClientBuilder createBuilder(ConnectionManagerFactory connectionManagerFactory) {
+ HttpClientBuilder builder = HttpClientBuilder.create();
+ addSslSocketFactory(builder, connectionManagerFactory);
+ addHttpsRewritingRoutePlanner(builder);
+ return builder;
+ }
+
+ private static void addSslSocketFactory(HttpClientBuilder builder, ConnectionManagerFactory connectionManagerFactory) {
+ TransportSecurityUtils.getSystemTlsContext()
+ .ifPresent(tlsContext -> {
+ log.log(Level.FINE, "Adding ssl socket factory to client");
+ SSLConnectionSocketFactory socketFactory = createSslSocketFactory(tlsContext);
+ if (connectionManagerFactory != null) {
+ builder.setConnectionManager(connectionManagerFactory.create(createRegistry(socketFactory)));
+ } else {
+ builder.setSSLSocketFactory(socketFactory);
+ }
+ // Workaround that allows re-using https connections, see https://stackoverflow.com/a/42112034/1615280 for details.
+ // Proper solution would be to add a request interceptor that adds a x500 principal as user token,
+ // but certificate subject CN is not accessible through the TlsContext currently.
+ builder.setUserTokenHandler(context -> null);
+ });
+ }
+
+ private static void addHttpsRewritingRoutePlanner(HttpClientBuilder builder) {
+ if (TransportSecurityUtils.isTransportSecurityEnabled()
+ && TransportSecurityUtils.getInsecureMixedMode() != MixedMode.PLAINTEXT_CLIENT_MIXED_SERVER) {
+ builder.setRoutePlanner(new HttpToHttpsRoutePlanner());
+ }
+ }
+
+ private static SSLConnectionSocketFactory createSslSocketFactory(TlsContext tlsContext) {
+ SSLParameters parameters = tlsContext.parameters();
+ return new SSLConnectionSocketFactory(tlsContext.context(), parameters.getProtocols(), parameters.getCipherSuites(), new NoopHostnameVerifier());
+ }
+
+ private static Registry<ConnectionSocketFactory> createRegistry(SSLConnectionSocketFactory sslSocketFactory) {
+ return RegistryBuilder.<ConnectionSocketFactory>create()
+ .register("https", sslSocketFactory)
+ .register("http", PlainConnectionSocketFactory.getSocketFactory())
+ .build();
+ }
+
+
+ /**
+ * Reroutes requests using 'http' to 'https'.
+ * Implementation inspired by {@link org.apache.http.impl.conn.DefaultRoutePlanner}, but without proxy support.
+ */
+ static class HttpToHttpsRoutePlanner implements HttpRoutePlanner {
+
+ @Override
+ public HttpRoute determineRoute(HttpHost host, HttpRequest request, HttpContext context) throws HttpException {
+ HttpClientContext clientContext = HttpClientContext.adapt(context);
+ RequestConfig config = clientContext.getRequestConfig();
+ InetAddress local = config.getLocalAddress();
+
+ HttpHost target = resolveTarget(host);
+ boolean secure = target.getSchemeName().equalsIgnoreCase("https");
+ return new HttpRoute(target, local, secure);
+ }
+
+ private HttpHost resolveTarget(HttpHost host) throws HttpException {
+ try {
+ String originalScheme = host.getSchemeName();
+ String scheme = originalScheme.equalsIgnoreCase("http") ? "https" : originalScheme;
+ int port = DefaultSchemePortResolver.INSTANCE.resolve(host);
+ return new HttpHost(host.getHostName(), port, scheme);
+ } catch (UnsupportedSchemeException e) {
+ throw new HttpException(e.getMessage(), e);
+ }
+ }
+ }
+}
diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/package-info.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/package-info.java
new file mode 100644
index 00000000000..823bfdcde4b
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package ai.vespa.util.http.hc4;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelaySupplier.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelaySupplier.java
new file mode 100644
index 00000000000..b202966c412
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelaySupplier.java
@@ -0,0 +1,44 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.util.http.hc4.retry;
+
+import java.time.Duration;
+
+/**
+ * An abstraction that calculates the next delay based on the current retry count.
+ *
+ * @author bjorncs
+ */
+@FunctionalInterface
+interface DelaySupplier {
+ Duration getDelay(int executionCount);
+
+ class Fixed implements DelaySupplier {
+ private final Duration delay;
+
+ Fixed(Duration delay) {
+ this.delay = delay;
+ }
+
+ @Override
+ public Duration getDelay(int executionCount) { return delay; }
+ }
+
+ class Exponential implements DelaySupplier {
+ private final Duration startDelay;
+ private final Duration maxDelay;
+
+ Exponential(Duration startDelay, Duration maxDelay) {
+ this.startDelay = startDelay;
+ this.maxDelay = maxDelay;
+ }
+
+ @Override
+ public Duration getDelay(int executionCount) {
+ Duration nextDelay = startDelay;
+ for (int i = 1; i < executionCount; ++i) {
+ nextDelay = nextDelay.multipliedBy(2);
+ }
+ return maxDelay.compareTo(nextDelay) > 0 ? nextDelay : maxDelay;
+ }
+ }
+}
diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelayedConnectionLevelRetryHandler.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelayedConnectionLevelRetryHandler.java
new file mode 100644
index 00000000000..3ba92c08e30
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelayedConnectionLevelRetryHandler.java
@@ -0,0 +1,126 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.util.http.hc4.retry;
+
+import org.apache.http.annotation.Contract;
+import org.apache.http.annotation.ThreadingBehavior;
+import org.apache.http.client.HttpRequestRetryHandler;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.protocol.HttpContext;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.logging.Logger;
+
+/**
+ * A {@link HttpRequestRetryHandler} that supports delayed retries.
+ *
+ * @author bjorncs
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public class DelayedConnectionLevelRetryHandler implements HttpRequestRetryHandler {
+
+ private static final Logger log = Logger.getLogger(HttpRequestRetryHandler.class.getName());
+
+ private final DelaySupplier delaySupplier;
+ private final int maxRetries;
+ private final RetryPredicate<IOException> predicate;
+ private final RetryConsumer<IOException> retryConsumer;
+ private final RetryFailedConsumer<IOException> retryFailedConsumer;
+ private final Sleeper sleeper;
+
+ private DelayedConnectionLevelRetryHandler(
+ DelaySupplier delaySupplier,
+ int maxRetries,
+ RetryPredicate<IOException> predicate,
+ RetryConsumer<IOException> retryConsumer,
+ RetryFailedConsumer<IOException> retryFailedConsumer,
+ Sleeper sleeper) {
+ this.delaySupplier = delaySupplier;
+ this.maxRetries = maxRetries;
+ this.predicate = predicate;
+ this.retryConsumer = retryConsumer;
+ this.retryFailedConsumer = retryFailedConsumer;
+ this.sleeper = sleeper;
+ }
+
+ @Override
+ public boolean retryRequest(IOException exception, int executionCount, HttpContext ctx) {
+ log.fine(() -> String.format("retryRequest(exception='%s', executionCount='%d', ctx='%s'",
+ exception.getClass().getName(), executionCount, ctx));
+ HttpClientContext clientCtx = HttpClientContext.adapt(ctx);
+ if (!predicate.test(exception, clientCtx)) {
+ log.fine(() -> String.format("Not retrying for '%s'", ctx));
+ return false;
+ }
+ if (executionCount > maxRetries) {
+ log.fine(() -> String.format("Max retries exceeded for '%s'", ctx));
+ retryFailedConsumer.onRetryFailed(exception, executionCount, clientCtx);
+ return false;
+ }
+ Duration delay = delaySupplier.getDelay(executionCount);
+ log.fine(() -> String.format("Retrying after %s for '%s'", delay, ctx));
+ retryConsumer.onRetry(exception, delay, executionCount, clientCtx);
+ sleeper.sleep(delay);
+ return true;
+ }
+
+ public static class Builder {
+
+ private final DelaySupplier delaySupplier;
+ private final int maxRetries;
+ private RetryPredicate<IOException> predicate = (ioException, ctx) -> true;
+ private RetryConsumer<IOException> retryConsumer = (exception, delay, count, ctx) -> {};
+ private RetryFailedConsumer<IOException> retryFailedConsumer = (exception, count, ctx) -> {};
+ private Sleeper sleeper = new Sleeper.Default();
+
+ private Builder(DelaySupplier delaySupplier, int maxRetries) {
+ this.delaySupplier = delaySupplier;
+ this.maxRetries = maxRetries;
+ }
+
+ public static Builder withFixedDelay(Duration delay, int maxRetries) {
+ return new Builder(new DelaySupplier.Fixed(delay), maxRetries);
+ }
+
+ public static Builder withExponentialBackoff(Duration startDelay, Duration maxDelay, int maxRetries) {
+ return new Builder(new DelaySupplier.Exponential(startDelay, maxDelay), maxRetries);
+ }
+
+ public Builder retryForExceptions(List<Class<? extends IOException>> exceptionTypes) {
+ this.predicate = (ioException, ctx) -> exceptionTypes.stream().anyMatch(type -> type.isInstance(ioException));
+ return this;
+ }
+
+ public Builder retryForExceptions(Predicate<IOException> predicate) {
+ this.predicate = (ioException, ctx) -> predicate.test(ioException);
+ return this;
+ }
+
+ public Builder retryFor(RetryPredicate<IOException> predicate) {
+ this.predicate = predicate;
+ return this;
+ }
+
+ public Builder onRetry(RetryConsumer<IOException> consumer) {
+ this.retryConsumer = consumer;
+ return this;
+ }
+
+ public Builder onRetryFailed(RetryFailedConsumer<IOException> consumer) {
+ this.retryFailedConsumer = consumer;
+ return this;
+ }
+
+ // For unit testing
+ Builder withSleeper(Sleeper sleeper) {
+ this.sleeper = sleeper;
+ return this;
+ }
+
+ public DelayedConnectionLevelRetryHandler build() {
+ return new DelayedConnectionLevelRetryHandler(delaySupplier, maxRetries, predicate, retryConsumer, retryFailedConsumer, sleeper);
+ }
+ }
+}
diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelayedResponseLevelRetryHandler.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelayedResponseLevelRetryHandler.java
new file mode 100644
index 00000000000..d4ceb44a3ab
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/DelayedResponseLevelRetryHandler.java
@@ -0,0 +1,125 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.util.http.hc4.retry;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.annotation.Contract;
+import org.apache.http.annotation.ThreadingBehavior;
+import org.apache.http.client.ServiceUnavailableRetryStrategy;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.protocol.HttpContext;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.logging.Logger;
+
+/**
+ * A {@link ServiceUnavailableRetryStrategy} that supports delayed retries on any response types.
+ *
+ * @author bjorncs
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public class DelayedResponseLevelRetryHandler implements ServiceUnavailableRetryStrategy {
+
+ private static final Logger log = Logger.getLogger(DelayedResponseLevelRetryHandler.class.getName());
+
+ private final DelaySupplier delaySupplier;
+ private final int maxRetries;
+ private final RetryPredicate<HttpResponse> predicate;
+ private final RetryConsumer<HttpResponse> retryConsumer;
+ private final RetryFailedConsumer<HttpResponse> retryFailedConsumer;
+ private final ThreadLocal<Long> retryInterval = ThreadLocal.withInitial(() -> 0L);
+
+ private DelayedResponseLevelRetryHandler(
+ DelaySupplier delaySupplier,
+ int maxRetries,
+ RetryPredicate<HttpResponse> predicate,
+ RetryConsumer<HttpResponse> retryConsumer,
+ RetryFailedConsumer<HttpResponse> retryFailedConsumer) {
+
+ this.delaySupplier = delaySupplier;
+ this.maxRetries = maxRetries;
+ this.predicate = predicate;
+ this.retryConsumer = retryConsumer;
+ this.retryFailedConsumer = retryFailedConsumer;
+ }
+
+ @Override
+ public boolean retryRequest(HttpResponse response, int executionCount, HttpContext ctx) {
+ log.fine(() -> String.format("retryRequest(responseCode='%s', executionCount='%d', ctx='%s'",
+ response.getStatusLine().getStatusCode(), executionCount, ctx));
+ HttpClientContext clientCtx = HttpClientContext.adapt(ctx);
+ if (!predicate.test(response, clientCtx)) {
+ log.fine(() -> String.format("Not retrying for '%s'", ctx));
+ return false;
+ }
+ if (executionCount > maxRetries) {
+ log.fine(() -> String.format("Max retries exceeded for '%s'", ctx));
+ retryFailedConsumer.onRetryFailed(response, executionCount, clientCtx);
+ return false;
+ }
+ Duration delay = delaySupplier.getDelay(executionCount);
+ log.fine(() -> String.format("Retrying after %s for '%s'", delay, ctx));
+ retryInterval.set(delay.toMillis());
+ retryConsumer.onRetry(response, delay, executionCount, clientCtx);
+ return true;
+ }
+
+ @Override
+ public long getRetryInterval() {
+ // Calls to getRetryInterval are always guarded by a call to retryRequest (using the same thread).
+ // A thread local allows this retry handler to be thread safe and support dynamic retry intervals
+ return retryInterval.get();
+ }
+
+ public static class Builder {
+
+ private final DelaySupplier delaySupplier;
+ private final int maxRetries;
+ private RetryPredicate<HttpResponse> predicate = (response, ctx) -> true;
+ private RetryConsumer<HttpResponse> retryConsumer = (response, delay, count, ctx) -> {};
+ private RetryFailedConsumer<HttpResponse> retryFailedConsumer = (response, count, ctx) -> {};
+
+ private Builder(DelaySupplier delaySupplier, int maxRetries) {
+ this.delaySupplier = delaySupplier;
+ this.maxRetries = maxRetries;
+ }
+
+ public static Builder withFixedDelay(Duration delay, int maxRetries) {
+ return new Builder(new DelaySupplier.Fixed(delay), maxRetries);
+ }
+
+ public static Builder withExponentialBackoff(Duration startDelay, Duration maxDelay, int maxRetries) {
+ return new Builder(new DelaySupplier.Exponential(startDelay, maxDelay), maxRetries);
+ }
+
+ public Builder retryForStatusCodes(List<Integer> statusCodes) {
+ this.predicate = (response, ctx) -> statusCodes.contains(response.getStatusLine().getStatusCode());
+ return this;
+ }
+
+ public Builder retryForResponses(Predicate<HttpResponse> predicate) {
+ this.predicate = (response, ctx) -> predicate.test(response);
+ return this;
+ }
+
+ public Builder retryFor(RetryPredicate<HttpResponse> predicate) {
+ this.predicate = predicate;
+ return this;
+ }
+
+ public Builder onRetry(RetryConsumer<HttpResponse> consumer) {
+ this.retryConsumer = consumer;
+ return this;
+ }
+
+ public Builder onRetryFailed(RetryFailedConsumer<HttpResponse> consumer) {
+ this.retryFailedConsumer = consumer;
+ return this;
+ }
+
+ public DelayedResponseLevelRetryHandler build() {
+ return new DelayedResponseLevelRetryHandler(delaySupplier, maxRetries, predicate, retryConsumer, retryFailedConsumer);
+ }
+ }
+}
diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryConsumer.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryConsumer.java
new file mode 100644
index 00000000000..c168f7d50c9
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryConsumer.java
@@ -0,0 +1,16 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.util.http.hc4.retry;
+
+import org.apache.http.client.protocol.HttpClientContext;
+
+import java.time.Duration;
+
+/**
+ * Invoked before performing a delay and retry.
+ *
+ * @author bjorncs
+ */
+@FunctionalInterface
+public interface RetryConsumer<T> {
+ void onRetry(T data, Duration delay, int executionCount, HttpClientContext context);
+}
diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryFailedConsumer.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryFailedConsumer.java
new file mode 100644
index 00000000000..801c8a5af2f
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryFailedConsumer.java
@@ -0,0 +1,14 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.util.http.hc4.retry;
+
+import org.apache.http.client.protocol.HttpClientContext;
+
+/**
+ * Invoked after the last retry has failed.
+ *
+ * @author bjorncs
+ */
+@FunctionalInterface
+public interface RetryFailedConsumer<T> {
+ void onRetryFailed(T response, int executionCount, HttpClientContext context);
+}
diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryPredicate.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryPredicate.java
new file mode 100644
index 00000000000..45c5ef0d623
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/RetryPredicate.java
@@ -0,0 +1,13 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.util.http.hc4.retry;
+
+import org.apache.http.client.protocol.HttpClientContext;
+
+import java.util.function.BiPredicate;
+
+/**
+ * A predicate that determines whether an operation should be retried.
+ *
+ * @author bjorncs
+ */
+public interface RetryPredicate<T> extends BiPredicate<T, HttpClientContext> {}
diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/Sleeper.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/Sleeper.java
new file mode 100644
index 00000000000..f593561888d
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/Sleeper.java
@@ -0,0 +1,26 @@
+// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.util.http.hc4.retry;
+
+import java.time.Duration;
+
+/**
+ * An abstraction used for mocking {@link Thread#sleep(long)} in unit tests.
+ *
+ * @author bjorncs
+ */
+public interface Sleeper {
+ void sleep(Duration duration);
+
+ class Default implements Sleeper {
+ @Override
+ public void sleep(Duration duration) {
+ try {
+ Thread.sleep(duration.toMillis());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
+ }
+ }
+ }
+}
+
diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/package-info.java b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/package-info.java
new file mode 100644
index 00000000000..f20247fe13e
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/util/http/hc4/retry/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package ai.vespa.util.http.hc4.retry;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc5/HttpToHttpsRoutePlanner.java b/vespajlib/src/main/java/ai/vespa/util/http/hc5/HttpToHttpsRoutePlanner.java
new file mode 100644
index 00000000000..92cc35fc354
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/util/http/hc5/HttpToHttpsRoutePlanner.java
@@ -0,0 +1,33 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.util.http.hc5;
+
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.routing.HttpRoutePlanner;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.protocol.HttpContext;
+
+/**
+ * {@link HttpRoutePlanner} that changes assumes requests specify the HTTP scheme,
+ * and then changes this to HTTPS, keeping the other host parameters.
+ *
+ * @author jonmv
+ */
+class HttpToHttpsRoutePlanner implements HttpRoutePlanner {
+
+ @Override
+ public HttpRoute determineRoute(HttpHost target, HttpContext context) throws HttpException {
+ if ( ! target.getSchemeName().equals("http") && ! target.getSchemeName().equals("https"))
+ throw new IllegalArgumentException("Scheme must be 'http' or 'https' when using HttpToHttpsRoutePlanner, was '" + target.getSchemeName() + "'");
+
+ if (target.getPort() == -1)
+ throw new IllegalArgumentException("Port must be set when using HttpToHttpsRoutePlanner");
+
+ if (HttpClientContext.adapt(context).getRequestConfig().getProxy() != null)
+ throw new IllegalArgumentException("Proxies are not supported with HttpToHttpsRoutePlanner");
+
+ return new HttpRoute(new HttpHost("https", target.getAddress(), target.getHostName(), target.getPort()));
+ }
+
+}
diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc5/VespaAsyncHttpClientBuilder.java b/vespajlib/src/main/java/ai/vespa/util/http/hc5/VespaAsyncHttpClientBuilder.java
new file mode 100644
index 00000000000..50af29f92aa
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/util/http/hc5/VespaAsyncHttpClientBuilder.java
@@ -0,0 +1,71 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.util.http.hc5;
+
+import com.yahoo.security.tls.MixedMode;
+import com.yahoo.security.tls.TlsContext;
+import com.yahoo.security.tls.TransportSecurityUtils;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
+import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
+import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder;
+import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLParameters;
+
+/**
+ * Async http client builder for internal Vespa communications over http/https.
+ * Configures Vespa mTLS and handles TLS mixed mode automatically.
+ * Client should only be used for requests to Vespa services.
+ *
+ * Caveats:
+ * - custom connection manager must be configured through {@link #create(AsyncConnectionManagerFactory)}.
+ *
+ * @author bjorncs
+ */
+public class VespaAsyncHttpClientBuilder {
+
+ public interface AsyncConnectionManagerFactory {
+ AsyncClientConnectionManager create(TlsStrategy tlsStrategy);
+ }
+
+ public static HttpAsyncClientBuilder create() {
+ return create(
+ tlsStrategy -> PoolingAsyncClientConnectionManagerBuilder.create()
+ .setTlsStrategy(tlsStrategy)
+ .build());
+ }
+
+ public static HttpAsyncClientBuilder create(AsyncConnectionManagerFactory factory) {
+ return create(factory, new NoopHostnameVerifier());
+ }
+
+ public static HttpAsyncClientBuilder create(AsyncConnectionManagerFactory factory, HostnameVerifier hostnameVerifier) {
+ HttpAsyncClientBuilder clientBuilder = HttpAsyncClientBuilder.create();
+ TlsContext vespaTlsContext = TransportSecurityUtils.getSystemTlsContext().orElse(null);
+ TlsStrategy tlsStrategy;
+ if (vespaTlsContext != null) {
+ SSLParameters vespaTlsParameters = vespaTlsContext.parameters();
+ tlsStrategy = ClientTlsStrategyBuilder.create()
+ .setHostnameVerifier(hostnameVerifier)
+ .setSslContext(vespaTlsContext.context())
+ .setTlsVersions(vespaTlsParameters.getProtocols())
+ .setCiphers(vespaTlsParameters.getCipherSuites())
+ .build();
+ if (TransportSecurityUtils.getInsecureMixedMode() != MixedMode.PLAINTEXT_CLIENT_MIXED_SERVER) {
+ clientBuilder.setRoutePlanner(new HttpToHttpsRoutePlanner());
+ }
+ } else {
+ tlsStrategy = ClientTlsStrategyBuilder.create().build();
+ }
+ clientBuilder.disableConnectionState(); // Share connections between subsequent requests
+ clientBuilder.disableCookieManagement();
+ clientBuilder.disableAuthCaching();
+ clientBuilder.disableRedirectHandling();
+ clientBuilder.setConnectionManager(factory.create(tlsStrategy));
+ clientBuilder.setConnectionManagerShared(false);
+ return clientBuilder;
+ }
+
+}
diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc5/VespaHttpClientBuilder.java b/vespajlib/src/main/java/ai/vespa/util/http/hc5/VespaHttpClientBuilder.java
new file mode 100644
index 00000000000..e01d278ff38
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/util/http/hc5/VespaHttpClientBuilder.java
@@ -0,0 +1,93 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package ai.vespa.util.http.hc5;
+
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
+import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
+import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
+import org.apache.hc.core5.http.config.Registry;
+import org.apache.hc.core5.http.config.RegistryBuilder;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLParameters;
+
+import static com.yahoo.security.tls.MixedMode.PLAINTEXT_CLIENT_MIXED_SERVER;
+import static com.yahoo.security.tls.TransportSecurityUtils.getInsecureMixedMode;
+import static com.yahoo.security.tls.TransportSecurityUtils.getSystemTlsContext;
+import static com.yahoo.security.tls.TransportSecurityUtils.isTransportSecurityEnabled;
+
+/**
+ * Sync HTTP client builder <em>for internal Vespa communications over http/https.</em>
+ *
+ * Configures Vespa mTLS and handles TLS mixed mode automatically.
+ * Custom connection managers must be configured through {@link #create(HttpClientConnectionManagerFactory)}.
+ *
+ * @author jonmv
+ */
+public class VespaHttpClientBuilder {
+
+ public interface HttpClientConnectionManagerFactory {
+ HttpClientConnectionManager create(Registry<ConnectionSocketFactory> socketFactories);
+ }
+
+ public static HttpClientBuilder create() {
+ return create(PoolingHttpClientConnectionManager::new);
+ }
+
+ public static HttpClientBuilder create(HttpClientConnectionManagerFactory connectionManagerFactory) {
+ return create(connectionManagerFactory, new NoopHostnameVerifier());
+ }
+
+ public static HttpClientBuilder create(HttpClientConnectionManagerFactory connectionManagerFactory,
+ HostnameVerifier hostnameVerifier) {
+ return create(connectionManagerFactory, hostnameVerifier, true);
+ }
+
+ public static HttpClientBuilder create(HttpClientConnectionManagerFactory connectionManagerFactory,
+ HostnameVerifier hostnameVerifier,
+ boolean rewriteHttpToHttps) {
+ HttpClientBuilder builder = HttpClientBuilder.create();
+ addSslSocketFactory(builder, connectionManagerFactory, hostnameVerifier);
+ if (rewriteHttpToHttps)
+ addHttpsRewritingRoutePlanner(builder);
+
+ builder.disableConnectionState(); // Share connections between subsequent requests.
+ builder.disableCookieManagement();
+ builder.disableAuthCaching();
+ builder.disableRedirectHandling();
+
+ return builder;
+ }
+
+ private static void addSslSocketFactory(HttpClientBuilder builder, HttpClientConnectionManagerFactory connectionManagerFactory,
+ HostnameVerifier hostnameVerifier) {
+ getSystemTlsContext().ifPresent(tlsContext -> {
+ SSLParameters parameters = tlsContext.parameters();
+ SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(tlsContext.context(),
+ parameters.getProtocols(),
+ parameters.getCipherSuites(),
+ hostnameVerifier);
+ builder.setConnectionManager(connectionManagerFactory.create(createRegistry(socketFactory)));
+ // Workaround that allows re-using https connections, see https://stackoverflow.com/a/42112034/1615280 for details.
+ // Proper solution would be to add a request interceptor that adds a x500 principal as user token,
+ // but certificate subject CN is not accessible through the TlsContext currently.
+ builder.setUserTokenHandler((route, context) -> null);
+ });
+ }
+
+ private static Registry<ConnectionSocketFactory> createRegistry(SSLConnectionSocketFactory sslSocketFactory) {
+ return RegistryBuilder.<ConnectionSocketFactory>create()
+ .register("https", sslSocketFactory)
+ .register("http", PlainConnectionSocketFactory.getSocketFactory())
+ .build();
+ }
+
+ private static void addHttpsRewritingRoutePlanner(HttpClientBuilder builder) {
+ if (isTransportSecurityEnabled() && getInsecureMixedMode() != PLAINTEXT_CLIENT_MIXED_SERVER)
+ builder.setRoutePlanner(new HttpToHttpsRoutePlanner());
+ }
+
+}
diff --git a/vespajlib/src/main/java/ai/vespa/util/http/hc5/package-info.java b/vespajlib/src/main/java/ai/vespa/util/http/hc5/package-info.java
new file mode 100644
index 00000000000..1deb7cd6afb
--- /dev/null
+++ b/vespajlib/src/main/java/ai/vespa/util/http/hc5/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package ai.vespa.util.http.hc5;
+
+import com.yahoo.osgi.annotation.ExportPackage;