Apache httpclient 5 (#4950)

Signed-off-by: Steffen Nießing <zuniquex@protonmail.com>
diff --git a/connectors/apache5-connector/pom.xml b/connectors/apache5-connector/pom.xml
new file mode 100644
index 0000000..669ac11
--- /dev/null
+++ b/connectors/apache5-connector/pom.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0"?>
+<!--
+
+    Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+
+    This program and the accompanying materials are made available under the
+    terms of the Eclipse Public License v. 2.0, which is available at
+    http://www.eclipse.org/legal/epl-2.0.
+
+    This Source Code may also be made available under the following Secondary
+    Licenses when the conditions for such availability set forth in the
+    Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+    version 2 with the GNU Classpath Exception, which is available at
+    https://www.gnu.org/software/classpath/license.html.
+
+    SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.glassfish.jersey.connectors</groupId>
+        <artifactId>project</artifactId>
+        <version>2.36-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>jersey-apache5-connector</artifactId>
+    <packaging>jar</packaging>
+    <name>jersey-connectors-apache5</name>
+
+    <description>Jersey Client Transport via Apache HttpClient 5.x</description>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.httpcomponents.client5</groupId>
+            <artifactId>httpclient5</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.containers</groupId>
+            <artifactId>jersey-container-grizzly2-http</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+            <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>com.sun.istack</groupId>
+                <artifactId>istack-commons-maven-plugin</artifactId>
+                <inherited>true</inherited>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>build-helper-maven-plugin</artifactId>
+                <inherited>true</inherited>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <inherited>true</inherited>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ClientProperties.java b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ClientProperties.java
new file mode 100644
index 0000000..eac5be5
--- /dev/null
+++ b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ClientProperties.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.util.Map;
+
+import org.glassfish.jersey.internal.util.PropertiesClass;
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+
+/**
+ * Configuration options specific to the Client API that utilizes {@link Apache5ConnectorProvider}.
+ *
+ * @author jorgeluisw@mac.com
+ * @author Paul Sandoz
+ * @author Pavel Bucek
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Steffen Nießing
+ */
+@PropertiesClass
+public final class Apache5ClientProperties {
+
+    /**
+     * The credential provider that should be used to retrieve
+     * credentials from a user. Credentials needed for proxy authentication
+     * are stored here as well.
+     * <p/>
+     * The value MUST be an instance of {@link org.apache.hc.client5.http.auth.CredentialsProvider}.
+     * <p/>
+     * If the property is absent a default provider will be used.
+     * <p/>
+     * The name of the configuration property is <tt>{@value}</tt>.
+     */
+    public static final String CREDENTIALS_PROVIDER = "jersey.config.apache5.client.credentialsProvider";
+
+    /**
+     * A value of {@code false} indicates the client should handle cookies
+     * automatically using HttpClient's default cookie policy. A value
+     * of {@code true} will cause the client to ignore all cookies.
+     * <p/>
+     * The value MUST be an instance of {@link java.lang.Boolean}.
+     * <p/>
+     * The default value is {@code false}.
+     * <p/>
+     * The name of the configuration property is <tt>{@value}</tt>.
+     */
+    public static final String DISABLE_COOKIES = "jersey.config.apache5.client.handleCookies";
+
+    /**
+     * A value of {@code true} indicates that a client should send an
+     * authentication request even before the server gives a 401
+     * response.
+     * <p>
+     * This property may only be set prior to constructing Apache connector using {@link Apache5ConnectorProvider}.
+     * <p/>
+     * The value MUST be an instance of {@link java.lang.Boolean}.
+     * <p/>
+     * The default value is {@code false}.
+     * <p/>
+     * The name of the configuration property is <tt>{@value}</tt>.
+     */
+    public static final String PREEMPTIVE_BASIC_AUTHENTICATION = "jersey.config.apache5.client.preemptiveBasicAuthentication";
+
+    /**
+     * Connection Manager which will be used to create {@link org.apache.hc.client5.http.classic.HttpClient}.
+     * <p/>
+     * The value MUST be an instance of {@link org.apache.hc.client5.http.io.HttpClientConnectionManager}.
+     * <p/>
+     * If the property is absent a default Connection Manager will be used
+     * ({@link org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager}).
+     * If you want to use this client in multi-threaded environment, be sure you override default value with
+     * {@link org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager} instance.
+     * <p/>
+     * The name of the configuration property is <tt>{@value}</tt>.
+     */
+    public static final String CONNECTION_MANAGER = "jersey.config.apache5.client.connectionManager";
+
+    /**
+     * A value of {@code true} indicates that configured connection manager should be shared
+     * among multiple Jersey {@link org.glassfish.jersey.client.ClientRuntime} instances. It means that closing
+     * a particular {@link org.glassfish.jersey.client.ClientRuntime} instance does not shut down the underlying
+     * connection manager automatically. In such case, the connection manager life-cycle
+     * should be fully managed by the application code. To release all allocated resources,
+     * caller code should especially ensure {@link org.apache.hc.client5.http.io.HttpClientConnectionManager#close()} gets
+     * invoked eventually.
+     * <p>
+     * This property may only be set prior to constructing Apache connector using {@link Apache5ConnectorProvider}.
+     * <p/>
+     * The value MUST be an instance of {@link java.lang.Boolean}.
+     * <p/>
+     * The default value is {@code false}.
+     * <p/>
+     * The name of the configuration property is <tt>{@value}</tt>.
+     *
+     * @since 2.18
+     */
+    public static final String CONNECTION_MANAGER_SHARED = "jersey.config.apache5.client.connectionManagerShared";
+
+    /**
+     * Request configuration for the {@link org.apache.hc.client5.http.classic.HttpClient}.
+     * Http parameters which will be used to create {@link org.apache.hc.client5.http.classic.HttpClient}.
+     * <p/>
+     * The value MUST be an instance of {@link org.apache.hc.client5.http.config.RequestConfig}.
+     * <p/>
+     * If the property is absent default request configuration will be used.
+     * <p/>
+     * The name of the configuration property is <tt>{@value}</tt>.
+     *
+     * @since 2.5
+     */
+    public static final String REQUEST_CONFIG = "jersey.config.apache5.client.requestConfig";
+
+    /**
+     * HttpRequestRetryHandler which will be used to create {@link org.apache.hc.client5.http.classic.HttpClient}.
+     * <p/>
+     * The value MUST be an instance of {@link org.apache.hc.client5.http.HttpRequestRetryStrategy}.
+     * <p/>
+     * If the property is absent a default retry handler will be used
+     * ({@link org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy}).
+     * <p/>
+     * The name of the configuration property is <tt>{@value}</tt>.
+     */
+    public static final String RETRY_STRATEGY = "jersey.config.apache5.client.retryStrategy";
+
+    /**
+     * ConnectionReuseStrategy for the {@link org.apache.hc.client5.http.classic.HttpClient}.
+     * <p/>
+     * The value MUST be an instance of {@link org.apache.hc.core5.http.ConnectionReuseStrategy}.
+     * <p/>
+     * If the property is absent the default reuse strategy of the Apache HTTP library will be used
+     * <p/>
+     * The name of the configuration property is <tt>{@value}</tt>.
+     */
+    public static final String REUSE_STRATEGY = "jersey.config.apache5.client.reuseStrategy";
+
+    /**
+     * ConnectionKeepAliveStrategy for the {@link org.apache.hc.client5.http.classic.HttpClient}.
+     * <p/>
+     * The value MUST be an instance of {@link org.apache.hc.client5.http.ConnectionKeepAliveStrategy}.
+     * <p/>
+     * If the property is absent the default keepalive strategy of the Apache HTTP library will be used
+     * <p/>
+     * The name of the configuration property is <tt>{@value}</tt>.
+     */
+    public static final String KEEPALIVE_STRATEGY = "jersey.config.apache5.client.keepAliveStrategy";
+
+
+    /**
+     * Strategy that closes the Apache Connection. Accepts an instance of {@link Apache5ConnectionClosingStrategy}.
+     *
+     * @see Apache5ConnectionClosingStrategy
+     * @since 2.30
+     */
+    public static final String CONNECTION_CLOSING_STRATEGY = "jersey.config.apache5.client.connectionClosingStrategy";
+
+    /**
+     * A value of {@code false} indicates the client will use default ApacheConnector params. A value
+     * of {@code true} will cause the client to take into account the system properties
+     * {@code https.protocols}, {@code https.cipherSuites}, {@code http.keepAlive},
+     * {@code http.maxConnections}.
+     * <p/>
+     * The value MUST be an instance of {@link java.lang.Boolean}.
+     * <p/>
+     * The default value is {@code false}.
+     * <p/>
+     * The name of the configuration property is <tt>{@value}</tt>.
+     */
+    public static final String USE_SYSTEM_PROPERTIES = "jersey.config.apache5.client.useSystemProperties";
+
+    /**
+     * Get the value of the specified property.
+     *
+     * If the property is not set or the actual property value type is not compatible with the specified type, the method will
+     * return {@code null}.
+     *
+     * @param properties    Map of properties to get the property value from.
+     * @param key           Name of the property.
+     * @param type          Type to retrieve the value as.
+     * @param <T>           Type of the property value.
+     * @return Value of the property or {@code null}.
+     *
+     * @since 2.8
+     */
+    public static <T> T getValue(final Map<String, ?> properties, final String key, final Class<T> type) {
+        return PropertiesHelper.getValue(properties, key, type, null);
+    }
+
+    /**
+     * Prevents instantiation.
+     */
+    private Apache5ClientProperties() {
+        throw new AssertionError("No instances allowed.");
+    }
+}
diff --git a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ConnectionClosingStrategy.java b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ConnectionClosingStrategy.java
new file mode 100644
index 0000000..ad23f2d
--- /dev/null
+++ b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ConnectionClosingStrategy.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.glassfish.jersey.client.ClientRequest;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+
+/**
+ * Strategy that defines the way the Apache client releases resources. The client enables closing the content stream
+ * and the response. From the Apache documentation:
+ * <pre>
+ *     The difference between closing the content stream and closing the response is that
+ *     the former will attempt to keep the underlying connection alive by consuming the
+ *     entity content while the latter immediately shuts down and discards the connection.
+ * </pre>
+ * In the case of Chunk content stream, the stream is not closed on the server side, and the client can hang on reading
+ * the closing chunk. Using the {@link org.glassfish.jersey.client.ClientProperties#READ_TIMEOUT} property can prevent
+ * this hanging forever and the reading of the closing chunk is terminated when the time is out. The other option, when
+ * the timeout is not set, is to abort the Apache client request. This is the default for Apache Client 4.5.1+ when the
+ * read timeout is not set.
+ * <p/>
+ * Another option is not to close the content stream, which is possible by the Apache client documentation. In this case,
+ * however, the server side may not be notified and would not close its chunk stream.
+ */
+public interface Apache5ConnectionClosingStrategy {
+    /**
+     * Method to close the connection.
+     * @param clientRequest The {@link ClientRequest} to get {@link ClientRequest#getConfiguration() configuration},
+     *                      and {@link ClientRequest#resolveProperty(String, Class) resolve properties}.
+     * @param request Apache {@code HttpUriRequest} that can be {@code abort}ed.
+     * @param response Apache {@code CloseableHttpResponse} that can be {@code close}d.
+     * @param stream The entity stream that can be {@link InputStream#close() closed}.
+     * @throws IOException In case of some of the closing methods throws {@link IOException}
+     */
+    void close(ClientRequest clientRequest, HttpUriRequest request, CloseableHttpResponse response, InputStream stream)
+            throws IOException;
+
+    /**
+     * Strategy that aborts Apache HttpRequests for the case of Chunked Stream, closes the stream, and response next.
+     */
+    class Apache5GracefulClosingStrategy implements Apache5ConnectionClosingStrategy {
+        private static final String UNIX_PROTOCOL = "unix";
+
+        static final Apache5GracefulClosingStrategy INSTANCE = new Apache5GracefulClosingStrategy();
+
+        @Override
+        public void close(ClientRequest clientRequest, HttpUriRequest request, CloseableHttpResponse response, InputStream stream)
+                throws IOException {
+            boolean isUnixProtocol = false;
+            try {
+                isUnixProtocol = UNIX_PROTOCOL.equals(request.getUri().getScheme());
+            } catch (URISyntaxException ex) {
+                // Ignore
+            }
+            if (response.getEntity() != null && response.getEntity().isChunked() && !isUnixProtocol) {
+                request.abort();
+            }
+            try {
+                stream.close();
+            } catch (IOException ex) {
+                // Ignore
+            } finally {
+                response.close();
+            }
+        }
+    }
+}
diff --git a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5Connector.java b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5Connector.java
new file mode 100644
index 0000000..13b8279
--- /dev/null
+++ b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5Connector.java
@@ -0,0 +1,814 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.ProcessingException;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.core.Configuration;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+
+import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
+import org.apache.hc.client5.http.auth.AuthCache;
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.CredentialsProvider;
+import org.apache.hc.client5.http.auth.CredentialsStore;
+import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.cookie.BasicCookieStore;
+import org.apache.hc.client5.http.cookie.CookieStore;
+import org.apache.hc.client5.http.cookie.StandardCookieSpec;
+import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
+import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
+import org.apache.hc.client5.http.impl.auth.BasicScheme;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
+import org.apache.hc.client5.http.socket.LayeredConnectionSocketFactory;
+import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
+import org.apache.hc.core5.http.ConnectionReuseStrategy;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.config.Http1Config;
+import org.apache.hc.core5.http.config.Registry;
+import org.apache.hc.core5.http.config.RegistryBuilder;
+import org.apache.hc.core5.http.impl.DefaultContentLengthStrategy;
+import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
+import org.apache.hc.core5.http.io.entity.BufferedHttpEntity;
+import org.apache.hc.core5.ssl.SSLContexts;
+import org.apache.hc.core5.util.TextUtils;
+import org.apache.hc.core5.util.Timeout;
+import org.apache.hc.core5.util.VersionInfo;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.ClientResponse;
+import org.glassfish.jersey.client.RequestEntityProcessing;
+import org.glassfish.jersey.client.spi.AsyncConnectorCallback;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+import org.glassfish.jersey.message.internal.HeaderUtils;
+import org.glassfish.jersey.message.internal.OutboundMessageContext;
+import org.glassfish.jersey.message.internal.ReaderWriter;
+import org.glassfish.jersey.message.internal.Statuses;
+
+/**
+ * A {@link Connector} that utilizes the Apache HTTP Client to send and receive
+ * HTTP request and responses.
+ * <p/>
+ * The following properties are only supported at construction of this class:
+ * <ul>
+ * <li>{@link Apache5ClientProperties#CONNECTION_MANAGER}</li>
+ * <li>{@link Apache5ClientProperties#REQUEST_CONFIG}</li>
+ * <li>{@link Apache5ClientProperties#CREDENTIALS_PROVIDER}</li>
+ * <li>{@link Apache5ClientProperties#DISABLE_COOKIES}</li>
+ * <li>{@link Apache5ClientProperties#KEEPALIVE_STRATEGY}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_URI}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_USERNAME}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_PASSWORD}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#REQUEST_ENTITY_PROCESSING} - default value is {@link org.glassfish.jersey.client.RequestEntityProcessing#CHUNKED}</li>
+ * <li>{@link Apache5ClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION}</li>
+ * <li>{@link Apache5ClientProperties#RETRY_STRATEGY}</li>
+ * <li>{@link Apache5ClientProperties#REUSE_STRATEGY}</li>
+ * </ul>
+ * <p>
+ * This connector uses {@link RequestEntityProcessing#CHUNKED chunked encoding} as a default setting. This can
+ * be overridden by the {@link ClientProperties#REQUEST_ENTITY_PROCESSING}. By default the
+ * {@link ClientProperties#CHUNKED_ENCODING_SIZE} property is only supported by using default connection manager. If custom
+ * connection manager needs to be used then chunked encoding size can be set by providing a custom
+ * {@link org.apache.hc.core5.http.io.HttpClientConnection} (via custom {@link org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory})
+ * and overriding {@code createOutputStream} method.
+ * </p>
+ * <p>
+ * Using of authorization is dependent on the chunk encoding setting. If the entity
+ * buffering is enabled, the entity is buffered and authorization can be performed
+ * automatically in response to a 401 by sending the request again. When entity buffering
+ * is disabled (chunked encoding is used) then the property
+ * {@link Apache5ClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION} must
+ * be set to {@code true}.
+ * </p>
+ * <p>
+ * Registration of {@link Apache5HttpClientBuilderConfigurator} instance on the
+ * {@link javax.ws.rs.client.Client#register(Object) Client} is supported. A configuration provided by
+ * {@link Apache5HttpClientBuilderConfigurator} will override the {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder}
+ * configuration set by using the properties.
+ * </p>
+ * <p>
+ * If a {@link org.glassfish.jersey.client.ClientResponse} is obtained and an
+ * entity is not read from the response then
+ * {@link org.glassfish.jersey.client.ClientResponse#close()} MUST be called
+ * after processing the response to release connection-based resources.
+ * </p>
+ * <p>
+ * Client operations are thread safe, the HTTP connection may
+ * be shared between different threads.
+ * </p>
+ * <p>
+ * If a response entity is obtained that is an instance of {@link Closeable}
+ * then the instance MUST be closed after processing the entity to release
+ * connection-based resources.
+ * </p>
+ * <p>
+ * The following methods are currently supported: HEAD, GET, POST, PUT, DELETE, OPTIONS, PATCH and TRACE.
+ * </p>
+ *
+ * @author jorgeluisw@mac.com
+ * @author Paul Sandoz
+ * @author Pavel Bucek
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Steffen Nießing
+ * @see Apache5ClientProperties#CONNECTION_MANAGER
+ */
+class Apache5Connector implements Connector {
+
+    private static final Logger LOGGER = Logger.getLogger(Apache5Connector.class.getName());
+    private static final VersionInfo vi;
+    private static final String release;
+
+    static {
+        vi = VersionInfo.loadVersionInfo("org.apache.hc.client5", HttpClientBuilder.class.getClassLoader());
+        release = (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE;
+    }
+
+    private final CloseableHttpClient client;
+    private final CookieStore cookieStore;
+    private final boolean preemptiveBasicAuth;
+    private final RequestConfig requestConfig;
+
+    /**
+     * Create the new Apache HTTP Client connector.
+     *
+     * @param client JAX-RS client instance for which the connector is being created.
+     * @param config client configuration.
+     */
+    Apache5Connector(final Client client, final Configuration config) {
+        final Object connectionManager = config.getProperties().get(Apache5ClientProperties.CONNECTION_MANAGER);
+        if (connectionManager != null) {
+            if (!(connectionManager instanceof HttpClientConnectionManager)) {
+                LOGGER.log(
+                        Level.WARNING,
+                        LocalizationMessages.IGNORING_VALUE_OF_PROPERTY(
+                                Apache5ClientProperties.CONNECTION_MANAGER,
+                                connectionManager.getClass().getName(),
+                                HttpClientConnectionManager.class.getName())
+                );
+            }
+        }
+
+        Object keepAliveStrategy = config.getProperties().get(Apache5ClientProperties.KEEPALIVE_STRATEGY);
+        if (keepAliveStrategy != null) {
+            if (!(keepAliveStrategy instanceof ConnectionKeepAliveStrategy)) {
+                LOGGER.log(
+                        Level.WARNING,
+                        LocalizationMessages.IGNORING_VALUE_OF_PROPERTY(
+                                Apache5ClientProperties.KEEPALIVE_STRATEGY,
+                                keepAliveStrategy.getClass().getName(),
+                                ConnectionKeepAliveStrategy.class.getName())
+                );
+                keepAliveStrategy = null;
+            }
+        }
+
+        Object reuseStrategy = config.getProperties().get(Apache5ClientProperties.REUSE_STRATEGY);
+        if (reuseStrategy != null) {
+            if (!(reuseStrategy instanceof ConnectionReuseStrategy)) {
+                LOGGER.log(
+                        Level.WARNING,
+                        LocalizationMessages.IGNORING_VALUE_OF_PROPERTY(
+                                Apache5ClientProperties.REUSE_STRATEGY,
+                                reuseStrategy.getClass().getName(),
+                                ConnectionReuseStrategy.class.getName())
+                );
+                reuseStrategy = null;
+            }
+        }
+
+        Object reqConfig = config.getProperties().get(Apache5ClientProperties.REQUEST_CONFIG);
+        if (reqConfig != null) {
+            if (!(reqConfig instanceof RequestConfig)) {
+                LOGGER.log(
+                        Level.WARNING,
+                        LocalizationMessages.IGNORING_VALUE_OF_PROPERTY(
+                                Apache5ClientProperties.REQUEST_CONFIG,
+                                reqConfig.getClass().getName(),
+                                RequestConfig.class.getName())
+                );
+                reqConfig = null;
+            }
+        }
+
+        final SSLContext sslContext = client.getSslContext();
+        final HttpClientBuilder clientBuilder = HttpClientBuilder.create();
+
+        clientBuilder.setConnectionManager(getConnectionManager(client, config, sslContext));
+        clientBuilder.setConnectionManagerShared(
+                PropertiesHelper.getValue(
+                        config.getProperties(),
+                        Apache5ClientProperties.CONNECTION_MANAGER_SHARED,
+                        false,
+                        null
+                )
+        );
+        if (keepAliveStrategy != null) {
+            clientBuilder.setKeepAliveStrategy((ConnectionKeepAliveStrategy) keepAliveStrategy);
+        }
+        if (reuseStrategy != null) {
+            clientBuilder.setConnectionReuseStrategy((ConnectionReuseStrategy) reuseStrategy);
+        }
+
+        final RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
+
+        final Object credentialsProvider = config.getProperty(Apache5ClientProperties.CREDENTIALS_PROVIDER);
+        if (credentialsProvider != null && (credentialsProvider instanceof CredentialsProvider)) {
+            clientBuilder.setDefaultCredentialsProvider((CredentialsProvider) credentialsProvider);
+        }
+
+        final Object retryHandler = config.getProperties().get(Apache5ClientProperties.RETRY_STRATEGY);
+        if (retryHandler != null && (retryHandler instanceof HttpRequestRetryStrategy)) {
+            clientBuilder.setRetryStrategy((HttpRequestRetryStrategy) retryHandler);
+        }
+
+        final Object proxyUri;
+        proxyUri = config.getProperty(ClientProperties.PROXY_URI);
+        if (proxyUri != null) {
+            final URI u = getProxyUri(proxyUri);
+            final HttpHost proxy = new HttpHost(u.getScheme(), u.getHost(), u.getPort());
+            final String userName;
+            userName = ClientProperties.getValue(config.getProperties(), ClientProperties.PROXY_USERNAME, String.class);
+            if (userName != null) {
+                final String password;
+                password = ClientProperties.getValue(config.getProperties(), ClientProperties.PROXY_PASSWORD, String.class);
+
+                if (password != null) {
+                    final CredentialsStore credsProvider = new BasicCredentialsProvider();
+                    credsProvider.setCredentials(
+                            new AuthScope(u.getHost(), u.getPort()),
+                            new UsernamePasswordCredentials(userName, password.toCharArray())
+                    );
+                    clientBuilder.setDefaultCredentialsProvider(credsProvider);
+                }
+            }
+            clientBuilder.setProxy(proxy);
+        }
+
+        final Boolean preemptiveBasicAuthProperty = (Boolean) config.getProperties()
+                .get(Apache5ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION);
+        this.preemptiveBasicAuth = (preemptiveBasicAuthProperty != null) ? preemptiveBasicAuthProperty : false;
+
+        final boolean ignoreCookies = PropertiesHelper.isProperty(
+                config.getProperties(),
+                Apache5ClientProperties.DISABLE_COOKIES
+        );
+
+        if (reqConfig != null) {
+            final RequestConfig.Builder reqConfigBuilder = RequestConfig.copy((RequestConfig) reqConfig);
+            if (ignoreCookies) {
+                reqConfigBuilder.setCookieSpec(StandardCookieSpec.IGNORE);
+            }
+            requestConfig = reqConfigBuilder.build();
+        } else {
+            if (ignoreCookies) {
+                requestConfigBuilder.setCookieSpec(StandardCookieSpec.IGNORE);
+            }
+            requestConfig = requestConfigBuilder.build();
+        }
+
+        if (requestConfig.getCookieSpec() == null || !requestConfig.getCookieSpec().equals(StandardCookieSpec.IGNORE)) {
+            this.cookieStore = new BasicCookieStore();
+            clientBuilder.setDefaultCookieStore(cookieStore);
+        } else {
+            this.cookieStore = null;
+        }
+        clientBuilder.setDefaultRequestConfig(requestConfig);
+
+        LinkedList<Object> contracts = config.getInstances().stream()
+                .filter(Apache5HttpClientBuilderConfigurator.class::isInstance)
+                .collect(Collectors.toCollection(LinkedList::new));
+
+        HttpClientBuilder configuredBuilder = clientBuilder;
+        for (Object configurator : contracts) {
+            configuredBuilder = ((Apache5HttpClientBuilderConfigurator) configurator).configure(configuredBuilder);
+        }
+
+        this.client = configuredBuilder.build();
+    }
+
+    private HttpClientConnectionManager getConnectionManager(final Client client,
+                                                             final Configuration config,
+                                                             final SSLContext sslContext) {
+        final Object cmObject = config.getProperties().get(Apache5ClientProperties.CONNECTION_MANAGER);
+
+        // Connection manager from configuration.
+        if (cmObject != null) {
+            if (cmObject instanceof HttpClientConnectionManager) {
+                return (HttpClientConnectionManager) cmObject;
+            } else {
+                LOGGER.log(
+                        Level.WARNING,
+                        LocalizationMessages.IGNORING_VALUE_OF_PROPERTY(
+                                Apache5ClientProperties.CONNECTION_MANAGER,
+                                cmObject.getClass().getName(),
+                                HttpClientConnectionManager.class.getName())
+                );
+            }
+        }
+
+        final boolean useSystemProperties =
+            PropertiesHelper.isProperty(config.getProperties(), Apache5ClientProperties.USE_SYSTEM_PROPERTIES);
+
+        // Create custom connection manager.
+        return createConnectionManager(
+                client,
+                config,
+                sslContext,
+            useSystemProperties);
+    }
+
+    private HttpClientConnectionManager createConnectionManager(
+            final Client client,
+            final Configuration config,
+            final SSLContext sslContext,
+            final boolean useSystemProperties) {
+
+        final String[] supportedProtocols = useSystemProperties ? split(
+                System.getProperty("https.protocols")) : null;
+        final String[] supportedCipherSuites = useSystemProperties ? split(
+                System.getProperty("https.cipherSuites")) : null;
+
+        HostnameVerifier hostnameVerifier = client.getHostnameVerifier();
+
+        final LayeredConnectionSocketFactory sslSocketFactory;
+        if (sslContext != null) {
+            sslSocketFactory = new SSLConnectionSocketFactory(
+                    sslContext, supportedProtocols, supportedCipherSuites, hostnameVerifier);
+        } else {
+            if (useSystemProperties) {
+                sslSocketFactory = new SSLConnectionSocketFactory(
+                        (SSLSocketFactory) SSLSocketFactory.getDefault(),
+                        supportedProtocols, supportedCipherSuites, hostnameVerifier);
+            } else {
+                sslSocketFactory = new SSLConnectionSocketFactory(
+                        SSLContexts.createDefault(),
+                        hostnameVerifier);
+            }
+        }
+
+        final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
+                .register("http", PlainConnectionSocketFactory.getSocketFactory())
+                .register("https", sslSocketFactory)
+                .build();
+
+        final Integer chunkSize = ClientProperties.getValue(config.getProperties(),
+                ClientProperties.CHUNKED_ENCODING_SIZE, ClientProperties.DEFAULT_CHUNK_SIZE, Integer.class);
+
+        final PoolingHttpClientConnectionManager connectionManager =
+                new PoolingHttpClientConnectionManager(registry, new ConnectionFactory(chunkSize));
+
+        if (useSystemProperties) {
+            String s = System.getProperty("http.keepAlive", "true");
+            if ("true".equalsIgnoreCase(s)) {
+                s = System.getProperty("http.maxConnections", "5");
+                final int max = Integer.parseInt(s);
+                connectionManager.setDefaultMaxPerRoute(max);
+                connectionManager.setMaxTotal(2 * max);
+            }
+        }
+
+        return connectionManager;
+    }
+
+    private static String[] split(final String s) {
+        if (TextUtils.isBlank(s)) {
+            return null;
+        }
+        return s.split(" *, *");
+    }
+
+    /**
+     * Get the {@link HttpClient}.
+     *
+     * @return the {@link HttpClient}.
+     */
+    @SuppressWarnings("UnusedDeclaration")
+    public HttpClient getHttpClient() {
+        return client;
+    }
+
+    /**
+     * Get the {@link CookieStore}.
+     *
+     * @return the {@link CookieStore} instance or {@code null} when {@value Apache5ClientProperties#DISABLE_COOKIES} set to
+     * {@code true}.
+     */
+    public CookieStore getCookieStore() {
+        return cookieStore;
+    }
+
+    private static URI getProxyUri(final Object proxy) {
+        if (proxy instanceof URI) {
+            return (URI) proxy;
+        } else if (proxy instanceof String) {
+            return URI.create((String) proxy);
+        } else {
+            throw new ProcessingException(LocalizationMessages.WRONG_PROXY_URI_TYPE(ClientProperties.PROXY_URI));
+        }
+    }
+
+    @Override
+    public ClientResponse apply(final ClientRequest clientRequest) throws ProcessingException {
+        final HttpUriRequest request = getUriHttpRequest(clientRequest);
+        final Map<String, String> clientHeadersSnapshot = writeOutBoundHeaders(clientRequest, request);
+
+        try {
+            final CloseableHttpResponse response;
+            final HttpClientContext context = HttpClientContext.create();
+            if (preemptiveBasicAuth) {
+                final AuthCache authCache = new BasicAuthCache();
+                final BasicScheme basicScheme = new BasicScheme();
+                authCache.put(getHost(request), basicScheme);
+                context.setAuthCache(authCache);
+            }
+
+            // If a request-specific CredentialsProvider exists, use it instead of the default one
+            CredentialsProvider credentialsProvider =
+                    clientRequest.resolveProperty(Apache5ClientProperties.CREDENTIALS_PROVIDER, CredentialsProvider.class);
+            if (credentialsProvider != null) {
+                context.setCredentialsProvider(credentialsProvider);
+            }
+
+            response = client.execute(getHost(request), request, context);
+            HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, clientRequest.getHeaders(),
+                    this.getClass().getName(), clientRequest.getConfiguration());
+
+            final Response.StatusType status = response.getReasonPhrase() == null
+                    ? Statuses.from(response.getCode())
+                    : Statuses.from(response.getCode(), response.getReasonPhrase());
+
+            final ClientResponse responseContext = new ClientResponse(status, clientRequest);
+            final List<URI> redirectLocations = context.getRedirectLocations().getAll();
+            if (redirectLocations != null && !redirectLocations.isEmpty()) {
+                responseContext.setResolvedRequestUri(redirectLocations.get(redirectLocations.size() - 1));
+            }
+
+            final Header[] respHeaders = response.getHeaders();
+            final MultivaluedMap<String, String> headers = responseContext.getHeaders();
+            for (final Header header : respHeaders) {
+                final String headerName = header.getName();
+                List<String> list = headers.get(headerName);
+                if (list == null) {
+                    list = new ArrayList<>();
+                }
+                list.add(header.getValue());
+                headers.put(headerName, list);
+            }
+
+            final HttpEntity entity = response.getEntity();
+
+            if (entity != null) {
+                if (headers.get(HttpHeaders.CONTENT_LENGTH) == null) {
+                    headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(entity.getContentLength()));
+                }
+
+                final String contentEncoding = entity.getContentEncoding();
+                if (headers.get(HttpHeaders.CONTENT_ENCODING) == null && contentEncoding != null && !contentEncoding.isEmpty()) {
+                    headers.add(HttpHeaders.CONTENT_ENCODING, contentEncoding);
+                }
+            }
+
+            try {
+                final ConnectionClosingMechanism closingMechanism = new ConnectionClosingMechanism(clientRequest, request);
+                responseContext.setEntityStream(getInputStream(response, closingMechanism));
+            } catch (final IOException e) {
+                LOGGER.log(Level.SEVERE, null, e);
+            }
+
+            return responseContext;
+        } catch (final Exception e) {
+            throw new ProcessingException(e);
+        }
+    }
+
+    @Override
+    public Future<?> apply(final ClientRequest request, final AsyncConnectorCallback callback) {
+        try {
+            ClientResponse response = apply(request);
+            callback.response(response);
+            return CompletableFuture.completedFuture(response);
+        } catch (Throwable t) {
+            callback.failure(t);
+            CompletableFuture<Object> future = new CompletableFuture<>();
+            future.completeExceptionally(t);
+            return future;
+        }
+    }
+
+    @Override
+    public String getName() {
+        return "Apache HttpClient " + release;
+    }
+
+    @Override
+    public void close() {
+        try {
+            client.close();
+        } catch (final IOException e) {
+            throw new ProcessingException(LocalizationMessages.FAILED_TO_STOP_CLIENT(), e);
+        }
+    }
+
+    private HttpHost getHost(final HttpUriRequest request) throws URISyntaxException {
+        return new HttpHost(request.getUri().getScheme(), request.getUri().getHost(), request.getUri().getPort());
+    }
+
+    private HttpUriRequest getUriHttpRequest(final ClientRequest clientRequest) {
+        final RequestConfig.Builder requestConfigBuilder = RequestConfig.copy(requestConfig);
+
+        final int connectTimeout = clientRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, -1);
+        final int socketTimeout = clientRequest.resolveProperty(ClientProperties.READ_TIMEOUT, -1);
+
+        if (connectTimeout >= 0) {
+            requestConfigBuilder.setConnectTimeout(Timeout.ofMilliseconds(connectTimeout));
+        }
+        if (socketTimeout >= 0) {
+            requestConfigBuilder.setResponseTimeout(Timeout.ofMilliseconds(socketTimeout));
+        }
+
+        final Boolean redirectsEnabled =
+                clientRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, requestConfig.isRedirectsEnabled());
+        requestConfigBuilder.setRedirectsEnabled(redirectsEnabled);
+
+        final Boolean bufferingEnabled = clientRequest.resolveProperty(ClientProperties.REQUEST_ENTITY_PROCESSING,
+                RequestEntityProcessing.class) == RequestEntityProcessing.BUFFERED;
+        final HttpEntity entity = getHttpEntity(clientRequest, bufferingEnabled);
+
+        HttpUriRequestBase httpUriRequestBase = new HttpUriRequestBase(clientRequest.getMethod(), clientRequest.getUri());
+        httpUriRequestBase.setConfig(requestConfigBuilder.build());
+        httpUriRequestBase.setEntity(entity);
+
+        return httpUriRequestBase;
+    }
+
+    private HttpEntity getHttpEntity(final ClientRequest clientRequest, final boolean bufferingEnabled) {
+        final Object entity = clientRequest.getEntity();
+
+        if (entity == null) {
+            return null;
+        }
+
+        if (HttpEntity.class.isInstance(entity)) {
+            return wrapHttpEntity(clientRequest, (HttpEntity) entity);
+        }
+
+        String contentType = clientRequest.getHeaderString(HttpHeaders.CONTENT_TYPE);
+        String contentEncoding = clientRequest.getHeaderString(HttpHeaders.CONTENT_ENCODING);
+
+        final AbstractHttpEntity httpEntity = new AbstractHttpEntity(contentType, contentEncoding) {
+            @Override
+            public void close() throws IOException {
+
+            }
+
+            @Override
+            public boolean isRepeatable() {
+                return false;
+            }
+
+            @Override
+            public long getContentLength() {
+                return -1;
+            }
+
+            @Override
+            public InputStream getContent() throws IOException, IllegalStateException {
+                if (bufferingEnabled) {
+                    final ByteArrayOutputStream buffer = new ByteArrayOutputStream(512);
+                    writeTo(buffer);
+                    return new ByteArrayInputStream(buffer.toByteArray());
+                } else {
+                    return null;
+                }
+            }
+
+            @Override
+            public void writeTo(final OutputStream outputStream) throws IOException {
+                clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {
+                    @Override
+                    public OutputStream getOutputStream(final int contentLength) throws IOException {
+                        return outputStream;
+                    }
+                });
+                clientRequest.writeEntity();
+            }
+
+            @Override
+            public boolean isStreaming() {
+                return false;
+            }
+        };
+
+        return bufferEntity(httpEntity, bufferingEnabled);
+    }
+
+    private HttpEntity wrapHttpEntity(final ClientRequest clientRequest, final HttpEntity originalEntity) {
+        final boolean bufferingEnabled = BufferedHttpEntity.class.isInstance(originalEntity);
+
+        try {
+            clientRequest.setEntity(originalEntity.getContent());
+        } catch (IOException e) {
+            throw new ProcessingException(LocalizationMessages.ERROR_READING_HTTPENTITY_STREAM(e.getMessage()), e);
+        }
+
+        final AbstractHttpEntity httpEntity = new AbstractHttpEntity(
+                originalEntity.getContentType(),
+                originalEntity.getContentEncoding(),
+                originalEntity.isChunked()
+        ) {
+            @Override
+            public void close() throws IOException {
+
+            }
+
+            @Override
+            public boolean isRepeatable() {
+                return originalEntity.isRepeatable();
+            }
+
+            @Override
+            public long getContentLength() {
+                return originalEntity.getContentLength();
+            }
+
+            @Override
+            public InputStream getContent() throws IOException, IllegalStateException {
+               return originalEntity.getContent();
+            }
+
+            @Override
+            public void writeTo(final OutputStream outputStream) throws IOException {
+                clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {
+                    @Override
+                    public OutputStream getOutputStream(final int contentLength) throws IOException {
+                        return outputStream;
+                    }
+                });
+                clientRequest.writeEntity();
+            }
+
+            @Override
+            public boolean isStreaming() {
+                return originalEntity.isStreaming();
+            }
+        };
+
+        return bufferEntity(httpEntity, bufferingEnabled);
+    }
+
+    private static HttpEntity bufferEntity(HttpEntity httpEntity, boolean bufferingEnabled) {
+        if (bufferingEnabled) {
+            try {
+                return new BufferedHttpEntity(httpEntity);
+            } catch (final IOException e) {
+                throw new ProcessingException(LocalizationMessages.ERROR_BUFFERING_ENTITY(), e);
+            }
+        } else {
+            return httpEntity;
+        }
+    }
+
+    private static Map<String, String> writeOutBoundHeaders(final ClientRequest clientRequest,
+                                                            final HttpUriRequest request) {
+        final Map<String, String> stringHeaders =
+                HeaderUtils.asStringHeadersSingleValue(clientRequest.getHeaders(), clientRequest.getConfiguration());
+
+        for (final Map.Entry<String, String> e : stringHeaders.entrySet()) {
+            request.addHeader(e.getKey(), e.getValue());
+        }
+        return stringHeaders;
+    }
+
+    private static InputStream getInputStream(final CloseableHttpResponse response,
+                                              final ConnectionClosingMechanism closingMechanism) throws IOException {
+        final InputStream inputStream;
+
+        if (response.getEntity() == null) {
+            inputStream = new ByteArrayInputStream(new byte[0]);
+        } else {
+            final InputStream i = response.getEntity().getContent();
+            if (i.markSupported()) {
+                inputStream = i;
+            } else {
+                inputStream = new BufferedInputStream(i, ReaderWriter.BUFFER_SIZE);
+            }
+        }
+
+        return closingMechanism.getEntityStream(inputStream, response);
+    }
+
+    /**
+     * The way the Apache CloseableHttpResponse is to be closed.
+     * See https://github.com/eclipse-ee4j/jersey/issues/4321
+     * {@link Apache5ClientProperties#CONNECTION_CLOSING_STRATEGY}
+     */
+    private final class ConnectionClosingMechanism {
+        private Apache5ConnectionClosingStrategy connectionClosingStrategy = null;
+        private final ClientRequest clientRequest;
+        private final HttpUriRequest apacheRequest;
+
+        private ConnectionClosingMechanism(ClientRequest clientRequest, HttpUriRequest apacheRequest) {
+            this.clientRequest = clientRequest;
+            this.apacheRequest = apacheRequest;
+            Object closingStrategyProperty = clientRequest
+                    .resolveProperty(Apache5ClientProperties.CONNECTION_CLOSING_STRATEGY, Object.class);
+            if (closingStrategyProperty != null) {
+                if (Apache5ConnectionClosingStrategy.class.isInstance(closingStrategyProperty)) {
+                    connectionClosingStrategy = (Apache5ConnectionClosingStrategy) closingStrategyProperty;
+                } else {
+                    LOGGER.log(
+                            Level.WARNING,
+                            LocalizationMessages.IGNORING_VALUE_OF_PROPERTY(
+                                    Apache5ClientProperties.CONNECTION_CLOSING_STRATEGY,
+                                    closingStrategyProperty,
+                                    Apache5ConnectionClosingStrategy.class.getName())
+                    );
+                }
+            }
+
+            if (connectionClosingStrategy == null) {
+                connectionClosingStrategy = Apache5ConnectionClosingStrategy.Apache5GracefulClosingStrategy.INSTANCE;
+            }
+        }
+
+        private InputStream getEntityStream(final InputStream inputStream,
+                                            final CloseableHttpResponse response) {
+            InputStream filterStream = new FilterInputStream(inputStream) {
+                @Override
+                public void close() throws IOException {
+                    connectionClosingStrategy.close(clientRequest, apacheRequest, response, in);
+                }
+            };
+            return filterStream;
+        }
+    }
+
+    private static class ConnectionFactory extends ManagedHttpClientConnectionFactory {
+        private ConnectionFactory(final int chunkSize) {
+            super(
+                    Http1Config.custom().setChunkSizeHint(chunkSize).build(),
+                    null,
+                    null,
+                    null,
+                    DefaultContentLengthStrategy.INSTANCE,
+                    DefaultContentLengthStrategy.INSTANCE
+            );
+        }
+    }
+}
diff --git a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ConnectorProvider.java b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ConnectorProvider.java
new file mode 100644
index 0000000..9a32a2c
--- /dev/null
+++ b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5ConnectorProvider.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.core.Configurable;
+import javax.ws.rs.core.Configuration;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.cookie.CookieStore;
+import org.glassfish.jersey.client.Initializable;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.client.spi.ConnectorProvider;
+
+/**
+ * Connector provider for Jersey {@link Connector connectors} that utilize
+ * Apache HTTP Client to send and receive HTTP request and responses.
+ * <p>
+ * The following connector configuration properties are supported:
+ * <ul>
+ * <li>{@link Apache5ClientProperties#CONNECTION_MANAGER}</li>
+ * <li>{@link Apache5ClientProperties#REQUEST_CONFIG}</li>
+ * <li>{@link Apache5ClientProperties#CREDENTIALS_PROVIDER}</li>
+ * <li>{@link Apache5ClientProperties#DISABLE_COOKIES}</li>
+ * <li>{@link Apache5ClientProperties#KEEPALIVE_STRATEGY}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_URI}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_USERNAME}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#PROXY_PASSWORD}</li>
+ * <li>{@link org.glassfish.jersey.client.ClientProperties#REQUEST_ENTITY_PROCESSING}
+ * - default value is {@link org.glassfish.jersey.client.RequestEntityProcessing#CHUNKED}</li>
+ * <li>{@link Apache5ClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION}</li>
+ * <li>{@link Apache5ClientProperties#RETRY_STRATEGY}</li>
+ * <li>{@link Apache5ClientProperties#REUSE_STRATEGY}</li>
+ * </ul>
+ * </p>
+ * <p>
+ * Connector instances created via this connector provider use
+ * {@link org.glassfish.jersey.client.RequestEntityProcessing#CHUNKED chunked encoding} as a default setting.
+ * This can be overridden by the {@link org.glassfish.jersey.client.ClientProperties#REQUEST_ENTITY_PROCESSING}.
+ * By default the {@link org.glassfish.jersey.client.ClientProperties#CHUNKED_ENCODING_SIZE} property is only supported
+ * when using the default {@link org.apache.hc.core5.http.io.HttpClientConnection} instance. If custom
+ * connection manager is used, then chunked encoding size can be set by providing a custom
+ * {@link org.apache.hc.core5.http.io.HttpClientConnection} (via custom {@link org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory})
+ * and overriding it's {@code createOutputStream} method.
+ * </p>
+ * <p>
+ * Use of authorization by the AHC-based connectors is dependent on the chunk encoding setting.
+ * If the entity buffering is enabled, the entity is buffered and authorization can be performed
+ * automatically in response to a 401 by sending the request again. When entity buffering
+ * is disabled (chunked encoding is used) then the property
+ * {@link Apache5ClientProperties#PREEMPTIVE_BASIC_AUTHENTICATION} must
+ * be set to {@code true}.
+ * </p>
+ * <p>
+ * If a {@link org.glassfish.jersey.client.ClientResponse} is obtained and an entity is not read from the response then
+ * {@link org.glassfish.jersey.client.ClientResponse#close()} MUST be called after processing the response to release
+ * connection-based resources.
+ * </p>
+ * <p>
+ * Registration of {@link Apache5HttpClientBuilderConfigurator} instance on the
+ * {@link javax.ws.rs.client.Client#register(Object) Client} is supported. A configuration provided by
+ * {@link Apache5HttpClientBuilderConfigurator} will override the {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder}
+ * configuration set by using the properties.
+ * </p>
+ * <p>
+ * If a response entity is obtained that is an instance of {@link java.io.Closeable}
+ * then the instance MUST be closed after processing the entity to release
+ * connection-based resources.
+ * <p/>
+ * <p>
+ * The following methods are currently supported: HEAD, GET, POST, PUT, DELETE, OPTIONS, PATCH and TRACE.
+ * <p/>
+ *
+ * @author Pavel Bucek
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author jorgeluisw at mac.com
+ * @author Marek Potociar
+ * @author Paul Sandoz
+ * @author Maksim Mukosey (mmukosey at gmail.com)
+ * @since 2.5
+ */
+public class Apache5ConnectorProvider implements ConnectorProvider {
+
+    @Override
+    public Connector getConnector(final Client client, final Configuration runtimeConfig) {
+        return new Apache5Connector(client, runtimeConfig);
+    }
+
+    /**
+     * Retrieve the underlying Apache {@link org.apache.hc.client5.http.classic.HttpClient} instance from
+     * {@link org.glassfish.jersey.client.JerseyClient} or {@link org.glassfish.jersey.client.JerseyWebTarget}
+     * configured to use {@code ApacheConnectorProvider}.
+     *
+     * @param component {@code JerseyClient} or {@code JerseyWebTarget} instance that is configured to use
+     *                  {@code ApacheConnectorProvider}.
+     * @return underlying Apache {@code HttpClient} instance.
+     *
+     * @throws java.lang.IllegalArgumentException in case the {@code component} is neither {@code JerseyClient}
+     *                                            nor {@code JerseyWebTarget} instance or in case the component
+     *                                            is not configured to use a {@code ApacheConnectorProvider}.
+     * @since 2.8
+     */
+    public static HttpClient getHttpClient(final Configurable<?> component) {
+        return getConnector(component).getHttpClient();
+    }
+
+    /**
+     * Retrieve the underlying Apache {@link CookieStore} instance from
+     * {@link org.glassfish.jersey.client.JerseyClient} or {@link org.glassfish.jersey.client.JerseyWebTarget}
+     * configured to use {@code ApacheConnectorProvider}.
+     *
+     * @param component {@code JerseyClient} or {@code JerseyWebTarget} instance that is configured to use
+     *                  {@code ApacheConnectorProvider}.
+     * @return underlying Apache {@code CookieStore} instance.
+     * @throws java.lang.IllegalArgumentException in case the {@code component} is neither {@code JerseyClient}
+     *                                            nor {@code JerseyWebTarget} instance or in case the component
+     *                                            is not configured to use a {@code ApacheConnectorProvider}.
+     * @since 2.16
+     */
+    public static CookieStore getCookieStore(final Configurable<?> component) {
+        return getConnector(component).getCookieStore();
+    }
+
+    private static Apache5Connector getConnector(final Configurable<?> component) {
+        if (!(component instanceof Initializable)) {
+            throw new IllegalArgumentException(
+                    LocalizationMessages.INVALID_CONFIGURABLE_COMPONENT_TYPE(component.getClass().getName()));
+        }
+
+        final Initializable<?> initializable = (Initializable<?>) component;
+        Connector connector = initializable.getConfiguration().getConnector();
+        if (connector == null) {
+            initializable.preInitialize();
+            connector = initializable.getConfiguration().getConnector();
+        }
+
+        if (connector instanceof Apache5Connector) {
+            return (Apache5Connector) connector;
+        } else {
+            throw new IllegalArgumentException(LocalizationMessages.EXPECTED_CONNECTOR_PROVIDER_NOT_USED());
+        }
+    }
+}
diff --git a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5HttpClientBuilderConfigurator.java b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5HttpClientBuilderConfigurator.java
new file mode 100644
index 0000000..d179f5c
--- /dev/null
+++ b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/Apache5HttpClientBuilderConfigurator.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.glassfish.jersey.spi.Contract;
+
+/**
+ * A callback interface used to configure {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder}. It is called immediately before
+ * the {@link Apache5ConnectorProvider} creates {@link org.apache.hc.client5.http.classic.HttpClient}, after the
+ * {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder} is configured using the properties.
+ */
+@Contract
+public interface Apache5HttpClientBuilderConfigurator {
+    /**
+     * A callback method to configure the {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder}
+     * @param httpClientBuilder {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder} object to be further configured
+     * @return the configured {@link org.apache.hc.client5.http.impl.classic.HttpClientBuilder}. If {@code null} is returned the
+     * {@code httpClientBuilder} is used by {@link Apache5ConnectorProvider} instead.
+     */
+    HttpClientBuilder configure(HttpClientBuilder httpClientBuilder);
+}
diff --git a/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/package-info.java b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/package-info.java
new file mode 100644
index 0000000..0b05d4b
--- /dev/null
+++ b/connectors/apache5-connector/src/main/java/org/glassfish/jersey/apache5/connector/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+/**
+ * Jersey client {@link org.glassfish.jersey.client.spi.Connector connector} based on the
+ * Apache Http Client.
+ */
+package org.glassfish.jersey.apache5.connector;
diff --git a/connectors/apache5-connector/src/main/resources/org/glassfish/jersey/apache5/connector/localization.properties b/connectors/apache5-connector/src/main/resources/org/glassfish/jersey/apache5/connector/localization.properties
new file mode 100644
index 0000000..16dbc30
--- /dev/null
+++ b/connectors/apache5-connector/src/main/resources/org/glassfish/jersey/apache5/connector/localization.properties
@@ -0,0 +1,25 @@
+#
+# Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Eclipse Public License v. 2.0, which is available at
+# http://www.eclipse.org/legal/epl-2.0.
+#
+# This Source Code may also be made available under the following Secondary
+# Licenses when the conditions for such availability set forth in the
+# Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+# version 2 with the GNU Classpath Exception, which is available at
+# https://www.gnu.org/software/classpath/license.html.
+#
+# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+#
+
+error.buffering.entity=Error buffering the entity.
+error.reading.httpentity.stream=Error reading InputStream from HttpEntity: "{0}"
+failed.to.stop.client=Failed to stop the client.
+# {0} - property name, e.g. jersey.config.client.httpclient.connectionManager; {1}, {2} - full class name
+ignoring.value.of.property=Ignoring value of property "{0}" ("{1}") - not instance of "{2}".
+# {0} - property name - jersey.config.client.httpclient.proxyUri
+wrong.proxy.uri.type=The proxy URI ("{0}") property MUST be an instance of String or URI.
+invalid.configurable.component.type=The supplied component "{0}" is not assignable from JerseyClient or JerseyWebTarget.
+expected.connector.provider.not.used=The supplied component is not configured to use a ApacheConnectorProvider.
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/AsyncTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/AsyncTest.java
new file mode 100644
index 0000000..50e8f65
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/AsyncTest.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.container.TimeoutHandler;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.hamcrest.Matchers;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Asynchronous connector test.
+ *
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Marek Potociar
+ */
+public class AsyncTest extends JerseyTest {
+    private static final Logger LOGGER = Logger.getLogger(AsyncTest.class.getName());
+    private static final String PATH = "async";
+
+    /**
+     * Asynchronous test resource.
+     */
+    @Path(PATH)
+    public static class AsyncResource {
+        /**
+         * Typical long-running operation duration.
+         */
+        public static final long OPERATION_DURATION = 1000;
+
+        /**
+         * Long-running asynchronous post.
+         *
+         * @param asyncResponse async response.
+         * @param id            post request id (received as request payload).
+         */
+        @POST
+        public void asyncPost(@Suspended final AsyncResponse asyncResponse, final String id) {
+            LOGGER.info("Long running post operation called with id " + id + " on thread " + Thread.currentThread().getName());
+            new Thread(new Runnable() {
+
+                @Override
+                public void run() {
+                    String result = veryExpensiveOperation();
+                    asyncResponse.resume(result);
+                }
+
+                private String veryExpensiveOperation() {
+                    // ... very expensive operation that typically finishes within 1 seconds, simulated using sleep()
+                    try {
+                        Thread.sleep(OPERATION_DURATION);
+                        return "DONE-" + id;
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                        return "INTERRUPTED-" + id;
+                    } finally {
+                        LOGGER.info("Long running post operation finished on thread " + Thread.currentThread().getName());
+                    }
+                }
+            }, "async-post-runner-" + id).start();
+        }
+
+        /**
+         * Long-running async get request that times out.
+         *
+         * @param asyncResponse async response.
+         */
+        @GET
+        @Path("timeout")
+        public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) {
+            LOGGER.info("Async long-running get with timeout called on thread " + Thread.currentThread().getName());
+            asyncResponse.setTimeoutHandler(new TimeoutHandler() {
+
+                @Override
+                public void handleTimeout(AsyncResponse asyncResponse) {
+                    asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE)
+                            .entity("Operation time out.").build());
+                }
+            });
+            asyncResponse.setTimeout(1, TimeUnit.SECONDS);
+
+            new Thread(new Runnable() {
+
+                @Override
+                public void run() {
+                    String result = veryExpensiveOperation();
+                    asyncResponse.resume(result);
+                }
+
+                private String veryExpensiveOperation() {
+                    // very expensive operation that typically finishes within 1 second but can take up to 5 seconds,
+                    // simulated using sleep()
+                    try {
+                        Thread.sleep(5 * OPERATION_DURATION);
+                        return "DONE";
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                        return "INTERRUPTED";
+                    } finally {
+                        LOGGER.info("Async long-running get with timeout finished on thread " + Thread.currentThread().getName());
+                    }
+                }
+            }).start();
+        }
+
+    }
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(AsyncResource.class)
+                .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        config.connectorProvider(new Apache5ConnectorProvider());
+    }
+
+    /**
+     * Test asynchronous POST.
+     *
+     * Send 3 async POST requests and wait to receive the responses. Check the response content and
+     * assert that the operation did not take more than twice as long as a single long operation duration
+     * (this ensures async request execution).
+     *
+     * @throws Exception in case of a test error.
+     */
+    @Test
+    public void testAsyncPost() throws Exception {
+        final long tic = System.currentTimeMillis();
+
+        // Submit requests asynchronously.
+        final Future<Response> rf1 = target(PATH).request().async().post(Entity.text("1"));
+        final Future<Response> rf2 = target(PATH).request().async().post(Entity.text("2"));
+        final Future<Response> rf3 = target(PATH).request().async().post(Entity.text("3"));
+        // get() waits for the response
+
+        // workaround for AHC default connection manager limitation of
+        // only 2 open connections per host that may intermittently block
+        // the test
+        final CountDownLatch latch = new CountDownLatch(3);
+        ExecutorService executor = Executors.newFixedThreadPool(3);
+
+        final Future<String> r1 = executor.submit(new Callable<String>() {
+            @Override
+            public String call() throws Exception {
+                try {
+                    return rf1.get().readEntity(String.class);
+                } finally {
+                    latch.countDown();
+                }
+            }
+        });
+        final Future<String> r2 = executor.submit(new Callable<String>() {
+            @Override
+            public String call() throws Exception {
+                try {
+                    return rf2.get().readEntity(String.class);
+                } finally {
+                    latch.countDown();
+                }
+            }
+        });
+        final Future<String> r3 = executor.submit(new Callable<String>() {
+            @Override
+            public String call() throws Exception {
+                try {
+                    return rf3.get().readEntity(String.class);
+                } finally {
+                    latch.countDown();
+                }
+            }
+        });
+
+        assertTrue("Waiting for results has timed out.", latch.await(5 * getAsyncTimeoutMultiplier(), TimeUnit.SECONDS));
+        final long toc = System.currentTimeMillis();
+
+        assertEquals("DONE-1", r1.get());
+        assertEquals("DONE-2", r2.get());
+        assertEquals("DONE-3", r3.get());
+
+        final int asyncTimeoutMultiplier = getAsyncTimeoutMultiplier();
+        LOGGER.info("Using async timeout multiplier: " + asyncTimeoutMultiplier);
+        assertThat("Async processing took too long.", toc - tic, Matchers.lessThan(4 * AsyncResource.OPERATION_DURATION
+                * asyncTimeoutMultiplier));
+
+    }
+
+    /**
+     * Test accessing an operation that times out on the server.
+     *
+     * @throws Exception in case of a test error.
+     */
+    @Test
+    public void testAsyncGetWithTimeout() throws Exception {
+        final Future<Response> responseFuture = target(PATH).path("timeout").request().async().get();
+        // Request is being processed asynchronously.
+        final Response response = responseFuture.get();
+
+        // get() waits for the response
+        assertEquals(503, response.getStatus());
+        assertEquals("Operation time out.", response.readEntity(String.class));
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/AuthTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/AuthTest.java
new file mode 100644
index 0000000..f59c0cf
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/AuthTest.java
@@ -0,0 +1,595 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import javax.inject.Singleton;
+
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.CredentialsStore;
+import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
+import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
+import org.glassfish.jersey.client.authentication.ResponseAuthenticationException;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class AuthTest extends JerseyTest {
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(PreemptiveAuthResource.class, AuthResource.class);
+    }
+
+    @Path("/")
+    public static class PreemptiveAuthResource {
+
+        @GET
+        public String get(@Context HttpHeaders h) {
+            String value = h.getRequestHeaders().getFirst("Authorization");
+            assertNotNull(value);
+            return "GET";
+        }
+
+        @POST
+        public String post(@Context HttpHeaders h, String e) {
+            String value = h.getRequestHeaders().getFirst("Authorization");
+            assertNotNull(value);
+            return e;
+        }
+    }
+
+    @Test
+    public void testPreemptiveAuth() {
+        CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+        credentialsProvider.setCredentials(
+                new AuthScope("localhost", getPort()),
+                new UsernamePasswordCredentials("name", "password".toCharArray())
+        );
+
+        ClientConfig cc = new ClientConfig();
+        cc.property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider)
+                .property(Apache5ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, true);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+
+        WebTarget r = client.target(getBaseUri());
+        assertEquals("GET", r.request().get(String.class));
+    }
+
+    @Test
+    public void testPreemptiveAuthPost() {
+        CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+        credentialsProvider.setCredentials(
+                new AuthScope("localhost", getPort()),
+                new UsernamePasswordCredentials("name", "password".toCharArray())
+        );
+
+        ClientConfig cc = new ClientConfig();
+        cc.property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider)
+                .property(Apache5ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION, true);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+
+        WebTarget r = client.target(getBaseUri());
+        assertEquals("POST", r.request().post(Entity.text("POST"), String.class));
+    }
+
+    @Path("/test")
+    @Singleton
+    public static class AuthResource {
+
+        int requestCount = 0;
+        int queryParamsBasicRequestCount = 0;
+        int queryParamsDigestRequestCount = 0;
+
+        @GET
+        public String get(@Context HttpHeaders h) {
+            requestCount++;
+            String value = h.getRequestHeaders().getFirst("Authorization");
+            if (value == null) {
+                assertEquals(1, requestCount);
+                throw new WebApplicationException(
+                        Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+            } else {
+                assertTrue(requestCount > 1);
+            }
+
+            return "GET";
+        }
+
+        @GET
+        @Path("filter")
+        public String getFilter(@Context HttpHeaders h) {
+            String value = h.getRequestHeaders().getFirst("Authorization");
+            if (value == null) {
+                throw new WebApplicationException(
+                        Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+            }
+
+            return "GET";
+        }
+
+        @GET
+        @Path("basicAndDigest")
+        public String getBasicAndDigest(@Context HttpHeaders h) {
+            String value = h.getRequestHeaders().getFirst("Authorization");
+            if (value == null) {
+                throw new WebApplicationException(
+                        Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"")
+                            .header("WWW-Authenticate", "Digest realm=\"WallyWorld\"")
+                            .entity("Forbidden").build());
+            } else if (value.startsWith("Basic")) {
+                throw new WebApplicationException(
+                        Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"")
+                            .header("WWW-Authenticate", "Digest realm=\"WallyWorld\"")
+                            .entity("Digest authentication expected").build());
+            }
+
+            return "GET";
+        }
+
+        @GET
+        @Path("noauth")
+        public String get() {
+            return "GET";
+        }
+
+        @GET
+        @Path("digest")
+        public String getDigest(@Context HttpHeaders h) {
+            String value = h.getRequestHeaders().getFirst("Authorization");
+            if (value == null) {
+                throw new WebApplicationException(
+                        Response.status(401).header("WWW-Authenticate", "Digest realm=\"WallyWorld\"")
+                            .entity("Forbidden").build());
+            }
+
+            return "GET";
+        }
+
+        @POST
+        public String post(@Context HttpHeaders h, String e) {
+            requestCount++;
+            String value = h.getRequestHeaders().getFirst("Authorization");
+            if (value == null) {
+                assertEquals(1, requestCount);
+                throw new WebApplicationException(
+                        Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+            } else {
+                assertTrue(requestCount > 1);
+            }
+
+            return e;
+        }
+
+        @POST
+        @Path("filter")
+        public String postFilter(@Context HttpHeaders h, String e) {
+            String value = h.getRequestHeaders().getFirst("Authorization");
+            if (value == null) {
+                throw new WebApplicationException(
+                        Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+            }
+
+            return e;
+        }
+
+        @DELETE
+        public void delete(@Context HttpHeaders h) {
+            requestCount++;
+            String value = h.getRequestHeaders().getFirst("Authorization");
+            if (value == null) {
+                assertEquals(1, requestCount);
+                throw new WebApplicationException(
+                        Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+            } else {
+                assertTrue(requestCount > 1);
+            }
+        }
+
+        @DELETE
+        @Path("filter")
+        public void deleteFilter(@Context HttpHeaders h) {
+            String value = h.getRequestHeaders().getFirst("Authorization");
+            if (value == null) {
+                throw new WebApplicationException(
+                        Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+            }
+        }
+
+        @DELETE
+        @Path("filter/withEntity")
+        public String deleteFilterWithEntity(@Context HttpHeaders h, String e) {
+            String value = h.getRequestHeaders().getFirst("Authorization");
+            if (value == null) {
+                throw new WebApplicationException(
+                        Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+            }
+
+            return e;
+        }
+
+        @GET
+        @Path("content")
+        public String getWithContent(@Context HttpHeaders h) {
+            requestCount++;
+            String value = h.getRequestHeaders().getFirst("Authorization");
+            if (value == null) {
+                assertEquals(1, requestCount);
+                throw new WebApplicationException(
+                        Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"")
+                            .entity("Forbidden").build());
+            } else {
+                assertTrue(requestCount > 1);
+            }
+
+            return "GET";
+        }
+
+        @GET
+        @Path("contentDigestAuth")
+        public String getWithContentDigestAuth(@Context HttpHeaders h) {
+            requestCount++;
+            String value = h.getRequestHeaders().getFirst("Authorization");
+            if (value == null) {
+                assertEquals(1, requestCount);
+                throw new WebApplicationException(
+                        Response.status(401).header("WWW-Authenticate", "Digest nonce=\"1234\"")
+                            .entity("Forbidden").build());
+            } else {
+                assertTrue(requestCount > 1);
+            }
+
+            return "GET";
+        }
+
+        @GET
+        @Path("queryParamsBasic")
+        public String getQueryParamsBasic(@Context HttpHeaders h, @Context UriInfo uriDetails) {
+            queryParamsBasicRequestCount++;
+            String value = h.getRequestHeaders().getFirst("Authorization");
+            if (value == null) {
+                throw new WebApplicationException(
+                        Response.status(401).header("WWW-Authenticate", "Basic realm=\"WallyWorld\"").build());
+            }
+            return "GET " + queryParamsBasicRequestCount;
+        }
+
+        @GET
+        @Path("queryParamsDigest")
+        public String getQueryParamsDigest(@Context HttpHeaders h, @Context UriInfo uriDetails) {
+            queryParamsDigestRequestCount++;
+            String value = h.getRequestHeaders().getFirst("Authorization");
+            if (value == null) {
+                throw new WebApplicationException(
+                        Response.status(401).header("WWW-Authenticate", "Digest realm=\"WallyWorld\"").build());
+            }
+            return "GET " + queryParamsDigestRequestCount;
+        }
+    }
+
+    @Test
+    public void testAuthGet() {
+        CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+        credentialsProvider.setCredentials(
+                new AuthScope("localhost", getPort()),
+                new UsernamePasswordCredentials("name", "password".toCharArray())
+        );
+
+        ClientConfig cc = new ClientConfig();
+        cc.property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        WebTarget r = client.target(getBaseUri()).path("test");
+
+        assertEquals("GET", r.request().get(String.class));
+    }
+
+    @Test
+    public void testAuthGetWithRequestCredentialsProvider() {
+        CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+        credentialsProvider.setCredentials(
+                new AuthScope("localhost", getPort()),
+                new UsernamePasswordCredentials("name", "password".toCharArray())
+        );
+
+        ClientConfig cc = new ClientConfig();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        WebTarget r = client.target(getBaseUri()).path("test");
+
+        assertEquals("GET",
+                     r.request()
+                      .property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider)
+                      .get(String.class));
+    }
+
+    @Test
+    public void testAuthGetWithClientFilter() {
+        ClientConfig cc = new ClientConfig();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        client.register(HttpAuthenticationFeature.basic("name", "password"));
+        WebTarget r = client.target(getBaseUri()).path("test/filter");
+
+        assertEquals("GET", r.request().get(String.class));
+    }
+
+    @Test
+    public void testAuthGetWithBasicAndDigestFilter() {
+        ClientConfig cc = new ClientConfig();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        client.register(HttpAuthenticationFeature.universal("name", "password"));
+        WebTarget r = client.target(getBaseUri()).path("test/basicAndDigest");
+
+        assertEquals("GET", r.request().get(String.class));
+    }
+
+    @Test
+    public void testAuthGetBasicNoChallenge() {
+        ClientConfig cc = new ClientConfig();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        client.register(HttpAuthenticationFeature.basicBuilder().build());
+        WebTarget r = client.target(getBaseUri()).path("test/noauth");
+
+        assertEquals("GET", r.request().get(String.class));
+    }
+
+    @Test
+    public void testAuthGetWithDigestFilter() {
+        ClientConfig cc = new ClientConfig();
+        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        cc.property(Apache5ClientProperties.CONNECTION_MANAGER, cm);
+        Client client = ClientBuilder.newClient(cc);
+        client.register(HttpAuthenticationFeature.universal("name", "password"));
+        WebTarget r = client.target(getBaseUri()).path("test/digest");
+
+        assertEquals("GET", r.request().get(String.class));
+
+        // Verify the connection that was used for the request is available for reuse
+        // and no connections are leased
+        assertEquals(cm.getTotalStats().getAvailable(), 1);
+        assertEquals(cm.getTotalStats().getLeased(), 0);
+    }
+
+    @Test
+    @Ignore("JERSEY-1750: Cannot retry request with a non-repeatable request entity. How to buffer the entity?"
+            + " Allow repeatable write in jersey?")
+    public void testAuthPost() {
+        CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+        credentialsProvider.setCredentials(
+                new AuthScope("localhost", getPort()),
+                new UsernamePasswordCredentials("name", "password".toCharArray())
+        );
+
+        ClientConfig cc = new ClientConfig();
+        cc.property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        WebTarget r = client.target(getBaseUri()).path("test");
+
+        assertEquals("POST", r.request().post(Entity.text("POST"), String.class));
+    }
+
+    @Test
+    public void testAuthPostWithClientFilter() {
+        ClientConfig cc = new ClientConfig();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        client.register(HttpAuthenticationFeature.basic("name", "password"));
+        WebTarget r = client.target(getBaseUri()).path("test/filter");
+
+        assertEquals("POST", r.request().post(Entity.text("POST"), String.class));
+    }
+
+    @Test
+    public void testAuthDelete() {
+        CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+        credentialsProvider.setCredentials(
+                new AuthScope("localhost", getPort()),
+                new UsernamePasswordCredentials("name", "password".toCharArray())
+        );
+        ClientConfig cc = new ClientConfig();
+        cc.property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        WebTarget r = client.target(getBaseUri()).path("test");
+
+        Response response = r.request().delete();
+        assertEquals(response.getStatus(), 204);
+    }
+
+    @Test
+    public void testAuthDeleteWithClientFilter() {
+        ClientConfig cc = new ClientConfig();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        client.register(HttpAuthenticationFeature.basic("name", "password"));
+        WebTarget r = client.target(getBaseUri()).path("test/filter");
+
+        Response response = r.request().delete();
+        assertEquals(204, response.getStatus());
+    }
+
+    @Test
+    public void testAuthInteractiveGet() {
+        CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+        credentialsProvider.setCredentials(
+                new AuthScope("localhost", getPort()),
+                new UsernamePasswordCredentials("name", "password".toCharArray())
+        );
+        ClientConfig cc = new ClientConfig();
+        cc.property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+
+        WebTarget r = client.target(getBaseUri()).path("test");
+
+        assertEquals("GET", r.request().get(String.class));
+    }
+
+    @Test
+    @Ignore("JERSEY-1750: Cannot retry request with a non-repeatable request entity. How to buffer the entity?"
+            + " Allow repeatable write in jersey?")
+    public void testAuthInteractivePost() {
+        CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+        credentialsProvider.setCredentials(
+                new AuthScope("localhost", getPort()),
+                new UsernamePasswordCredentials("name", "password".toCharArray())
+        );
+
+        ClientConfig cc = new ClientConfig();
+        cc.property(Apache5ClientProperties.CREDENTIALS_PROVIDER, credentialsProvider);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        WebTarget r = client.target(getBaseUri()).path("test");
+
+        assertEquals("POST", r.request().post(Entity.text("POST"), String.class));
+    }
+
+    @Test
+    public void testAuthGetWithBasicFilterAndContent() {
+        ClientConfig cc = new ClientConfig();
+        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        cc.property(Apache5ClientProperties.CONNECTION_MANAGER, cm);
+        Client client = ClientBuilder.newClient(cc);
+        client.register(HttpAuthenticationFeature.universalBuilder().build());
+        WebTarget r = client.target(getBaseUri()).path("test/content");
+
+        try {
+            assertEquals("GET", r.request().get(String.class));
+            fail();
+        } catch (ResponseAuthenticationException ex) {
+            // expected
+        }
+
+        // Verify the connection that was used for the request is available for reuse
+        // and no connections are leased
+        assertEquals(cm.getTotalStats().getAvailable(), 1);
+        assertEquals(cm.getTotalStats().getLeased(), 0);
+    }
+
+    @Test
+    public void testAuthGetWithDigestFilterAndContent() {
+        ClientConfig cc = new ClientConfig();
+        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        cc.property(Apache5ClientProperties.CONNECTION_MANAGER, cm);
+        Client client = ClientBuilder.newClient(cc);
+        client.register(HttpAuthenticationFeature.universalBuilder().build());
+        WebTarget r = client.target(getBaseUri()).path("test/contentDigestAuth");
+
+        try {
+            assertEquals("GET", r.request().get(String.class));
+            fail();
+        } catch (ResponseAuthenticationException ex) {
+            // expected
+        }
+
+        // Verify the connection that was used for the request is available for reuse
+        // and no connections are leased
+        assertEquals(cm.getTotalStats().getAvailable(), 1);
+        assertEquals(cm.getTotalStats().getLeased(), 0);
+    }
+
+    @Test
+    public void testAuthGetQueryParamsBasic() {
+        ClientConfig cc = new ClientConfig();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        client.register(HttpAuthenticationFeature.universal("name", "password"));
+
+        WebTarget r = client.target(getBaseUri()).path("test/queryParamsBasic");
+        assertEquals("GET 2", r.request().get(String.class));
+
+        r = client.target(getBaseUri())
+                .path("test/queryParamsBasic")
+                .queryParam("param1", "value1")
+                .queryParam("param2", "value2");
+        assertEquals("GET 3", r.request().get(String.class));
+
+    }
+
+    @Test
+    public void testAuthGetQueryParamsDigest() {
+        ClientConfig cc = new ClientConfig();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        client.register(HttpAuthenticationFeature.universal("name", "password"));
+
+        WebTarget r = client.target(getBaseUri()).path("test/queryParamsDigest");
+        assertEquals("GET 2", r.request().get(String.class));
+
+        r = client.target(getBaseUri())
+                .path("test/queryParamsDigest")
+                .queryParam("param1", "value1")
+                .queryParam("param2", "value2");
+        assertEquals("GET 3", r.request().get(String.class));
+    }
+
+    @Test
+    public void testAuthGetWithConfigurator() {
+        CredentialsStore credentialsProvider = new BasicCredentialsProvider();
+        credentialsProvider.setCredentials(
+                new AuthScope("localhost", getPort()),
+                new UsernamePasswordCredentials("name", "password".toCharArray())
+        );
+        Apache5HttpClientBuilderConfigurator apache5HttpClientBuilderConfigurator = (httpClientBuilder) -> {
+            return httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
+        };
+
+        ClientConfig cc = new ClientConfig();
+        cc.register(apache5HttpClientBuilderConfigurator);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        WebTarget r = client.target(getBaseUri()).path("test");
+
+        assertEquals("GET", r.request().get(String.class));
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/CookieTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/CookieTest.java
new file mode 100644
index 0000000..0e619a6
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/CookieTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Cookie;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.NewCookie;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.JerseyClient;
+import org.glassfish.jersey.client.JerseyClientBuilder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class CookieTest extends JerseyTest {
+
+    @Path("/")
+    public static class CookieResource {
+
+        @GET
+        public Response get(@Context HttpHeaders h) {
+            Cookie c = h.getCookies().get("name");
+            String e = (c == null) ? "NO-COOKIE" : c.getValue();
+            return Response.ok(e)
+                    .cookie(new NewCookie("name", "value")).build();
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(CookieResource.class);
+    }
+
+    @Test
+    public void testCookieResource() {
+        ClientConfig cc = new ClientConfig();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        WebTarget r = client.target(getBaseUri());
+
+        assertEquals("NO-COOKIE", r.request().get(String.class));
+        assertEquals("value", r.request().get(String.class));
+    }
+
+    @Test
+    public void testDisabledCookies() {
+        ClientConfig cc = new ClientConfig();
+        cc.property(Apache5ClientProperties.DISABLE_COOKIES, true);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        JerseyClient client = JerseyClientBuilder.createClient(cc);
+        WebTarget r = client.target(getBaseUri());
+
+        assertEquals("NO-COOKIE", r.request().get(String.class));
+        assertEquals("NO-COOKIE", r.request().get(String.class));
+
+        final Apache5Connector connector = (Apache5Connector) client.getConfiguration().getConnector();
+        if (connector.getCookieStore() != null) {
+            assertTrue(connector.getCookieStore().getCookies().isEmpty());
+        } else {
+            assertNull(connector.getCookieStore());
+        }
+    }
+
+    @Test
+    public void testCookies() {
+        ClientConfig cc = new ClientConfig();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        JerseyClient client = JerseyClientBuilder.createClient(cc);
+        WebTarget r = client.target(getBaseUri());
+
+        assertEquals("NO-COOKIE", r.request().get(String.class));
+        assertEquals("value", r.request().get(String.class));
+
+        final Apache5Connector connector = (Apache5Connector) client.getConfiguration().getConnector();
+        assertNotNull(connector.getCookieStore().getCookies());
+        assertEquals(1, connector.getCookieStore().getCookies().size());
+        assertEquals("value", connector.getCookieStore().getCookies().get(0).getValue());
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/CustomLoggingFilter.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/CustomLoggingFilter.java
new file mode 100644
index 0000000..27caeda
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/CustomLoggingFilter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.io.IOException;
+
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.client.ClientRequestFilter;
+import javax.ws.rs.client.ClientResponseContext;
+import javax.ws.rs.client.ClientResponseFilter;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.container.ContainerResponseContext;
+import javax.ws.rs.container.ContainerResponseFilter;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Custom logging filter.
+ *
+ * @author Santiago Pericas-Geertsen (santiago.pericasgeertsen at oracle.com)
+ */
+public class CustomLoggingFilter implements ContainerRequestFilter, ContainerResponseFilter,
+        ClientRequestFilter, ClientResponseFilter {
+
+    static int preFilterCalled = 0;
+    static int postFilterCalled = 0;
+
+    @Override
+    public void filter(ClientRequestContext context) throws IOException {
+        System.out.println("CustomLoggingFilter.preFilter called");
+        assertEquals(context.getConfiguration().getProperty("foo"), "bar");
+        preFilterCalled++;
+    }
+
+    @Override
+    public void filter(ClientRequestContext context, ClientResponseContext clientResponseContext) throws IOException {
+        System.out.println("CustomLoggingFilter.postFilter called");
+        assertEquals(context.getConfiguration().getProperty("foo"), "bar");
+        postFilterCalled++;
+    }
+
+    @Override
+    public void filter(ContainerRequestContext context) throws IOException {
+        System.out.println("CustomLoggingFilter.preFilter called");
+        assertEquals(context.getProperty("foo"), "bar");
+        preFilterCalled++;
+    }
+
+    @Override
+    public void filter(ContainerRequestContext context, ContainerResponseContext containerResponseContext) throws IOException {
+        System.out.println("CustomLoggingFilter.postFilter called");
+        assertEquals(context.getProperty("foo"), "bar");
+        postFilterCalled++;
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/DisableContentEncodingTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/DisableContentEncodingTest.java
new file mode 100644
index 0000000..51bc6a0
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/DisableContentEncodingTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.message.GZipEncoder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Ondrej Kosatka
+ */
+public class DisableContentEncodingTest extends JerseyTest {
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(Resource.class);
+    }
+
+    @Path("/")
+    public static class Resource {
+
+        @GET
+        public String get(@HeaderParam("Accept-Encoding") String enc) {
+            return enc;
+        }
+    }
+
+    @Test
+    public void testDisabledByRequestConfig() {
+        ClientConfig cc = new ClientConfig(GZipEncoder.class);
+        final RequestConfig requestConfig = RequestConfig.custom().setContentCompressionEnabled(false).build();
+        cc.property(Apache5ClientProperties.REQUEST_CONFIG, requestConfig);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        WebTarget r = client.target(getBaseUri());
+
+        String enc = r.request().get().readEntity(String.class);
+        assertEquals("", enc);
+    }
+
+    @Test
+    public void testEnabledByRequestConfig() {
+        ClientConfig cc = new ClientConfig(GZipEncoder.class);
+        final RequestConfig requestConfig = RequestConfig.custom().setContentCompressionEnabled(true).build();
+        cc.property(Apache5ClientProperties.REQUEST_CONFIG, requestConfig);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        WebTarget r = client.target(getBaseUri());
+
+        String enc = r.request().get().readEntity(String.class);
+        assertEquals("gzip, x-gzip, deflate", enc);
+    }
+
+    @Test
+    public void testDefaultEncoding() {
+        ClientConfig cc = new ClientConfig(GZipEncoder.class);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        WebTarget r = client.target(getBaseUri());
+
+        String enc = r.request().get().readEntity(String.class);
+        assertEquals("gzip, x-gzip, deflate", enc);
+    }
+
+    @Test
+    public void testDefaultEncodingOverridden() {
+        ClientConfig cc = new ClientConfig(GZipEncoder.class);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        WebTarget r = client.target(getBaseUri());
+
+        String enc = r.request().acceptEncoding("gzip").get().readEntity(String.class);
+        assertEquals("gzip", enc);
+    }
+
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/FollowRedirectsTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/FollowRedirectsTest.java
new file mode 100644
index 0000000..9377a0a
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/FollowRedirectsTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.client.ClientResponseContext;
+import javax.ws.rs.client.ClientResponseFilter;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientResponse;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Apache connector follow redirect tests.
+ *
+ * @author Martin Matula
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Marek Potociar
+ */
+public class FollowRedirectsTest extends JerseyTest {
+    private static final Logger LOGGER = Logger.getLogger(TimeoutTest.class.getName());
+
+    @Path("/test")
+    public static class RedirectResource {
+        @GET
+        public String get() {
+            return "GET";
+        }
+
+        @GET
+        @Path("redirect")
+        public Response redirect() {
+            return Response.seeOther(UriBuilder.fromResource(RedirectResource.class).build()).build();
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        ResourceConfig config = new ResourceConfig(RedirectResource.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        return config;
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.connectorProvider(new Apache5ConnectorProvider());
+    }
+
+    private static class RedirectTestFilter implements ClientResponseFilter {
+        public static final String RESOLVED_URI_HEADER = "resolved-uri";
+
+        @Override
+        public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
+            if (responseContext instanceof ClientResponse) {
+                ClientResponse clientResponse = (ClientResponse) responseContext;
+                responseContext.getHeaders().putSingle(RESOLVED_URI_HEADER, clientResponse.getResolvedRequestUri().toString());
+            }
+        }
+    }
+
+    @Test
+    public void testDoFollow() {
+        Response r = target("test/redirect").register(RedirectTestFilter.class).request().get();
+        assertEquals(200, r.getStatus());
+        assertEquals("GET", r.readEntity(String.class));
+        assertEquals(
+                UriBuilder.fromUri(getBaseUri()).path(RedirectResource.class).build().toString(),
+                r.getHeaderString(RedirectTestFilter.RESOLVED_URI_HEADER));
+    }
+
+    @Test
+    public void testDontFollow() {
+        WebTarget t = target("test/redirect");
+        t.property(ClientProperties.FOLLOW_REDIRECTS, false);
+        assertEquals(303, t.request().get().getStatus());
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/GZIPContentEncodingTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/GZIPContentEncodingTest.java
new file mode 100644
index 0000000..0230ba1
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/GZIPContentEncodingTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.util.Arrays;
+
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.message.GZipEncoder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class GZIPContentEncodingTest extends JerseyTest {
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(Resource.class);
+    }
+
+    @Path("/")
+    public static class Resource {
+
+        @POST
+        public byte[] post(byte[] content) {
+            return content;
+        }
+    }
+
+    @Test
+    public void testPost() {
+        ClientConfig cc = new ClientConfig(GZipEncoder.class);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        WebTarget r = client.target(getBaseUri());
+
+        byte[] content = new byte[1024 * 1024];
+        assertTrue(Arrays.equals(content,
+                r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class)));
+
+        Response cr = r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE));
+        assertTrue(cr.hasEntity());
+        cr.close();
+    }
+
+    @Test
+    public void testPostChunked() {
+        ClientConfig cc = new ClientConfig(GZipEncoder.class);
+        cc.property(ClientProperties.CHUNKED_ENCODING_SIZE, 1024);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+
+        WebTarget r = client.target(getBaseUri());
+
+        byte[] content = new byte[1024 * 1024];
+        assertTrue(Arrays.equals(content,
+                r.request().post(Entity.entity(content, MediaType.APPLICATION_OCTET_STREAM_TYPE)).readEntity(byte[].class)));
+
+        Response cr = r.request().post(Entity.text("POST"));
+        assertTrue(cr.hasEntity());
+        cr.close();
+    }
+
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HelloWorldTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HelloWorldTest.java
new file mode 100644
index 0000000..e0f6612
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HelloWorldTest.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.InternalServerErrorException;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.InvocationCallback;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.io.ConnectionEndpoint;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.io.LeaseRequest;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Assert;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author Jakub Podlesak
+ */
+public class HelloWorldTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(HelloWorldTest.class.getName());
+    private static final String ROOT_PATH = "helloworld";
+
+    @Path("helloworld")
+    public static class HelloWorldResource {
+
+        public static final String CLICHED_MESSAGE = "Hello World!";
+
+        @GET
+        @Produces("text/plain")
+        public String getHello() {
+            return CLICHED_MESSAGE;
+        }
+
+        @GET
+        @Produces("text/plain")
+        @Path("error")
+        public Response getError() {
+            return Response.serverError().entity("Error.").build();
+        }
+
+        @GET
+        @Produces("text/plain")
+        @Path("error2")
+        public Response getError2() {
+            return Response.serverError().entity("Error2.").build();
+        }
+
+    }
+
+    @Override
+    protected Application configure() {
+        ResourceConfig config = new ResourceConfig(HelloWorldResource.class);
+        config.register(new LoggingFeature(LOGGER, Level.INFO, LoggingFeature.Verbosity.PAYLOAD_ANY,
+                LoggingFeature.DEFAULT_MAX_ENTITY_SIZE));
+        return config;
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.connectorProvider(new Apache5ConnectorProvider());
+    }
+
+    @Test
+    public void testConnection() {
+        Response response = target().path(ROOT_PATH).request("text/plain").get();
+        assertEquals(200, response.getStatus());
+    }
+
+    @Test
+    public void testClientStringResponse() {
+        String s = target().path(ROOT_PATH).request().get(String.class);
+        assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+    }
+
+    @Test
+    public void testConnectionPoolSharingEnabled() throws Exception {
+        _testConnectionPoolSharing(true);
+    }
+
+    @Test
+    public void testConnectionPoolSharingDisabled() throws Exception {
+        _testConnectionPoolSharing(false);
+    }
+
+    public void _testConnectionPoolSharing(final boolean sharingEnabled) throws Exception {
+
+        final HttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
+
+        final ClientConfig cc = new ClientConfig();
+        cc.property(Apache5ClientProperties.CONNECTION_MANAGER, connectionManager);
+        cc.property(Apache5ClientProperties.CONNECTION_MANAGER_SHARED, sharingEnabled);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+
+        final Client clientOne = ClientBuilder.newClient(cc);
+        WebTarget target = clientOne.target(getBaseUri()).path(ROOT_PATH);
+        target.request().get();
+        clientOne.close();
+
+        final boolean exceptionExpected = !sharingEnabled;
+
+        final Client clientTwo = ClientBuilder.newClient(cc);
+        target = clientTwo.target(getBaseUri()).path(ROOT_PATH);
+        try {
+            target.request().get();
+            if (exceptionExpected) {
+                Assert.fail("Exception expected");
+            }
+        } catch (Exception e) {
+            if (!exceptionExpected) {
+                Assert.fail("Exception not expected");
+            }
+        } finally {
+            clientTwo.close();
+        }
+
+        if (sharingEnabled) {
+            connectionManager.close();
+        }
+    }
+
+    @Test
+    public void testAsyncClientRequests() throws InterruptedException {
+        HttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
+        ClientConfig cc = new ClientConfig();
+        cc.property(Apache5ClientProperties.CONNECTION_MANAGER, connectionManager);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        WebTarget target = client.target(getBaseUri());
+        final int REQUESTS = 20;
+        final CountDownLatch latch = new CountDownLatch(REQUESTS);
+        final long tic = System.currentTimeMillis();
+        final Map<Integer, String> results = new ConcurrentHashMap<Integer, String>();
+        for (int i = 0; i < REQUESTS; i++) {
+            final int id = i;
+            target.path(ROOT_PATH).request().async().get(new InvocationCallback<Response>() {
+                @Override
+                public void completed(Response response) {
+                    try {
+                        final String result = response.readEntity(String.class);
+                        results.put(id, result);
+                    } finally {
+                        latch.countDown();
+                    }
+                }
+
+                @Override
+                public void failed(Throwable error) {
+                    Logger.getLogger(HelloWorldTest.class.getName()).log(Level.SEVERE, "Failed on throwable", error);
+                    results.put(id, "error: " + error.getMessage());
+                    latch.countDown();
+                }
+            });
+        }
+        assertTrue(latch.await(10 * getAsyncTimeoutMultiplier(), TimeUnit.SECONDS));
+        final long toc = System.currentTimeMillis();
+        Logger.getLogger(HelloWorldTest.class.getName()).info("Executed in: " + (toc - tic));
+
+        StringBuilder resultInfo = new StringBuilder("Results:\n");
+        for (int i = 0; i < REQUESTS; i++) {
+            String result = results.get(i);
+            resultInfo.append(i).append(": ").append(result).append('\n');
+        }
+        Logger.getLogger(HelloWorldTest.class.getName()).info(resultInfo.toString());
+
+        for (int i = 0; i < REQUESTS; i++) {
+            String result = results.get(i);
+            assertEquals(HelloWorldResource.CLICHED_MESSAGE, result);
+        }
+    }
+
+    @Test
+    public void testHead() {
+        Response response = target().path(ROOT_PATH).request().head();
+        assertEquals(200, response.getStatus());
+        assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType());
+    }
+
+    @Test
+    public void testFooBarOptions() {
+        Response response = target().path(ROOT_PATH).request().header("Accept", "foo/bar").options();
+        assertEquals(200, response.getStatus());
+        final String allowHeader = response.getHeaderString("Allow");
+        _checkAllowContent(allowHeader);
+        assertEquals("foo/bar", response.getMediaType().toString());
+        assertEquals(0, response.getLength());
+    }
+
+    @Test
+    public void testTextPlainOptions() {
+        Response response = target().path(ROOT_PATH).request().header("Accept", MediaType.TEXT_PLAIN).options();
+        assertEquals(200, response.getStatus());
+        final String allowHeader = response.getHeaderString("Allow");
+        _checkAllowContent(allowHeader);
+        assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType());
+        final String responseBody = response.readEntity(String.class);
+        _checkAllowContent(responseBody);
+    }
+
+    private void _checkAllowContent(final String content) {
+        assertTrue(content.contains("GET"));
+        assertTrue(content.contains("HEAD"));
+        assertTrue(content.contains("OPTIONS"));
+    }
+
+    @Test
+    public void testMissingResourceNotFound() {
+        Response response;
+
+        response = target().path(ROOT_PATH + "arbitrary").request().get();
+        assertEquals(404, response.getStatus());
+        response.close();
+
+        response = target().path(ROOT_PATH).path("arbitrary").request().get();
+        assertEquals(404, response.getStatus());
+        response.close();
+    }
+
+    @Test
+    public void testLoggingFilterClientClass() {
+        Client client = client();
+        client.register(CustomLoggingFilter.class).property("foo", "bar");
+        CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+        String s = target().path(ROOT_PATH).request().get(String.class);
+        assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+        assertEquals(1, CustomLoggingFilter.preFilterCalled);
+        assertEquals(1, CustomLoggingFilter.postFilterCalled);
+    }
+
+    @Test
+    public void testLoggingFilterClientInstance() {
+        Client client = client();
+        client.register(new CustomLoggingFilter()).property("foo", "bar");
+        CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+        String s = target().path(ROOT_PATH).request().get(String.class);
+        assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+        assertEquals(1, CustomLoggingFilter.preFilterCalled);
+        assertEquals(1, CustomLoggingFilter.postFilterCalled);
+    }
+
+    @Test
+    public void testLoggingFilterTargetClass() {
+        WebTarget target = target().path(ROOT_PATH);
+        target.register(CustomLoggingFilter.class).property("foo", "bar");
+        CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+        String s = target.request().get(String.class);
+        assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+        assertEquals(1, CustomLoggingFilter.preFilterCalled);
+        assertEquals(1, CustomLoggingFilter.postFilterCalled);
+    }
+
+    @Test
+    public void testLoggingFilterTargetInstance() {
+        WebTarget target = target().path(ROOT_PATH);
+        target.register(new CustomLoggingFilter()).property("foo", "bar");
+        CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+        String s = target.request().get(String.class);
+        assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+        assertEquals(1, CustomLoggingFilter.preFilterCalled);
+        assertEquals(1, CustomLoggingFilter.postFilterCalled);
+    }
+
+    @Test
+    public void testConfigurationUpdate() {
+        Client client1 = client();
+        client1.register(CustomLoggingFilter.class).property("foo", "bar");
+
+        Client client = ClientBuilder.newClient(client1.getConfiguration());
+        CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+        String s = client.target(getBaseUri()).path(ROOT_PATH).request().get(String.class);
+        assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+        assertEquals(1, CustomLoggingFilter.preFilterCalled);
+        assertEquals(1, CustomLoggingFilter.postFilterCalled);
+    }
+
+    /**
+     * JERSEY-2157 reproducer.
+     * <p>
+     * The test ensures that entities of the error responses which cause
+     * WebApplicationException being thrown by a JAX-RS client are buffered
+     * and that the underlying input connections are automatically released
+     * in such case.
+     */
+    @Test
+    public void testConnectionClosingOnExceptionsForErrorResponses() {
+        final BasicHttpClientConnectionManager cm = new BasicHttpClientConnectionManager();
+        final AtomicInteger connectionCounter = new AtomicInteger(0);
+
+        final ClientConfig config = new ClientConfig().property(Apache5ClientProperties.CONNECTION_MANAGER,
+                new HttpClientConnectionManager() {
+                    @Override
+                    public LeaseRequest lease(String id, HttpRoute route, Timeout requestTimeout, Object state) {
+                        connectionCounter.incrementAndGet();
+                        return cm.lease(id, route, requestTimeout, state);
+                    }
+
+                    @Override
+                    public void release(ConnectionEndpoint endpoint, Object newState, TimeValue validDuration) {
+                        connectionCounter.decrementAndGet();
+                        cm.release(endpoint, newState, validDuration);
+                    }
+
+                    @Override
+                    public void connect(
+                            ConnectionEndpoint endpoint,
+                            TimeValue connectTimeout,
+                            HttpContext context
+                    ) throws IOException {
+                        cm.connect(endpoint, connectTimeout, context);
+                    }
+
+                    @Override
+                    public void upgrade(ConnectionEndpoint endpoint, HttpContext context) throws IOException {
+                        cm.upgrade(endpoint, context);
+                    }
+
+                    @Override
+                    public void close(CloseMode closeMode) {
+                        cm.close(closeMode);
+                    }
+
+                    @Override
+                    public void close() throws IOException {
+                        cm.close();
+                    }
+                });
+        config.connectorProvider(new Apache5ConnectorProvider());
+
+        final Client client = ClientBuilder.newClient(config);
+        final WebTarget rootTarget = client.target(getBaseUri()).path(ROOT_PATH);
+
+        // Test that connection is getting closed properly for error responses.
+        try {
+            final String response = rootTarget.path("error").request().get(String.class);
+            fail("Exception expected. Received: " + response);
+        } catch (InternalServerErrorException isee) {
+            // do nothing - connection should be closed properly by now
+        }
+
+        // Fail if the previous connection has not been closed automatically.
+        assertEquals(0, connectionCounter.get());
+
+        try {
+            final String response = rootTarget.path("error2").request().get(String.class);
+            fail("Exception expected. Received: " + response);
+        } catch (InternalServerErrorException isee) {
+            assertEquals("Received unexpected data.", "Error2.", isee.getResponse().readEntity(String.class));
+            // Test buffering:
+            // second read would fail if entity was not buffered
+            assertEquals("Unexpected data in the entity buffer.", "Error2.", isee.getResponse().readEntity(String.class));
+        }
+
+        assertEquals(0, connectionCounter.get());
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpEntityTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpEntityTest.java
new file mode 100644
index 0000000..cad5ea8
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpEntityTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
+import org.apache.hc.core5.http.io.entity.InputStreamEntity;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.Assert;
+import org.junit.Test;
+
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.ByteArrayInputStream;
+import java.util.logging.Logger;
+
+public class HttpEntityTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(HttpEntityTest.class.getName());
+    private static final String ECHO_MESSAGE = "ECHO MESSAGE";
+
+    @Path("/")
+    public static class Resource {
+        @POST
+        public String echo(String message) {
+            return message;
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(Resource.class)
+                .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        config.connectorProvider(new Apache5ConnectorProvider());
+    }
+
+    @Test
+    public void testInputStreamEntity() {
+        ByteArrayInputStream bais = new ByteArrayInputStream(ECHO_MESSAGE.getBytes());
+        InputStreamEntity entity = new InputStreamEntity(bais, ContentType.TEXT_PLAIN);
+
+        try (Response response = target().request().post(Entity.entity(entity, MediaType.APPLICATION_OCTET_STREAM))) {
+            Assert.assertEquals(200, response.getStatus());
+            Assert.assertEquals(ECHO_MESSAGE, response.readEntity(String.class));
+        }
+    }
+
+    @Test
+    public void testByteArrayEntity() {
+        ByteArrayEntity entity = new ByteArrayEntity(ECHO_MESSAGE.getBytes(), ContentType.TEXT_PLAIN);
+
+        try (Response response = target().request().post(Entity.entity(entity, MediaType.APPLICATION_OCTET_STREAM))) {
+            Assert.assertEquals(200, response.getStatus());
+            Assert.assertEquals(ECHO_MESSAGE, response.readEntity(String.class));
+        }
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpHeadersTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpHeadersTest.java
new file mode 100644
index 0000000..c63b7a8
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpHeadersTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.logging.Logger;
+
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.TestProperties;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class HttpHeadersTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(HttpHeadersTest.class.getName());
+
+    @Path("/test")
+    public static class HttpMethodResource {
+
+        @POST
+        public String post(
+                @HeaderParam("Transfer-Encoding") String transferEncoding,
+                @HeaderParam("X-CLIENT") String xClient,
+                @HeaderParam("X-WRITER") String xWriter,
+                String entity) {
+            assertEquals("client", xClient);
+            if (transferEncoding == null || !transferEncoding.equals("chunked")) {
+                assertEquals("writer", xWriter);
+            }
+            return entity;
+        }
+    }
+
+    @Provider
+    @Produces("text/plain")
+    public static class HeaderWriter implements MessageBodyWriter<String> {
+
+        public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+            return type == String.class;
+        }
+
+        public long getSize(String t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+            return -1;
+        }
+
+        public void writeTo(String t,
+                            Class<?> type,
+                            Type genericType,
+                            Annotation[] annotations,
+                            MediaType mediaType,
+                            MultivaluedMap<String, Object> httpHeaders,
+                            OutputStream entityStream) throws IOException, WebApplicationException {
+            httpHeaders.add("X-WRITER", "writer");
+            entityStream.write(t.getBytes());
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        enable(TestProperties.LOG_TRAFFIC);
+        enable(TestProperties.DUMP_ENTITY);
+
+        ResourceConfig config = new ResourceConfig(HttpMethodResource.class, HeaderWriter.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        return config;
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.property(ClientProperties.READ_TIMEOUT, 1000).connectorProvider(new Apache5ConnectorProvider());
+    }
+
+    @Test
+    public void testPost() {
+        WebTarget r = target("test");
+
+        Response cr = r.request().header("X-CLIENT", "client").post(Entity.text("POST"));
+        assertEquals(200, cr.getStatus());
+        assertTrue(cr.hasEntity());
+        cr.close();
+    }
+
+    @Test
+    public void testPostChunked() {
+        WebTarget r = target("test");
+
+        Response cr = r.request().header("X-CLIENT", "client").post(Entity.text("POST"));
+        assertEquals(200, cr.getStatus());
+        assertTrue(cr.hasEntity());
+        cr.close();
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodTest.java
new file mode 100644
index 0000000..00a45a1
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodTest.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import javax.ws.rs.ClientErrorException;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class HttpMethodTest extends JerseyTest {
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(HttpMethodResource.class, ErrorResource.class);
+    }
+
+    protected Client createClient() {
+        ClientConfig cc = new ClientConfig();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        return ClientBuilder.newClient(cc);
+    }
+
+    protected Client createPoolingClient() {
+        ClientConfig cc = new ClientConfig();
+        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
+        connectionManager.setMaxTotal(100);
+        connectionManager.setDefaultMaxPerRoute(100);
+        cc.property(Apache5ClientProperties.CONNECTION_MANAGER, connectionManager);
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        return ClientBuilder.newClient(cc);
+    }
+
+    private WebTarget getWebTarget(final Client client) {
+        return client.target(getBaseUri()).path("test");
+    }
+
+    private WebTarget getWebTarget() {
+        return getWebTarget(createClient());
+    }
+
+    @Target({ElementType.METHOD})
+    @Retention(RetentionPolicy.RUNTIME)
+    @HttpMethod("PATCH")
+    public @interface PATCH {
+    }
+
+    @Path("/test")
+    public static class HttpMethodResource {
+        @GET
+        public String get() {
+            return "GET";
+        }
+
+        @POST
+        public String post(String entity) {
+            return entity;
+        }
+
+        @PUT
+        public String put(String entity) {
+            return entity;
+        }
+
+        @DELETE
+        public String delete() {
+            return "DELETE";
+        }
+
+        @DELETE
+        @Path("withentity")
+        public String delete(String entity) {
+            return entity;
+        }
+
+        @POST
+        @Path("noproduce")
+        public void postNoProduce(String entity) {
+        }
+
+        @POST
+        @Path("noconsumeproduce")
+        public void postNoConsumeProduce() {
+        }
+
+        @PATCH
+        public String patch(String entity) {
+            return entity;
+        }
+    }
+
+    @Test
+    public void testHead() {
+        WebTarget r = getWebTarget();
+        Response cr = r.request().head();
+        assertFalse(cr.hasEntity());
+    }
+
+    @Test
+    public void testOptions() {
+        WebTarget r = getWebTarget();
+        Response cr = r.request().options();
+        assertTrue(cr.hasEntity());
+        cr.close();
+    }
+
+    @Test
+    public void testOptionsWithEntity() {
+        WebTarget r = getWebTarget();
+        Response response = r.request().build("OPTIONS", Entity.text("OPTIONS")).invoke();
+        assertEquals(200, response.getStatus());
+        response.close();
+    }
+
+    @Test
+    public void testGet() {
+        WebTarget r = getWebTarget();
+        assertEquals("GET", r.request().get(String.class));
+
+        Response cr = r.request().get();
+        assertTrue(cr.hasEntity());
+        cr.close();
+    }
+
+    @Test
+    public void testPost() {
+        WebTarget r = getWebTarget();
+        assertEquals("POST", r.request().post(Entity.text("POST"), String.class));
+
+        Response cr = r.request().post(Entity.text("POST"));
+        assertTrue(cr.hasEntity());
+        cr.close();
+    }
+
+    @Test
+    public void testPostChunked() {
+        ClientConfig cc = new ClientConfig()
+                .property(ClientProperties.CHUNKED_ENCODING_SIZE, 1024)
+                .connectorProvider(new Apache5ConnectorProvider());
+        Client client = ClientBuilder.newClient(cc);
+        WebTarget r = getWebTarget(client);
+
+        assertEquals("POST", r.request().post(Entity.text("POST"), String.class));
+
+        Response cr = r.request().post(Entity.text("POST"));
+        assertTrue(cr.hasEntity());
+        cr.close();
+    }
+
+    @Test
+    public void testPostVoid() {
+        WebTarget r = getWebTarget(createPoolingClient());
+
+        for (int i = 0; i < 100; i++) {
+            r.request().post(Entity.text("POST"));
+        }
+    }
+
+    @Test
+    public void testPostNoProduce() {
+        WebTarget r = getWebTarget();
+        assertEquals(204, r.path("noproduce").request().post(Entity.text("POST")).getStatus());
+
+        Response cr = r.path("noproduce").request().post(Entity.text("POST"));
+        assertFalse(cr.hasEntity());
+        cr.close();
+    }
+
+
+    @Test
+    public void testPostNoConsumeProduce() {
+        WebTarget r = getWebTarget();
+        assertEquals(204, r.path("noconsumeproduce").request().post(null).getStatus());
+
+        Response cr = r.path("noconsumeproduce").request().post(Entity.text("POST"));
+        assertFalse(cr.hasEntity());
+        cr.close();
+    }
+
+    @Test
+    public void testPut() {
+        WebTarget r = getWebTarget();
+        assertEquals("PUT", r.request().put(Entity.text("PUT"), String.class));
+
+        Response cr = r.request().put(Entity.text("PUT"));
+        assertTrue(cr.hasEntity());
+        cr.close();
+    }
+
+    @Test
+    public void testDelete() {
+        WebTarget r = getWebTarget();
+        assertEquals("DELETE", r.request().delete(String.class));
+
+        Response cr = r.request().delete();
+        assertTrue(cr.hasEntity());
+        cr.close();
+    }
+
+    @Test
+    public void testPatch() {
+        WebTarget r = getWebTarget();
+        assertEquals("PATCH", r.request().method("PATCH", Entity.text("PATCH"), String.class));
+
+        Response cr = r.request().method("PATCH", Entity.text("PATCH"));
+        assertTrue(cr.hasEntity());
+        cr.close();
+    }
+
+    @Test
+    public void testAll() {
+        WebTarget r = getWebTarget();
+
+        assertEquals("GET", r.request().get(String.class));
+
+        assertEquals("POST", r.request().post(Entity.text("POST"), String.class));
+
+        assertEquals(204, r.path("noproduce").request().post(Entity.text("POST")).getStatus());
+
+        assertEquals(204, r.path("noconsumeproduce").request().post(null).getStatus());
+
+        assertEquals("PUT", r.request().post(Entity.text("PUT"), String.class));
+
+        assertEquals("DELETE", r.request().delete(String.class));
+    }
+
+
+    @Path("/error")
+    public static class ErrorResource {
+        @POST
+        public Response post(String entity) {
+            return Response.serverError().build();
+        }
+
+        @Path("entity")
+        @POST
+        public Response postWithEntity(String entity) {
+            return Response.serverError().entity("error").build();
+        }
+    }
+
+    @Test
+    public void testPostError() {
+        WebTarget r = createClient().target(getBaseUri()).path("error");
+
+        for (int i = 0; i < 100; i++) {
+            try {
+                final Response post = r.request().post(Entity.text("POST"));
+                post.close();
+            } catch (ClientErrorException ex) {
+            }
+        }
+    }
+
+    @Test
+    public void testPostErrorWithEntity() {
+        WebTarget r = createPoolingClient().target(getBaseUri()).path("error/entity");
+
+        for (int i = 0; i < 100; i++) {
+            try {
+                r.request().post(Entity.text("POST"));
+            } catch (ClientErrorException ex) {
+                String s = ex.getResponse().readEntity(String.class);
+                assertEquals("error", s);
+            }
+        }
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodWithClientFilterTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodWithClientFilterTest.java
new file mode 100644
index 0000000..35b38c3
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/HttpMethodWithClientFilterTest.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class HttpMethodWithClientFilterTest extends HttpMethodTest {
+
+    @Override
+    protected Client createClient() {
+        ClientConfig cc = new ClientConfig()
+                .register(LoggingFeature.class)
+                .connectorProvider(new Apache5ConnectorProvider());
+        return ClientBuilder.newClient(cc);
+    }
+
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/LargeDataTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/LargeDataTest.java
new file mode 100644
index 0000000..40f1b41
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/LargeDataTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.logging.Logger;
+
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.ServerErrorException;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.StreamingOutput;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * The LargeDataTest reproduces a problem when bytes of large data sent are incorrectly sent.
+ * As a result, the request body is different than what was sent by the client.
+ * <p>
+ * In order to be able to inspect the request body, the generated data is a sequence of numbers
+ * delimited with new lines. Such as
+ * <pre><code>
+ *     1
+ *     2
+ *     3
+ *
+ *     ...
+ *
+ *     57234
+ *     57235
+ *     57236
+ *
+ *     ...
+ * </code></pre>
+ * It is also possible to send the data to netcat: {@code nc -l 8080} and verify the problem is
+ * on the client side.
+ *
+ * @author Stepan Vavra
+ * @author Marek Potociar
+ */
+public class LargeDataTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(LargeDataTest.class.getName());
+    private static final int LONG_DATA_SIZE = 1_000_000;  // for large set around 5GB, try e.g.: 536_870_912;
+    private static volatile Throwable exception;
+
+    private static StreamingOutput longData(long sequence) {
+        return out -> {
+            long offset = 0;
+            while (offset < sequence) {
+                out.write(Long.toString(offset).getBytes());
+                out.write('\n');
+                offset++;
+            }
+        };
+    }
+
+    @Override
+    protected Application configure() {
+        ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.HEADERS_ONLY));
+        return config;
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.connectorProvider(new Apache5ConnectorProvider());
+    }
+
+    @Test
+    public void postWithLargeData() throws Throwable {
+        WebTarget webTarget = target("test");
+
+        Response response = webTarget.request().post(Entity.entity(longData(LONG_DATA_SIZE), MediaType.TEXT_PLAIN_TYPE));
+
+        try {
+            if (exception != null) {
+
+                // the reason to throw the exception is that IntelliJ gives you an option to compare the expected with the actual
+                throw exception;
+            }
+
+            Assert.assertEquals("Unexpected error: " + response.getStatus(),
+                    Status.Family.SUCCESSFUL,
+                    response.getStatusInfo().getFamily());
+        } finally {
+            response.close();
+        }
+    }
+
+    @Path("/test")
+    public static class HttpMethodResource {
+
+        @POST
+        public Response post(InputStream content) {
+            try {
+
+                longData(LONG_DATA_SIZE).write(new OutputStream() {
+
+                    private long position = 0;
+//                    private long mbRead = 0;
+
+                    @Override
+                    public void write(final int generated) throws IOException {
+                        int received = content.read();
+
+                        if (received != generated) {
+                            throw new IOException("Bytes don't match at position " + position
+                                    + ": received=" + received
+                                    + ", generated=" + generated);
+                        }
+
+                        position++;
+//                        if (position % (1024 * 1024) == 0) {
+//                            mbRead++;
+//                            System.out.println("MB read: " + mbRead);
+//                        }
+                    }
+                });
+            } catch (IOException e) {
+                exception = e;
+                throw new ServerErrorException(e.getMessage(), 500, e);
+            }
+
+            return Response.ok().build();
+        }
+
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/ManagedClientTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/ManagedClientTest.java
new file mode 100644
index 0000000..46ba4ee
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/ManagedClientTest.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.logging.Logger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.client.ClientRequestFilter;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.container.DynamicFeature;
+import javax.ws.rs.container.ResourceInfo;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.FeatureContext;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ClientBinding;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.Uri;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Jersey programmatic managed client test
+ *
+ * @author Marek Potociar
+ */
+public class ManagedClientTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(ManagedClientTest.class.getName());
+
+    /**
+     * Managed client configuration for client A.
+     *
+     * @author Marek Potociar (marek.potociar at oracle.com)
+     */
+    @ClientBinding(configClass = MyClientAConfig.class)
+    @Documented
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.FIELD, ElementType.PARAMETER})
+    public static @interface ClientA {
+    }
+
+    /**
+     * Managed client configuration for client B.
+     *
+     * @author Marek Potociar (marek.potociar at oracle.com)
+     */
+    @ClientBinding(configClass = MyClientBConfig.class)
+    @Documented
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.FIELD, ElementType.PARAMETER})
+    public @interface ClientB {
+    }
+
+    /**
+     * Dynamic feature that appends a properly configured {@link CustomHeaderFilter} instance
+     * to every method that is annotated with {@link Require &#64;Require} internal feature
+     * annotation.
+     *
+     * @author Marek Potociar
+     */
+    public static class CustomHeaderFeature implements DynamicFeature {
+
+        /**
+         * A method annotation to be placed on those resource methods to which a validating
+         * {@link CustomHeaderFilter} instance should be added.
+         */
+        @Retention(RetentionPolicy.RUNTIME)
+        @Documented
+        @Target(ElementType.METHOD)
+        public static @interface Require {
+
+            /**
+             * Expected custom header name to be validated by the {@link CustomHeaderFilter}.
+             */
+            public String headerName();
+
+            /**
+             * Expected custom header value to be validated by the {@link CustomHeaderFilter}.
+             */
+            public String headerValue();
+        }
+
+        @Override
+        public void configure(ResourceInfo resourceInfo, FeatureContext context) {
+            final Require va = resourceInfo.getResourceMethod().getAnnotation(Require.class);
+            if (va != null) {
+                context.register(new CustomHeaderFilter(va.headerName(), va.headerValue()));
+            }
+        }
+    }
+
+    /**
+     * A filter for appending and validating custom headers.
+     * <p>
+     * On the client side, appends a new custom request header with a configured name and value to each outgoing request.
+     * </p>
+     * <p>
+     * On the server side, validates that each request has a custom header with a configured name and value.
+     * If the validation fails a HTTP 403 response is returned.
+     * </p>
+     *
+     * @author Marek Potociar (marek.potociar at oracle.com)
+     */
+    public static class CustomHeaderFilter implements ContainerRequestFilter, ClientRequestFilter {
+
+        private final String headerName;
+        private final String headerValue;
+
+        public CustomHeaderFilter(String headerName, String headerValue) {
+            if (headerName == null || headerValue == null) {
+                throw new IllegalArgumentException("Header name and value must not be null.");
+            }
+            this.headerName = headerName;
+            this.headerValue = headerValue;
+        }
+
+        @Override
+        public void filter(ContainerRequestContext ctx) throws IOException { // validate
+            if (!headerValue.equals(ctx.getHeaderString(headerName))) {
+                ctx.abortWith(Response.status(Response.Status.FORBIDDEN)
+                        .type(MediaType.TEXT_PLAIN)
+                        .entity(String
+                                .format("Expected header '%s' not present or value not equal to '%s'", headerName, headerValue))
+                        .build());
+            }
+        }
+
+        @Override
+        public void filter(ClientRequestContext ctx) throws IOException { // append
+            ctx.getHeaders().putSingle(headerName, headerValue);
+        }
+    }
+
+    /**
+     * Internal resource accessed from the managed client resource.
+     *
+     * @author Marek Potociar (marek.potociar at oracle.com)
+     */
+    @Path("internal")
+    public static class InternalResource {
+
+        @GET
+        @Path("a")
+        @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "a")
+        public String getA() {
+            return "a";
+        }
+
+        @GET
+        @Path("b")
+        @CustomHeaderFeature.Require(headerName = "custom-header", headerValue = "b")
+        public String getB() {
+            return "b";
+        }
+    }
+
+    /**
+     * A resource that uses managed clients to retrieve values of internal
+     * resources 'A' and 'B', which are protected by a {@link CustomHeaderFilter}
+     * and require a specific custom header in a request to be set to a specific value.
+     * <p>
+     * Properly configured managed clients have a {@code CustomHeaderFilter} instance
+     * configured to insert the {@link CustomHeaderFeature.Require required} custom header
+     * with a proper value into the outgoing client requests.
+     * </p>
+     *
+     * @author Marek Potociar (marek.potociar at oracle.com)
+     */
+    @Path("public")
+    public static class PublicResource {
+
+        @Uri("a")
+        @ClientA // resolves to <base>/internal/a
+        private WebTarget targetA;
+
+        @GET
+        @Produces("text/plain")
+        @Path("a")
+        public String getTargetA() {
+            return targetA.request(MediaType.TEXT_PLAIN).get(String.class);
+        }
+
+        @GET
+        @Produces("text/plain")
+        @Path("b")
+        public Response getTargetB(@Uri("internal/b") @ClientB WebTarget targetB) {
+            return targetB.request(MediaType.TEXT_PLAIN).get();
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        ResourceConfig config = new ResourceConfig(PublicResource.class, InternalResource.class, CustomHeaderFeature.class)
+                .property(ClientA.class.getName() + ".baseUri", this.getBaseUri().toString() + "internal");
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        return config;
+    }
+
+    public static class MyClientAConfig extends ClientConfig {
+
+        public MyClientAConfig() {
+            this.register(new CustomHeaderFilter("custom-header", "a"));
+        }
+    }
+
+    public static class MyClientBConfig extends ClientConfig {
+
+        public MyClientBConfig() {
+            this.register(new CustomHeaderFilter("custom-header", "b"));
+        }
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.connectorProvider(new Apache5ConnectorProvider());
+    }
+
+    /**
+     * Test that a connection via managed clients works properly.
+     *
+     * @throws Exception in case of test failure.
+     */
+    @Test
+    public void testManagedClient() throws Exception {
+        final WebTarget resource = target().path("public").path("{name}");
+        Response response;
+
+        response = resource.resolveTemplate("name", "a").request(MediaType.TEXT_PLAIN).get();
+        assertEquals(200, response.getStatus());
+        assertEquals("a", response.readEntity(String.class));
+
+        response = resource.resolveTemplate("name", "b").request(MediaType.TEXT_PLAIN).get();
+        assertEquals(200, response.getStatus());
+        assertEquals("b", response.readEntity(String.class));
+    }
+
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/NoEntityTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/NoEntityTest.java
new file mode 100644
index 0000000..cdea49b
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/NoEntityTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.util.logging.Logger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+
+/**
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class NoEntityTest extends JerseyTest {
+    private static final Logger LOGGER = Logger.getLogger(NoEntityTest.class.getName());
+
+    @Path("/test")
+    public static class HttpMethodResource {
+        @GET
+        public Response get() {
+            return Response.status(Status.CONFLICT).build();
+        }
+
+        @POST
+        public void post(String entity) {
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        return config;
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.connectorProvider(new Apache5ConnectorProvider());
+    }
+
+    @Test
+    public void testGet() {
+        WebTarget r = target("test");
+
+        for (int i = 0; i < 5; i++) {
+            Response cr = r.request().get();
+            cr.close();
+        }
+    }
+
+    @Test
+    public void testGetWithClose() {
+        WebTarget r = target("test");
+        for (int i = 0; i < 5; i++) {
+            Response cr = r.request().get();
+            cr.close();
+        }
+    }
+
+    @Test
+    public void testPost() {
+        WebTarget r = target("test");
+        for (int i = 0; i < 5; i++) {
+            Response cr = r.request().post(null);
+        }
+    }
+
+    @Test
+    public void testPostWithClose() {
+        WebTarget r = target("test");
+        for (int i = 0; i < 5; i++) {
+            Response cr = r.request().post(null);
+            cr.close();
+        }
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/RetryStrategyTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/RetryStrategyTest.java
new file mode 100644
index 0000000..6254c43
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/RetryStrategyTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.io.IOException;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+
+import org.apache.hc.client5.http.HttpRequestRetryStrategy;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.util.TimeValue;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.RequestEntityProcessing;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+public class RetryStrategyTest extends JerseyTest {
+    private static final int READ_TIMEOUT_MS = 100;
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(RetryHandlerResource.class);
+    }
+
+    @Path("/")
+    public static class RetryHandlerResource {
+        private static volatile int postRequestNumber = 0;
+        private static volatile int getRequestNumber = 0;
+
+        // Cause a timeout on the first GET and POST request
+        @GET
+        public String get(@Context HttpHeaders h) {
+            if (getRequestNumber++ == 0) {
+                try {
+                    Thread.sleep(READ_TIMEOUT_MS * 10);
+                } catch (InterruptedException ex) {
+                    // ignore
+                }
+            }
+            return "GET";
+        }
+
+        @POST
+        public String post(@Context HttpHeaders h, String e) {
+            if (postRequestNumber++ == 0) {
+                try {
+                    Thread.sleep(READ_TIMEOUT_MS * 10);
+                } catch (InterruptedException ex) {
+                    // ignore
+                }
+            }
+            return "POST";
+        }
+    }
+
+    @Test
+    public void testRetryGet() throws IOException {
+        ClientConfig cc = new ClientConfig();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        cc.property(Apache5ClientProperties.RETRY_STRATEGY,
+                new HttpRequestRetryStrategy() {
+                    @Override
+                    public boolean retryRequest(HttpRequest request, IOException exception, int execCount, HttpContext context) {
+                        return true;
+                    }
+
+                    @Override
+                    public boolean retryRequest(HttpResponse response, int execCount, HttpContext context) {
+                        return true;
+                    }
+
+                    @Override
+                    public TimeValue getRetryInterval(HttpResponse response, int execCount, HttpContext context) {
+                        return TimeValue.ofMilliseconds(200);
+                    }
+                });
+        cc.property(ClientProperties.READ_TIMEOUT, READ_TIMEOUT_MS);
+        Client client = ClientBuilder.newClient(cc);
+
+        WebTarget r = client.target(getBaseUri());
+        assertEquals("GET", r.request().get(String.class));
+    }
+
+    @Test
+    public void testRetryPost() throws IOException {
+        ClientConfig cc = new ClientConfig();
+        cc.connectorProvider(new Apache5ConnectorProvider());
+        cc.property(Apache5ClientProperties.RETRY_STRATEGY,
+                new HttpRequestRetryStrategy() {
+                    @Override
+                    public boolean retryRequest(HttpRequest request, IOException exception, int execCount, HttpContext context) {
+                        return true;
+                    }
+
+                    @Override
+                    public boolean retryRequest(HttpResponse response, int execCount, HttpContext context) {
+                        return true;
+                    }
+
+                    @Override
+                    public TimeValue getRetryInterval(HttpResponse response, int execCount, HttpContext context) {
+                        return TimeValue.ofMilliseconds(200);
+                    }
+                });
+        cc.property(ClientProperties.READ_TIMEOUT, READ_TIMEOUT_MS);
+        Client client = ClientBuilder.newClient(cc);
+
+        WebTarget r = client.target(getBaseUri());
+        assertEquals("POST", r.request()
+                              .property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED)
+                              .post(Entity.text("POST"), String.class));
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/SpecialHeaderTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/SpecialHeaderTest.java
new file mode 100644
index 0000000..ce6a377
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/SpecialHeaderTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.message.GZipEncoder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ *
+ * @author Miroslav Fuksa
+ */
+public class SpecialHeaderTest extends JerseyTest {
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(MyResource.class, GZipEncoder.class, LoggingFeature.class);
+    }
+
+    @Path("resource")
+    public static class MyResource {
+        @GET
+        @Produces("text/plain")
+        @Path("encoded")
+        public Response getEncoded() {
+            return Response.ok("get").header(HttpHeaders.CONTENT_ENCODING, "gzip").build();
+        }
+
+        @GET
+        @Produces("text/plain")
+        @Path("non-encoded")
+        public Response getNormal() {
+            return Response.ok("get").build();
+        }
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.connectorProvider(new Apache5ConnectorProvider());
+    }
+
+
+    @Test
+    @Ignore("Apache connector does not provide information about encoding for gzip and deflate encoding")
+    public void testEncoded() {
+        final Response response = target().path("resource/encoded").request("text/plain").get();
+        Assert.assertEquals(200, response.getStatus());
+        Assert.assertEquals("get", response.readEntity(String.class));
+        Assert.assertEquals("gzip", response.getHeaderString(HttpHeaders.CONTENT_ENCODING));
+        Assert.assertEquals("text/plain", response.getHeaderString(HttpHeaders.CONTENT_TYPE));
+        Assert.assertEquals(3, response.getHeaderString(HttpHeaders.CONTENT_LENGTH));
+    }
+
+    @Test
+    public void testNonEncoded() {
+        final Response response = target().path("resource/non-encoded").request("text/plain").get();
+        Assert.assertEquals(200, response.getStatus());
+        Assert.assertEquals("get", response.readEntity(String.class));
+        Assert.assertNull(response.getHeaderString(HttpHeaders.CONTENT_ENCODING));
+        Assert.assertEquals("text/plain", response.getHeaderString(HttpHeaders.CONTENT_TYPE));
+        Assert.assertEquals("3", response.getHeaderString(HttpHeaders.CONTENT_LENGTH));
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/StreamingTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/StreamingTest.java
new file mode 100644
index 0000000..ea90e79
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/StreamingTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.inject.Singleton;
+
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.server.ChunkedOutput;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Petr Janouch
+ */
+public class StreamingTest extends JerseyTest {
+    private PoolingHttpClientConnectionManager connectionManager;
+
+    /**
+     * Test that a data stream can be terminated from the client side.
+     */
+    @Test
+    public void clientCloseNoTimeoutTest() throws IOException {
+        clientCloseTest(-1);
+    }
+
+    @Test
+    public void clientCloseWithTimeOutTest() throws IOException {
+        clientCloseTest(1_000);
+    }
+
+    /**
+     * Tests that closing a response after completely reading the entity reuses the connection
+     */
+    @Test
+    public void reuseConnectionTest() throws IOException {
+        Response response = target().path("/streamingEndpoint/get").request().get();
+        InputStream is = response.readEntity(InputStream.class);
+        byte[] buf = new byte[8192];
+        is.read(buf);
+        is.close();
+        response.close();
+
+        assertEquals(1, connectionManager.getTotalStats().getAvailable());
+        assertEquals(0, connectionManager.getTotalStats().getLeased());
+    }
+
+    /**
+     * Tests that closing a request without reading the entity does not throw an exception.
+     */
+    @Test
+    public void clientCloseThrowsNoExceptionTest() throws IOException {
+        Response response = target().path("/streamingEndpoint/get").request().get();
+        response.close();
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        connectionManager = new PoolingHttpClientConnectionManager();
+        config.property(Apache5ClientProperties.CONNECTION_MANAGER, connectionManager);
+        config.connectorProvider(new Apache5ConnectorProvider());
+    }
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(StreamingEndpoint.class);
+    }
+
+    /**
+     * Test that a data stream can be terminated from the client side.
+     */
+    private void clientCloseTest(int readTimeout) throws IOException {
+        // start streaming
+        AtomicInteger counter = new AtomicInteger(0);
+        Invocation.Builder builder = target().path("/streamingEndpoint").request();
+        if (readTimeout > -1) {
+            counter.set(1);
+            builder.property(ClientProperties.READ_TIMEOUT, readTimeout);
+            builder.property(Apache5ClientProperties.CONNECTION_CLOSING_STRATEGY,
+                    (Apache5ConnectionClosingStrategy) (config, request, response, stream) -> {
+                try {
+                    stream.close();
+                } catch (Exception e) {
+                    // timeout, no chunk ending
+                } finally {
+                    counter.set(0);
+                    response.close();
+                }
+            });
+        }
+        InputStream inputStream = builder.get(InputStream.class);
+
+        WebTarget sendTarget = target().path("/streamingEndpoint/send");
+        // trigger sending 'A' to the stream; OK is sent if everything on the server was OK
+        assertEquals("OK", sendTarget.request().get().readEntity(String.class));
+        // check 'A' has been sent
+        assertEquals('A', inputStream.read());
+        // closing the stream should tear down the connection
+        inputStream.close();
+        // trigger sending another 'A' to the stream; it should fail
+        // (indicating that the streaming has been terminated on the server)
+        assertEquals("NOK", sendTarget.request().get().readEntity(String.class));
+        assertEquals(0, counter.get());
+    }
+
+    @Singleton
+    @Path("streamingEndpoint")
+    public static class StreamingEndpoint {
+
+        private final ChunkedOutput<String> output = new ChunkedOutput<>(String.class);
+
+        @GET
+        @Path("send")
+        public String sendEvent() {
+            try {
+                output.write("A");
+            } catch (IOException e) {
+                return "NOK";
+            }
+
+            return "OK";
+        }
+
+        @GET
+        @Produces(MediaType.TEXT_PLAIN)
+        public ChunkedOutput<String> get() {
+            return output;
+        }
+
+        @GET
+        @Path("get")
+        @Produces(MediaType.TEXT_PLAIN)
+        public String getString() {
+            return "OK";
+        }
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TimeoutTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TimeoutTest.java
new file mode 100644
index 0000000..7eab1b3
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TimeoutTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.net.SocketTimeoutException;
+import java.util.logging.Logger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.ProcessingException;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+/**
+ * @author Martin Matula
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ */
+public class TimeoutTest extends JerseyTest {
+    private static final Logger LOGGER = Logger.getLogger(TimeoutTest.class.getName());
+
+    @Path("/test")
+    public static class TimeoutResource {
+        @GET
+        public String get() {
+            return "GET";
+        }
+
+        @GET
+        @Path("timeout")
+        public String getTimeout() {
+            try {
+                Thread.sleep(2000);
+            } catch (final InterruptedException e) {
+                e.printStackTrace();
+            }
+            return "GET";
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        final ResourceConfig config = new ResourceConfig(TimeoutResource.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        return config;
+    }
+
+    @Override
+    protected void configureClient(final ClientConfig config) {
+        config.property(ClientProperties.READ_TIMEOUT, 1000);
+        config.connectorProvider(new Apache5ConnectorProvider());
+    }
+
+    @Test
+    public void testFast() {
+        final Response r = target("test").request().get();
+        assertEquals(200, r.getStatus());
+        assertEquals("GET", r.readEntity(String.class));
+    }
+
+    @Test
+    public void testSlow() {
+        try {
+            target("test/timeout").request().get();
+            fail("Timeout expected.");
+        } catch (final ProcessingException e) {
+            assertThat("Unexpected processing exception cause",
+                    e.getCause(), instanceOf(SocketTimeoutException.class));
+        }
+    }
+
+    @Test
+    public void testPerRequestTimeout() {
+        final Response r = target("test/timeout").request()
+                .property(ClientProperties.READ_TIMEOUT, 3000).get();
+        assertEquals(200, r.getStatus());
+        assertEquals("GET", r.readEntity(String.class));
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TraceSupportTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TraceSupportTest.java
new file mode 100644
index 0000000..d95c2f2
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/TraceSupportTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Request;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.process.Inflector;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.model.Resource;
+import org.glassfish.jersey.test.JerseyTest;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * This very basic resource showcases support of a HTTP TRACE method,
+ * not directly supported by JAX-RS API.
+ *
+ * @author Marek Potociar
+ */
+public class TraceSupportTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(TraceSupportTest.class.getName());
+
+    /**
+     * Programmatic tracing root resource path.
+     */
+    public static final String ROOT_PATH_PROGRAMMATIC = "tracing/programmatic";
+
+    /**
+     * Annotated class-based tracing root resource path.
+     */
+    public static final String ROOT_PATH_ANNOTATED = "tracing/annotated";
+
+    @HttpMethod(TRACE.NAME)
+    @Target(ElementType.METHOD)
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface TRACE {
+        public static final String NAME = "TRACE";
+    }
+
+    @Path(ROOT_PATH_ANNOTATED)
+    public static class TracingResource {
+
+        @TRACE
+        @Produces("text/plain")
+        public String trace(Request request) {
+            return stringify((ContainerRequest) request);
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        ResourceConfig config = new ResourceConfig(TracingResource.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        final Resource.Builder resourceBuilder = Resource.builder(ROOT_PATH_PROGRAMMATIC);
+        resourceBuilder.addMethod(TRACE.NAME).handledBy(new Inflector<ContainerRequestContext, Response>() {
+
+            @Override
+            public Response apply(ContainerRequestContext request) {
+                if (request == null) {
+                    return Response.noContent().build();
+                } else {
+                    return Response.ok(stringify((ContainerRequest) request), MediaType.TEXT_PLAIN).build();
+                }
+            }
+        });
+
+        return config.registerResources(resourceBuilder.build());
+
+    }
+
+    private String[] expectedFragmentsProgrammatic = new String[]{
+            "TRACE http://localhost:" + this.getPort() + "/tracing/programmatic"
+    };
+    private String[] expectedFragmentsAnnotated = new String[]{
+            "TRACE http://localhost:" + this.getPort() + "/tracing/annotated"
+    };
+
+    private WebTarget prepareTarget(String path) {
+        final WebTarget target = target();
+        target.register(LoggingFeature.class);
+        return target.path(path);
+    }
+
+    @Test
+    public void testProgrammaticApp() throws Exception {
+        Response response = prepareTarget(ROOT_PATH_PROGRAMMATIC).request("text/plain").method(TRACE.NAME);
+
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode());
+
+        String responseEntity = response.readEntity(String.class);
+        for (String expectedFragment : expectedFragmentsProgrammatic) {
+            assertTrue("Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity,
+                    // toLowerCase - http header field names are case insensitive
+                    responseEntity.contains(expectedFragment));
+        }
+    }
+
+    @Test
+    public void testAnnotatedApp() throws Exception {
+        Response response = prepareTarget(ROOT_PATH_ANNOTATED).request("text/plain").method(TRACE.NAME);
+
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatusInfo().getStatusCode());
+
+        String responseEntity = response.readEntity(String.class);
+        for (String expectedFragment : expectedFragmentsAnnotated) {
+            assertTrue("Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity,
+                    // toLowerCase - http header field names are case insensitive
+                    responseEntity.contains(expectedFragment));
+        }
+    }
+
+    @Test
+    public void testTraceWithEntity() throws Exception {
+        _testTraceWithEntity(false, false);
+    }
+
+    @Test
+    public void testAsyncTraceWithEntity() throws Exception {
+        _testTraceWithEntity(true, false);
+    }
+
+    @Test
+    public void testTraceWithEntityApacheConnector() throws Exception {
+        _testTraceWithEntity(false, true);
+    }
+
+    @Test
+    public void testAsyncTraceWithEntityApacheConnector() throws Exception {
+        _testTraceWithEntity(true, true);
+    }
+
+    private void _testTraceWithEntity(final boolean isAsync, final boolean useApacheConnection) throws Exception {
+        try {
+            WebTarget target = useApacheConnection ? getApacheClient().target(target().getUri()) : target();
+            target = target.path(ROOT_PATH_ANNOTATED);
+
+            final Entity<String> entity = Entity.entity("trace", MediaType.WILDCARD_TYPE);
+
+            Response response;
+            if (!isAsync) {
+                response = target.request().method(TRACE.NAME, entity);
+            } else {
+                response = target.request().async().method(TRACE.NAME, entity).get();
+            }
+
+            fail("A TRACE request MUST NOT include an entity. (response=" + response + ")");
+        } catch (Exception e) {
+            // OK
+        }
+    }
+
+    private Client getApacheClient() {
+        return ClientBuilder.newClient(new ClientConfig().connectorProvider(new Apache5ConnectorProvider()));
+    }
+
+
+    public static String stringify(ContainerRequest request) {
+        StringBuilder buffer = new StringBuilder();
+
+        printRequestLine(buffer, request);
+        printPrefixedHeaders(buffer, request.getHeaders());
+
+        if (request.hasEntity()) {
+            buffer.append(request.readEntity(String.class)).append("\n");
+        }
+
+        return buffer.toString();
+    }
+
+    private static void printRequestLine(StringBuilder buffer, ContainerRequest request) {
+        buffer.append(request.getMethod()).append(" ").append(request.getUriInfo().getRequestUri().toASCIIString()).append("\n");
+    }
+
+    private static void printPrefixedHeaders(StringBuilder buffer, Map<String, List<String>> headers) {
+        for (Map.Entry<String, List<String>> e : headers.entrySet()) {
+            List<String> val = e.getValue();
+            String header = e.getKey();
+
+            if (val.size() == 1) {
+                buffer.append(header).append(": ").append(val.get(0)).append("\n");
+            } else {
+                StringBuilder sb = new StringBuilder();
+                boolean add = false;
+                for (String s : val) {
+                    if (add) {
+                        sb.append(',');
+                    }
+                    add = true;
+                    sb.append(s);
+                }
+                buffer.append(header).append(": ").append(sb.toString()).append("\n");
+            }
+        }
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingCookieStoreAccessTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingCookieStoreAccessTest.java
new file mode 100644
index 0000000..8248fdd
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingCookieStoreAccessTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+
+import org.apache.hc.client5.http.cookie.CookieStore;
+import org.glassfish.jersey.client.ClientConfig;
+
+import org.junit.Test;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+/**
+ * Test of access to the underlying CookieStore instance used by the connector.
+ *
+ * @author Maksim Mukosey (mmukosey at gmail.com)
+ */
+public class UnderlyingCookieStoreAccessTest {
+
+    @Test
+    public void testCookieStoreInstanceAccess() {
+        final Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new Apache5ConnectorProvider()));
+        final CookieStore csOnClient = Apache5ConnectorProvider.getCookieStore(client);
+        // important: the web target instance in this test must be only created AFTER the client has been pre-initialized
+        // (see org.glassfish.jersey.client.Initializable.preInitialize method). This is here achieved by calling the
+        // connector provider's static getCookieStore method above.
+        final WebTarget target = client.target("http://localhost/");
+        final CookieStore csOnTarget = Apache5ConnectorProvider.getCookieStore(target);
+
+        assertNotNull("CookieStore instance set on JerseyClient should not be null.", csOnClient);
+        assertNotNull("CookieStore instance set on JerseyWebTarget should not be null.", csOnTarget);
+        assertSame("CookieStore instance set on JerseyClient should be the same instance as the one set on JerseyWebTarget"
+                + "(provided the target instance has not been further configured).", csOnClient, csOnTarget);
+    }
+}
diff --git a/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingHttpClientAccessTest.java b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingHttpClientAccessTest.java
new file mode 100644
index 0000000..0c2e320
--- /dev/null
+++ b/connectors/apache5-connector/src/test/java/org/glassfish/jersey/apache5/connector/UnderlyingHttpClientAccessTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.apache5.connector;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.glassfish.jersey.client.ClientConfig;
+
+import org.junit.Test;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+/**
+ * Test of access to the underlying HTTP client instance used by the connector.
+ *
+ * @author Marek Potociar
+ */
+public class UnderlyingHttpClientAccessTest {
+
+    /**
+     * Verifier of JERSEY-2424 fix.
+     */
+    @Test
+    public void testHttpClientInstanceAccess() {
+        final Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new Apache5ConnectorProvider()));
+        final HttpClient hcOnClient = Apache5ConnectorProvider.getHttpClient(client);
+        // important: the web target instance in this test must be only created AFTER the client has been pre-initialized
+        // (see org.glassfish.jersey.client.Initializable.preInitialize method). This is here achieved by calling the
+        // connector provider's static getHttpClient method above.
+        final WebTarget target = client.target("http://localhost/");
+        final HttpClient hcOnTarget = Apache5ConnectorProvider.getHttpClient(target);
+
+        assertNotNull("HTTP client instance set on JerseyClient should not be null.", hcOnClient);
+        assertNotNull("HTTP client instance set on JerseyWebTarget should not be null.", hcOnTarget);
+        assertSame("HTTP client instance set on JerseyClient should be the same instance as the one set on JerseyWebTarget"
+                        + "(provided the target instance has not been further configured).",
+                hcOnClient, hcOnTarget
+        );
+    }
+}
diff --git a/connectors/pom.xml b/connectors/pom.xml
index 8266de6..18fce3c 100644
--- a/connectors/pom.xml
+++ b/connectors/pom.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 
-    Copyright (c) 2011, 2021 Oracle and/or its affiliates. All rights reserved.
+    Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved.
 
     This program and the accompanying materials are made available under the
     terms of the Eclipse Public License v. 2.0, which is available at
@@ -35,6 +35,7 @@
 
     <modules>
         <module>apache-connector</module>
+        <module>apache5-connector</module>
         <module>grizzly-connector</module>
         <module>jdk-connector</module>
         <module>jetty-connector</module>
diff --git a/pom.xml b/pom.xml
index 62bcb7b..8f3bb20 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1625,6 +1625,11 @@
                 <artifactId>httpclient</artifactId>
                 <version>${httpclient.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.apache.httpcomponents.client5</groupId>
+                <artifactId>httpclient5</artifactId>
+                <version>${httpclient5.version}</version>
+            </dependency>
 
             <dependency>
                 <groupId>org.eclipse.jetty</groupId>
@@ -2125,6 +2130,7 @@
         <hk2.jvnet.osgi.version>org.jvnet.hk2.*;version="[2.5,4)"</hk2.jvnet.osgi.version>
         <hk2.config.version>5.1.0</hk2.config.version>
         <httpclient.version>4.5.13</httpclient.version>
+        <httpclient5.version>5.1.2</httpclient5.version>
         <jackson.version>2.13.0</jackson.version>
         <jackson1.version>1.9.13</jackson1.version>
         <javassist.version>3.25.0-GA</javassist.version>