Merge remote-tracking branch 'upstream/3.0' into 'upstream/3.1'

Signed-off-by: Maxim Nesen <maxim.nesen@oracle.com>
diff --git a/connectors/jetty11-connector/pom.xml b/connectors/jetty11-connector/pom.xml
index 32b76c4..8862fb8 100644
--- a/connectors/jetty11-connector/pom.xml
+++ b/connectors/jetty11-connector/pom.xml
@@ -36,6 +36,25 @@
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     </properties>
 
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.eclipse.jetty</groupId>
+                <artifactId>jetty-server</artifactId>
+                <version>${jetty11.version}</version>
+            </dependency><dependency>
+                <groupId>org.eclipse.jetty</groupId>
+                <artifactId>jetty-client</artifactId>
+                <version>${jetty11.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.jetty</groupId>
+                <artifactId>jetty-util</artifactId>
+                <version>${jetty11.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
     <dependencies>
         <dependency>
             <groupId>org.eclipse.jetty</groupId>
@@ -46,10 +65,6 @@
                     <groupId>org.slf4j</groupId>
                     <artifactId>slf4j-api</artifactId>
                 </exclusion>
-                <exclusion>
-                    <groupId>org.eclipse.jetty</groupId>
-                    <artifactId>jetty-util</artifactId>
-                </exclusion>
             </exclusions>
         </dependency>
         <dependency>
diff --git a/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11Connector.java b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11Connector.java
index 24f10fc..beb723d 100644
--- a/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11Connector.java
+++ b/connectors/jetty11-connector/src/main/java/org/glassfish/jersey/jetty11/connector/Jetty11Connector.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2023 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
@@ -16,43 +16,33 @@
 
 package org.glassfish.jersey.jetty11.connector;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.FilterInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.CookieStore;
-import java.net.URI;
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
 import jakarta.ws.rs.ProcessingException;
 import jakarta.ws.rs.client.Client;
 import jakarta.ws.rs.core.Configuration;
 import jakarta.ws.rs.core.MultivaluedMap;
-
-import javax.net.ssl.SSLContext;
-
+import org.eclipse.jetty.client.HttpClient;
 import org.eclipse.jetty.client.HttpClientTransport;
-import org.eclipse.jetty.client.HttpRequest;
+import org.eclipse.jetty.client.HttpProxy;
+import org.eclipse.jetty.client.ProxyConfiguration;
+import org.eclipse.jetty.client.api.AuthenticationStore;
+import org.eclipse.jetty.client.api.ContentProvider;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.api.Result;
 import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
-import org.eclipse.jetty.io.ClientConnector;
 import org.eclipse.jetty.client.util.BasicAuthentication;
 import org.eclipse.jetty.client.util.BytesContentProvider;
 import org.eclipse.jetty.client.util.FutureResponseListener;
 import org.eclipse.jetty.client.util.OutputStreamContentProvider;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.io.ClientConnector;
+import org.eclipse.jetty.util.HttpCookieStore;
+import org.eclipse.jetty.util.Jetty;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
 import org.glassfish.jersey.client.ClientProperties;
 import org.glassfish.jersey.client.ClientRequest;
 import org.glassfish.jersey.client.ClientResponse;
@@ -65,22 +55,30 @@
 import org.glassfish.jersey.message.internal.OutboundMessageContext;
 import org.glassfish.jersey.message.internal.Statuses;
 
-import org.eclipse.jetty.client.HttpClient;
-import org.eclipse.jetty.client.HttpProxy;
-import org.eclipse.jetty.client.ProxyConfiguration;
-import org.eclipse.jetty.client.api.AuthenticationStore;
-import org.eclipse.jetty.client.api.ContentProvider;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.api.Request;
-import org.eclipse.jetty.client.api.Response;
-import org.eclipse.jetty.client.api.Result;
-import org.eclipse.jetty.http.HttpField;
-import org.eclipse.jetty.http.HttpFields;
-import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.util.HttpCookieStore;
-import org.eclipse.jetty.util.Jetty;
-import org.eclipse.jetty.util.ssl.SslContextFactory;
-import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import javax.net.ssl.SSLContext;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.CookieStore;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 /**
  * A {@link Connector} that utilizes the Jetty HTTP Client to send and receive
@@ -109,7 +107,7 @@
  * <pre>
  * {@code
  * ClientConfig config = new ClientConfig();
- * Connector connector = new JettyConnector(config);
+ * Connector connector = new Jetty11Connector(config);
  * config.connector(connector);
  * Client client = ClientBuilder.newClient(config);
  *
@@ -131,7 +129,7 @@
  * @author Arul Dhesiaseelan (aruld at acm.org)
  * @author Marek Potociar
  */
-class Jetty11Connector implements Connector {
+public class Jetty11Connector implements Connector {
 
     private static final Logger LOGGER = Logger.getLogger(Jetty11Connector.class.getName());
 
@@ -146,23 +144,17 @@
      * @param jaxrsClient JAX-RS client instance, for which the connector is created.
      * @param config client configuration.
      */
-    Jetty11Connector(final Client jaxrsClient, final Configuration config) {
+    protected Jetty11Connector(final Client jaxrsClient, final Configuration config) {
         this.configuration = config;
-        HttpClient httpClient = null;
-        if (config.isRegistered(Jetty11HttpClientSupplier.class)) {
-            Optional<Object> contract = config.getInstances().stream()
-                    .filter(a-> Jetty11HttpClientSupplier.class.isInstance(a)).findFirst();
-            if (contract.isPresent()) {
-                httpClient = ((Jetty11HttpClientSupplier) contract.get()).getHttpClient();
-            }
-        }
+        HttpClient httpClient = getRegisteredHttpClient(config);
+
         if (httpClient == null) {
             final SSLContext sslContext = jaxrsClient.getSslContext();
             final SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(false);
             sslContextFactory.setSslContext(sslContext);
             final ClientConnector connector = new ClientConnector();
             connector.setSslContextFactory(sslContextFactory);
-            final HttpClientTransport transport = new HttpClientTransportOverHTTP(connector);
+            final HttpClientTransport transport = initClientTransport(connector);
             httpClient = new HttpClient(transport);
         }
         this.client = httpClient;
@@ -201,7 +193,7 @@
         proxy.ifPresent(clientProxy -> {
             final ProxyConfiguration proxyConfig = client.getProxyConfiguration();
             final URI u = clientProxy.uri();
-            proxyConfig.getProxies().add(new HttpProxy(u.getHost(), u.getPort()));
+            proxyConfig.addProxy(new HttpProxy(u.getHost(), u.getPort()));
 
             if (clientProxy.userName() != null) {
                 auth.addAuthentication(new BasicAuthentication(u, "<<ANY_REALM>>",
@@ -214,9 +206,9 @@
         }
 
         final Object slResponseMaxSize = configuration.getProperties()
-            .get(Jetty11ClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE);
+                .get(Jetty11ClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE);
         if (slResponseMaxSize != null && slResponseMaxSize instanceof Integer
-            && (Integer) slResponseMaxSize > 0) {
+                && (Integer) slResponseMaxSize > 0) {
             this.syncListenerResponseMaxSize = Optional.of((Integer) slResponseMaxSize);
         }
         else {
@@ -232,6 +224,37 @@
     }
 
     /**
+     * provides required HTTP client transport for client
+     *
+     * the default transport is {@link HttpClientTransportOverHTTP}
+     *
+     * @return instance of {@link HttpClientTransport}
+     * @since 2.41
+     */
+    protected HttpClientTransport initClientTransport(ClientConnector clientConnector) {
+        return new HttpClientTransportOverHTTP(clientConnector);
+    }
+
+    /**
+     * provides custom registered {@link HttpClient} if any (or NULL)
+     *
+     * @param config configuration where {@link HttpClient} could be registered
+     * @return {@link HttpClient} instance if any was previously registered or NULL
+     *
+     * @since 2.41
+     */
+    protected HttpClient getRegisteredHttpClient(Configuration config) {
+        if (config.isRegistered(Jetty11HttpClientSupplier.class)) {
+            Optional<Object> contract = config.getInstances().stream()
+                    .filter(a-> Jetty11HttpClientSupplier.class.isInstance(a)).findFirst();
+            if (contract.isPresent()) {
+                return  ((Jetty11HttpClientSupplier) contract.get()).getHttpClient();
+            }
+        }
+        return null;
+    }
+
+    /**
      * Get the {@link HttpClient}.
      *
      * @return the {@link HttpClient}.
@@ -245,7 +268,7 @@
      * Get the {@link CookieStore}.
      *
      * @return the {@link CookieStore} instance or null when
-     * JettyClientProperties.DISABLE_COOKIES set to true.
+     * Jetty11ClientProperties.DISABLE_COOKIES set to true.
      */
     public CookieStore getCookieStore() {
         return cookieStore;
@@ -254,10 +277,13 @@
     @Override
     public ClientResponse apply(final ClientRequest jerseyRequest) throws ProcessingException {
         final Request jettyRequest = translateRequest(jerseyRequest);
-        final Map<String, String> clientHeadersSnapshot = writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest);
-        final ContentProvider entity = getBytesProvider(jerseyRequest);
+        final Map<String, String> clientHeadersSnapshot = new HashMap<>();
+        final ContentProvider entity =
+                getBytesProvider(jerseyRequest, jerseyRequest.getHeaders(), clientHeadersSnapshot, jettyRequest);
         if (entity != null) {
             jettyRequest.content(entity);
+        } else {
+            clientHeadersSnapshot.putAll(writeOutBoundHeaders(jerseyRequest.getHeaders(), jettyRequest));
         }
 
         try {
@@ -267,12 +293,12 @@
             }
             else {
                 final FutureResponseListener listener
-                    = new FutureResponseListener(jettyRequest, syncListenerResponseMaxSize.get());
+                        = new FutureResponseListener(jettyRequest, syncListenerResponseMaxSize.get());
                 jettyRequest.send(listener);
                 jettyResponse = listener.get();
             }
             HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, jerseyRequest.getHeaders(),
-                                           Jetty11Connector.this.getClass().getName(), jerseyRequest.getConfiguration());
+                    Jetty11Connector.this.getClass().getName(), jerseyRequest.getConfiguration());
 
             final jakarta.ws.rs.core.Response.StatusType status = jettyResponse.getReason() == null
                     ? Statuses.from(jettyResponse.getStatus())
@@ -338,19 +364,22 @@
 
     private Map<String, String> writeOutBoundHeaders(final MultivaluedMap<String, Object> headers, final Request request) {
         final Map<String, String> stringHeaders = HeaderUtils.asStringHeadersSingleValue(headers, configuration);
-
-        // remove User-agent header set by Jetty; Jersey already sets this in its request (incl. Jetty version)
-        request.headers(httpFields -> httpFields.remove(HttpHeader.USER_AGENT));
-        if (request instanceof HttpRequest) {
-            final HttpRequest httpRequest = (HttpRequest) request;
+        final Consumer<HttpFields.Mutable> mutableConsumer = httpFields -> {
+            // remove User-agent header set by Jetty; Jersey already sets this in its request (incl. Jetty version)
+            httpFields.remove(HttpHeader.USER_AGENT);
             for (final Map.Entry<String, String> e : stringHeaders.entrySet()) {
-                httpRequest.addHeader(new HttpField(e.getKey(), e.getValue()));
+                httpFields.put(e.getKey(), e.getValue());
             }
-        }
+        };
+        request.headers(mutableConsumer);
+
         return stringHeaders;
     }
 
-    private ContentProvider getBytesProvider(final ClientRequest clientRequest) {
+    private ContentProvider getBytesProvider(final ClientRequest clientRequest,
+                                             final MultivaluedMap<String, Object> headers,
+                                             final Map<String, String> snapshot,
+                                             final Request request) {
         final Object entity = clientRequest.getEntity();
 
         if (entity == null) {
@@ -361,6 +390,7 @@
         clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {
             @Override
             public OutputStream getOutputStream(final int contentLength) throws IOException {
+                snapshot.putAll(writeOutBoundHeaders(headers, request));
                 return outputStream;
             }
         });
@@ -414,13 +444,13 @@
         try {
             final CompletableFuture<ClientResponse> responseFuture = new CompletableFuture<ClientResponse>();
             responseFuture.whenComplete(
-                            (clientResponse, throwable) -> {
-                                if (throwable != null && throwable instanceof CancellationException) {
-                                    // take care of future cancellation
-                                    jettyRequest.abort(throwable);
+                    (clientResponse, throwable) -> {
+                        if (throwable != null && throwable instanceof CancellationException) {
+                            // take care of future cancellation
+                            jettyRequest.abort(throwable);
 
-                                }
-                            });
+                        }
+                    });
 
             final AtomicReference<ClientResponse> jerseyResponse = new AtomicReference<>();
             final ByteBufferInputStream entityStream = new ByteBufferInputStream();
@@ -429,7 +459,7 @@
                 @Override
                 public void onHeaders(final Response jettyResponse) {
                     HeaderUtils.checkHeaderChanges(clientHeadersSnapshot, jerseyRequest.getHeaders(),
-                                                   Jetty11Connector.this.getClass().getName(), jerseyRequest.getConfiguration());
+                            Jetty11Connector.this.getClass().getName(), jerseyRequest.getConfiguration());
 
                     if (responseFuture.isDone()) {
                         if (!callbackInvoked.compareAndSet(false, true)) {
@@ -498,7 +528,7 @@
     }
 
     private static ClientResponse translateResponse(final ClientRequest jerseyRequest,
-                                                    final Response jettyResponse,
+                                                    final org.eclipse.jetty.client.api.Response jettyResponse,
                                                     final NonBlockingInputStream entityStream) {
         final ClientResponse jerseyResponse = new ClientResponse(Statuses.from(jettyResponse.getStatus()), jerseyRequest);
         processResponseHeaders(jettyResponse.getHeaders(), jerseyResponse);
@@ -519,4 +549,4 @@
             throw new ProcessingException("Failed to stop the client.", e);
         }
     }
-}
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/pom.xml b/connectors/jetty11-http2-connector/pom.xml
new file mode 100644
index 0000000..c3633ed
--- /dev/null
+++ b/connectors/jetty11-http2-connector/pom.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0"?>
+<!--
+
+    Copyright (c) 2023 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>3.1.99-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>jersey-jetty11-http2-connector</artifactId>
+    <packaging>jar</packaging>
+    <name>jersey-connectors-jetty11-http2</name>
+
+    <description>Jersey Client Transport via Jetty 11</description>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <java8.build.outputDirectory>${project.basedir}/target</java8.build.outputDirectory>
+        <java8.sourceDirectory>${project.basedir}/src/main/java8</java8.sourceDirectory>
+        <java11.build.outputDirectory>${project.basedir}/target11</java11.build.outputDirectory>
+        <java11.sourceDirectory>${project.basedir}/src/main/java11</java11.sourceDirectory>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.eclipse.jetty</groupId>
+                <artifactId>jetty-server</artifactId>
+                <version>${jetty11.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.jetty</groupId>
+                <artifactId>jetty-client</artifactId>
+                <version>${jetty11.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.jetty</groupId>
+                <artifactId>jetty-util</artifactId>
+                <version>${jetty11.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.jetty.http2</groupId>
+                <artifactId>http2-server</artifactId>
+                <version>${jetty11.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.jetty</groupId>
+                <artifactId>jetty-alpn-conscrypt-server</artifactId>
+                <version>${jetty11.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.jetty.http2</groupId>
+                <artifactId>http2-client</artifactId>
+                <version>${jetty11.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.jetty.http2</groupId>
+                <artifactId>http2-http-client-transport</artifactId>
+                <version>${jetty11.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-client</artifactId>
+            <version>${jetty11.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty.http2</groupId>
+            <artifactId>http2-client</artifactId>
+            <version>${jetty11.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty.http2</groupId>
+            <artifactId>http2-http-client-transport</artifactId>
+            <version>${jetty11.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-util</artifactId>
+            <version>${jetty11.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.connectors</groupId>
+            <artifactId>jersey-jetty11-connector</artifactId>
+            <version>${project.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.eclipse.jetty</groupId>
+                    <artifactId>jetty-client</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.media</groupId>
+            <artifactId>jersey-media-jaxb</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.containers</groupId>
+            <artifactId>jersey-container-jetty11-http2</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.eclipse.jetty</groupId>
+                    <artifactId>http2-server</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.media</groupId>
+            <artifactId>jersey-media-json-jackson</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+            <artifactId>jersey-test-framework-provider-jetty11-http2</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+            <dependency>
+                <groupId>com.sun.xml.bind</groupId>
+                <artifactId>jaxb-osgi</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>
+                <configuration>
+                    <instructions>
+                        <Import-Package>
+                            ${jetty.osgi.version},
+                            *
+                        </Import-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2ClientSupplier.java b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2ClientSupplier.java
new file mode 100644
index 0000000..f13ef3d
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2ClientSupplier.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpClientTransport;
+import org.eclipse.jetty.http2.client.HTTP2Client;
+import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
+import org.glassfish.jersey.jetty11.connector.Jetty11Connector;
+import org.glassfish.jersey.jetty11.connector.Jetty11HttpClientContract;
+import org.glassfish.jersey.jetty11.connector.Jetty11HttpClientSupplier;
+
+/**
+ * HTTP/2 enabled version of the {@link Jetty11HttpClientSupplier}
+ *
+ * @since 2.41
+ */
+public class Jetty11Http2ClientSupplier implements Jetty11HttpClientContract {
+    private final HttpClient http2Client;
+
+    /**
+     * default Http2Client created for the supplier.
+     */
+    public Jetty11Http2ClientSupplier() {
+        this(createHttp2Client());
+    }
+    /**
+     * supplier for the {@code HttpClient} with {@code HttpClientTransportOverHTTP2} to be optionally registered
+     * to a {@link org.glassfish.jersey.client.ClientConfig}
+     * @param http2Client a HttpClient to be supplied when {@link Jetty11Connector#getHttpClient()} is called.
+     */
+    public Jetty11Http2ClientSupplier(HttpClient http2Client) {
+        this.http2Client = http2Client;
+    }
+
+    private static final HttpClient createHttp2Client() {
+        final HttpClientTransport transport =  new HttpClientTransportOverHTTP2(new HTTP2Client());
+        return new HttpClient(transport);
+    }
+
+    @Override
+    public HttpClient getHttpClient() {
+        return http2Client;
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2Connector.java b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2Connector.java
new file mode 100644
index 0000000..35dc504
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2Connector.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Configuration;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpClientTransport;
+import org.eclipse.jetty.http2.client.HTTP2Client;
+import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
+import org.eclipse.jetty.io.ClientConnector;
+import org.glassfish.jersey.jetty11.connector.Jetty11Connector;
+
+import java.util.Optional;
+
+/**
+ * Extends {@link Jetty11Connector} with HTTP/2 transport support
+ *
+ * @since 2.41
+ */
+class Jetty11Http2Connector extends Jetty11Connector {
+
+
+    /**
+     * Create the new Jetty HTTP/2 client connector.
+     *
+     * @param jaxrsClient JAX-RS client instance, for which the connector is created.
+     * @param config      client configuration.
+     */
+    Jetty11Http2Connector(Client jaxrsClient, Configuration config) {
+        super(jaxrsClient, config);
+    }
+
+    /**
+     * provides required {@link HttpClientTransport} for client
+     *
+     * The overriden method provides {@link HttpClientTransportOverHTTP2} with initialized {@link HTTP2Client}
+     *
+     * @return {@link HttpClientTransportOverHTTP2}
+     * @since 2.41
+     */
+    @Override
+    protected HttpClientTransport initClientTransport(ClientConnector clientConnector) {
+        return new HttpClientTransportOverHTTP2(new HTTP2Client(clientConnector));
+    }
+
+    /**
+     * provides custom registered {@link HttpClient} (if any) with HTTP/2 support
+     *
+     * @param config configuration where {@link HttpClient} could be registered
+     * @return {@link HttpClient} instance if any was previously registered or NULL
+     *
+     * @since 2.41
+     */
+    @Override
+    protected HttpClient getRegisteredHttpClient(Configuration config) {
+        if (config.isRegistered(Jetty11Http2ClientSupplier.class)) {
+            Optional<Object> contract = config.getInstances().stream()
+                    .filter(a-> Jetty11Http2ClientSupplier.class.isInstance(a)).findFirst();
+            if (contract.isPresent()) {
+                return  ((Jetty11Http2ClientSupplier) contract.get()).getHttpClient();
+            }
+        }
+        return null;
+    }
+}
diff --git a/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2ConnectorProvider.java b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2ConnectorProvider.java
new file mode 100644
index 0000000..a4d02d5
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/Jetty11Http2ConnectorProvider.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Configurable;
+import jakarta.ws.rs.core.Configuration;
+import org.eclipse.jetty.client.HttpClient;
+import org.glassfish.jersey.client.Initializable;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.jetty11.connector.Jetty11ConnectorProvider;
+import org.glassfish.jersey.jetty11.connector.LocalizationMessages;
+
+/**
+ * Provides HTTP2 enabled version of the {@link Jetty11ConnectorProvider} for a client
+ *
+ * @since 2.41
+ */
+public class Jetty11Http2ConnectorProvider extends Jetty11ConnectorProvider {
+    @Override
+    public Connector getConnector(Client client, Configuration runtimeConfig) {
+        return new Jetty11Http2Connector(client, runtimeConfig);
+    }
+
+    public static HttpClient getHttpClient(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 Jetty11Http2Connector) {
+            return ((Jetty11Http2Connector) connector).getHttpClient();
+        }
+
+        throw new IllegalArgumentException(LocalizationMessages.EXPECTED_CONNECTOR_PROVIDER_NOT_USED());
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/package-info.java b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/package-info.java
new file mode 100644
index 0000000..0caf270
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/main/java/org/glassfish/jersey/jetty11/http2/connector/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2023 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 HTTP2 client {@link org.glassfish.jersey.client.spi.Connector connector} based on the
+ * Jetty Client.
+ */
+package org.glassfish.jersey.jetty11.http2.connector;
diff --git a/connectors/jetty11-http2-connector/src/main/resources/org/glassfish/jersey/jetty11/http2/connector/localization.properties b/connectors/jetty11-http2-connector/src/main/resources/org/glassfish/jersey/jetty11/http2/connector/localization.properties
new file mode 100644
index 0000000..b219ef9
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/main/resources/org/glassfish/jersey/jetty11/http2/connector/localization.properties
@@ -0,0 +1,21 @@
+#
+# Copyright (c) 2023 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
+#
+
+# {0} - HTTP method, e.g. GET, DELETE
+method.not.supported=Method {0} not supported.
+invalid.configurable.component.type=The supplied component "{0}" is not assignable from Jersey11Client or JerseyWebTarget.
+expected.connector.provider.not.used=The supplied component is not configured to use a Jetty11ConnectorProvider.
+not.supported=Jetty connector is not supported on JDK version less than 11.
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AsyncTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AsyncTest.java
new file mode 100644
index 0000000..e938d43
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AsyncTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+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.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.container.AsyncResponse;
+import jakarta.ws.rs.container.Suspended;
+import jakarta.ws.rs.container.TimeoutHandler;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+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);
+            asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE)
+                    .entity("Operation time out.").build());
+
+            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.HEADERS_ONLY));
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+    }
+
+    /**
+     * 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
+        final String r1 = rf1.get().readEntity(String.class);
+        final String r2 = rf2.get().readEntity(String.class);
+        final String r3 = rf3.get().readEntity(String.class);
+
+        final long toc = System.currentTimeMillis();
+
+        assertEquals("DONE-1", r1);
+        assertEquals("DONE-2", r2);
+        assertEquals("DONE-3", r3);
+
+        assertThat("Async processing took too long.", toc - tic, Matchers.lessThan(3 * AsyncResource.OPERATION_DURATION));
+    }
+
+    /**
+     * 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));
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AuthFilterTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AuthFilterTest.java
new file mode 100644
index 0000000..2038901
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AuthFilterTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class AuthFilterTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(AuthFilterTest.class.getName());
+
+    @Override
+    protected Application configure() {
+        ResourceConfig config = new ResourceConfig(AuthTest.AuthResource.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        return config;
+    }
+
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+    }
+
+    @Test
+    public void testAuthGetWithClientFilter() {
+        client().register(HttpAuthenticationFeature.basic("name", "password"));
+        Response response = target("test/filter").request().get();
+        assertEquals("GET", response.readEntity(String.class));
+    }
+
+    @Test
+    public void testAuthPostWithClientFilter() {
+        client().register(HttpAuthenticationFeature.basic("name", "password"));
+        Response response = target("test/filter").request().post(Entity.text("POST"));
+        assertEquals("POST", response.readEntity(String.class));
+    }
+
+
+    @Test
+    public void testAuthDeleteWithClientFilter() {
+        client().register(HttpAuthenticationFeature.basic("name", "password"));
+        Response response = target("test/filter").request().delete();
+        assertEquals(204, response.getStatus());
+    }
+
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AuthTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AuthTest.java
new file mode 100644
index 0000000..e594c20
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/AuthTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+import org.eclipse.jetty.client.util.BasicAuthentication;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.jetty11.connector.Jetty11ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.inject.Singleton;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class AuthTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(AuthTest.class.getName());
+    private static final String PATH = "test";
+
+    @Path("/test")
+    @Singleton
+    public static class AuthResource {
+
+        int requestCount = 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";
+        }
+
+        @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;
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        ResourceConfig config = new ResourceConfig(AuthResource.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        return config;
+    }
+
+    @Test
+    public void testAuthGet() {
+        ClientConfig config = new ClientConfig();
+        config.property(Jetty11ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION,
+                new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password"));
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+        Client client = ClientBuilder.newClient(config);
+
+        Response response = client.target(getBaseUri()).path(PATH).request().get();
+        assertEquals("GET", response.readEntity(String.class));
+        client.close();
+    }
+
+    @Test
+    public void testAuthPost() {
+        ClientConfig config = new ClientConfig();
+        config.property(Jetty11ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION,
+                new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password"));
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+        Client client = ClientBuilder.newClient(config);
+
+        Response response = client.target(getBaseUri()).path(PATH).request().post(Entity.text("POST"));
+        assertEquals("POST", response.readEntity(String.class));
+        client.close();
+    }
+
+    @Test
+    public void testAuthDelete() {
+        ClientConfig config = new ClientConfig();
+        config.property(Jetty11ClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION,
+                new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password"));
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+        Client client = ClientBuilder.newClient(config);
+
+        Response response = client.target(getBaseUri()).path(PATH).request().delete();
+        assertEquals(response.getStatus(), 204);
+        client.close();
+    }
+
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/CookieTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/CookieTest.java
new file mode 100644
index 0000000..b5cd635
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/CookieTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.JerseyClient;
+import org.glassfish.jersey.client.JerseyClientBuilder;
+import org.glassfish.jersey.jetty11.connector.Jetty11ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.Cookie;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.NewCookie;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class CookieTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(CookieTest.class.getName());
+
+    @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() {
+        ResourceConfig config = new ResourceConfig(CookieResource.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        return config;
+    }
+
+    @Test
+    public void testCookieResource() {
+        ClientConfig config = new ClientConfig();
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+        Client client = ClientBuilder.newClient(config);
+        WebTarget r = client.target(getBaseUri());
+
+
+        assertEquals("NO-COOKIE", r.request().get(String.class));
+        assertEquals("value", r.request().get(String.class));
+        client.close();
+    }
+
+    @Test
+    public void testDisabledCookies() {
+        ClientConfig cc = new ClientConfig();
+        cc.property(Jetty11ClientProperties.DISABLE_COOKIES, true);
+        cc.connectorProvider(new Jetty11Http2ConnectorProvider());
+        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 Jetty11Http2Connector connector = (Jetty11Http2Connector) client.getConfiguration().getConnector();
+        if (connector.getCookieStore() != null) {
+            assertTrue(connector.getCookieStore().getCookies().isEmpty());
+        } else {
+            assertNull(connector.getCookieStore());
+        }
+        client.close();
+    }
+
+    @Test
+    public void testCookies() {
+        ClientConfig cc = new ClientConfig();
+        cc.connectorProvider(new Jetty11Http2ConnectorProvider());
+        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 Jetty11Http2Connector connector = (Jetty11Http2Connector) client.getConfiguration().getConnector();
+        assertNotNull(connector.getCookieStore().getCookies());
+        assertEquals(1, connector.getCookieStore().getCookies().size());
+        assertEquals("value", connector.getCookieStore().getCookies().get(0).getValue());
+        client.close();
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/CustomLoggingFilter.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/CustomLoggingFilter.java
new file mode 100644
index 0000000..45708b6
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/CustomLoggingFilter.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.client.ClientResponseFilter;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.ContainerResponseContext;
+import jakarta.ws.rs.container.ContainerResponseFilter;
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+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("bar", context.getConfiguration().getProperty("foo"));
+        preFilterCalled++;
+    }
+
+    @Override
+    public void filter(ClientRequestContext context, ClientResponseContext clientResponseContext) throws IOException {
+        System.out.println("CustomLoggingFilter.postFilter called");
+        assertEquals("bar", context.getConfiguration().getProperty("foo"));
+        postFilterCalled++;
+    }
+
+    @Override
+    public void filter(ContainerRequestContext context) throws IOException {
+        System.out.println("CustomLoggingFilter.preFilter called");
+        assertEquals("bar", context.getProperty("foo"));
+        preFilterCalled++;
+    }
+
+    @Override
+    public void filter(ContainerRequestContext context, ContainerResponseContext containerResponseContext) throws IOException {
+        System.out.println("CustomLoggingFilter.postFilter called");
+        assertEquals("bar", context.getProperty("foo"));
+        postFilterCalled++;
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/EntityTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/EntityTest.java
new file mode 100644
index 0000000..9c276e8
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/EntityTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class EntityTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(EntityTest.class.getName());
+
+    private static final String PATH = "test";
+
+    @Path("/test")
+    public static class EntityResource {
+
+        @GET
+        public Person get() {
+            return new Person("John", "Doe");
+        }
+
+        @POST
+        public Person post(Person entity) {
+            return entity;
+        }
+
+    }
+
+    @XmlRootElement
+    public static class Person {
+
+        private String firstName;
+        private String lastName;
+
+        public Person() {
+        }
+
+        public Person(String firstName, String lastName) {
+            this.firstName = firstName;
+            this.lastName = lastName;
+        }
+
+        public String getFirstName() {
+            return firstName;
+        }
+
+        public void setFirstName(String firstName) {
+            this.firstName = firstName;
+        }
+
+        public String getLastName() {
+            return lastName;
+        }
+
+        public void setLastName(String lastName) {
+            this.lastName = lastName;
+        }
+
+        @Override
+        public String toString() {
+            return firstName + " " + lastName;
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        ResourceConfig config = new ResourceConfig(EntityResource.class, JacksonFeature.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        return config;
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.connectorProvider(new Jetty11Http2ConnectorProvider())
+                .register(JacksonFeature.class);
+    }
+
+    @Test
+    public void testGet() {
+        Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).get();
+        Person person = response.readEntity(Person.class);
+        assertEquals("John Doe", person.toString());
+        response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).get();
+        person = response.readEntity(Person.class);
+        assertEquals("John Doe", person.toString());
+    }
+
+    @Test
+    public void testGetAsync() throws ExecutionException, InterruptedException {
+        Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).async().get().get();
+        Person person = response.readEntity(Person.class);
+        assertEquals("John Doe", person.toString());
+        response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).async().get().get();
+        person = response.readEntity(Person.class);
+        assertEquals("John Doe", person.toString());
+    }
+
+    @Test
+    public void testPost() {
+        Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).post(Entity.xml(new Person("John", "Doe")));
+        Person person = response.readEntity(Person.class);
+        assertEquals("John Doe", person.toString());
+        response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).post(Entity.xml(new Person("John", "Doe")));
+        person = response.readEntity(Person.class);
+        assertEquals("John Doe", person.toString());
+    }
+
+    @Test
+    public void testPostAsync() throws ExecutionException, InterruptedException, TimeoutException {
+        Response response = target(PATH).request(MediaType.APPLICATION_XML_TYPE).async()
+                .post(Entity.xml(new Person("John", "Doe"))).get();
+        Person person = response.readEntity(Person.class);
+        assertEquals("John Doe", person.toString());
+        response = target(PATH).request(MediaType.APPLICATION_JSON_TYPE).async().post(Entity.xml(new Person("John", "Doe")))
+                .get();
+        person = response.readEntity(Person.class);
+        assertEquals("John Doe", person.toString());
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/ErrorTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/ErrorTest.java
new file mode 100644
index 0000000..bc534fe
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/ErrorTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+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.jupiter.api.Test;
+
+import jakarta.ws.rs.ClientErrorException;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ErrorTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(ErrorTest.class.getName());
+
+    @Override
+    protected Application configure() {
+        ResourceConfig config = new ResourceConfig(ErrorResource.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        return config;
+    }
+
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+    }
+
+
+    @Path("/test")
+    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 = target("test");
+
+        for (int i = 0; i < 100; i++) {
+            try {
+                r.request().post(Entity.text("POST"));
+            } catch (ClientErrorException ex) {
+            }
+        }
+    }
+
+    @Test
+    public void testPostErrorWithEntity() {
+        WebTarget r = target("test");
+
+        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);
+            }
+        }
+    }
+
+    @Test
+    public void testPostErrorAsync() {
+        WebTarget r = target("test");
+
+        for (int i = 0; i < 100; i++) {
+            try {
+                r.request().async().post(Entity.text("POST"));
+            } catch (ClientErrorException ex) {
+            }
+        }
+    }
+
+    @Test
+    public void testPostErrorWithEntityAsync() {
+        WebTarget r = target("test");
+
+        for (int i = 0; i < 100; i++) {
+            try {
+                r.request().async().post(Entity.text("POST"));
+            } catch (ClientErrorException ex) {
+                String s = ex.getResponse().readEntity(String.class);
+                assertEquals("error", s);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/FollowRedirectsTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/FollowRedirectsTest.java
new file mode 100644
index 0000000..121a826
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/FollowRedirectsTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+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.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.client.ClientResponseFilter;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.UriBuilder;
+import java.io.IOException;
+import java.net.URI;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class FollowRedirectsTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(FollowRedirectsTest.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.property(ClientProperties.FOLLOW_REDIRECTS, false);
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+    }
+
+    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() {
+        final URI u = target().getUri();
+        ClientConfig config = new ClientConfig().property(ClientProperties.FOLLOW_REDIRECTS, true);
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+        Client c = ClientBuilder.newClient(config);
+        WebTarget t = c.target(u);
+        Response r = t.path("test/redirect")
+                .register(RedirectTestFilter.class)
+                .request().get();
+        assertEquals(200, r.getStatus());
+        assertEquals("GET", r.readEntity(String.class));
+        c.close();
+    }
+
+    @Test
+    public void testDoFollowPerRequestOverride() {
+        WebTarget t = target("test/redirect");
+        t.property(ClientProperties.FOLLOW_REDIRECTS, true);
+        Response r = t.request().get();
+        assertEquals(200, r.getStatus());
+        assertEquals("GET", r.readEntity(String.class));
+    }
+
+    @Test
+    public void testDontFollow() {
+        WebTarget t = target("test/redirect");
+        assertEquals(303, t.request().get().getStatus());
+    }
+
+    @Test
+    public void testDontFollowPerRequestOverride() {
+        final URI u = target().getUri();
+        ClientConfig config = new ClientConfig().property(ClientProperties.FOLLOW_REDIRECTS, true);
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+        Client client = ClientBuilder.newClient(config);
+        WebTarget t = client.target(u);
+        t.property(ClientProperties.FOLLOW_REDIRECTS, false);
+        Response r = t.path("test/redirect").request().get();
+        assertEquals(303, r.getStatus());
+        client.close();
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/GZIPContentEncodingTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/GZIPContentEncodingTest.java
new file mode 100644
index 0000000..8139643
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/GZIPContentEncodingTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+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.jupiter.api.Test;
+
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.util.Arrays;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class GZIPContentEncodingTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(EntityTest.class.getName());
+
+    @Path("/")
+    public static class Resource {
+
+        @POST
+        public byte[] post(byte[] content) {
+            return content;
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        ResourceConfig config = new ResourceConfig(Resource.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        return config;
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.register(GZipEncoder.class);
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+    }
+
+    @Test
+    public void testPost() {
+        WebTarget r = target();
+        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 config = new ClientConfig();
+        config.property(ClientProperties.CHUNKED_ENCODING_SIZE, 1024);
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+
+        Client client = ClientBuilder.newClient(config);
+        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();
+
+        client.close();
+    }
+
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/HelloWorldTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/HelloWorldTest.java
new file mode 100644
index 0000000..0f674ac
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/HelloWorldTest.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+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.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.InvocationCallback;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+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;
+        }
+
+    }
+
+    @Override
+    protected Application configure() {
+        ResourceConfig config = new ResourceConfig(HelloWorldResource.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        return config;
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+    }
+
+    @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 testAsyncClientRequests() throws InterruptedException {
+        final int REQUESTS = 20;
+        final CountDownLatch latch = new CountDownLatch(REQUESTS);
+        final long tic = System.currentTimeMillis();
+        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);
+                        assertEquals(HelloWorldResource.CLICHED_MESSAGE, result);
+                    } finally {
+                        latch.countDown();
+                    }
+                }
+
+                @Override
+                public void failed(Throwable error) {
+                    error.printStackTrace();
+                    latch.countDown();
+                }
+            });
+        }
+        latch.await(10 * getAsyncTimeoutMultiplier(), TimeUnit.SECONDS);
+        final long toc = System.currentTimeMillis();
+        Logger.getLogger(HelloWorldTest.class.getName()).info("Executed in: " + (toc - tic));
+    }
+
+    @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);
+        client.close();
+    }
+
+    @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);
+        client.close();
+    }
+
+    @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 = target().path(ROOT_PATH).request().get(String.class);
+        assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+        assertEquals(1, CustomLoggingFilter.preFilterCalled);
+        assertEquals(1, CustomLoggingFilter.postFilterCalled);
+        client.close();
+    }
+
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/Http2PresenceTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/Http2PresenceTest.java
new file mode 100644
index 0000000..df7da3f
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/Http2PresenceTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.spi.ConnectorProvider;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HeaderParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/**
+ * Tests the HTTP2 presence.
+ *
+ */
+public class Http2PresenceTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(Http2PresenceTest.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);
+            return "POST";
+        }
+
+        @GET
+        public String testUserAgent(@Context HttpHeaders httpHeaders) {
+            final List<String> requestHeader = httpHeaders.getRequestHeader(HttpHeaders.USER_AGENT);
+            if (requestHeader.size() != 1) {
+                return "FAIL";
+            }
+            return requestHeader.get(0);
+        }
+    }
+
+    @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 Jetty11Http2ConnectorProvider());
+    }
+
+    @Test
+    public void testPost() {
+        Response response = target().path("test").request().header("X-CLIENT", "client").post(null);
+
+        assertEquals(200, response.getStatus());
+        assertTrue(response.hasEntity());
+    }
+
+    @Test
+    public void testHttp2Presence() {
+        final ConnectorProvider provider = ((ClientConfig) target().getConfiguration()).getConnectorProvider();
+        assertTrue(provider instanceof Jetty11Http2ConnectorProvider);
+
+        final HttpClient client = ((Jetty11Http2ConnectorProvider) provider).getHttpClient(target());
+        assertTrue(client.getTransport() instanceof HttpClientTransportOverHTTP2);
+    }
+
+    /**
+     * Test, that {@code User-agent} header is as set by Jersey, not by underlying Jetty client.
+     */
+    @Test
+    public void testUserAgent() {
+        String response = target().path("test").request().get(String.class);
+        assertTrue(response.startsWith("Jersey"), "User-agent header should start with 'Jersey', but was " + response);
+    }
+}
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/HttpHeadersTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/HttpHeadersTest.java
new file mode 100644
index 0000000..9cdd81e
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/HttpHeadersTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+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.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HeaderParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+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);
+            return "POST";
+        }
+
+        @GET
+        public String testUserAgent(@Context HttpHeaders httpHeaders) {
+            final List<String> requestHeader = httpHeaders.getRequestHeader(HttpHeaders.USER_AGENT);
+            if (requestHeader.size() != 1) {
+                return "FAIL";
+            }
+            return requestHeader.get(0);
+        }
+    }
+
+    @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 Jetty11Http2ConnectorProvider());
+    }
+
+    @Test
+    public void testPost() {
+        Response response = target().path("test").request().header("X-CLIENT", "client").post(null);
+
+        assertEquals(200, response.getStatus());
+        assertTrue(response.hasEntity());
+    }
+
+    /**
+     * Test, that {@code User-agent} header is as set by Jersey, not by underlying Jetty client.
+     */
+    @Test
+    public void testUserAgent() {
+        String response = target().path("test").request().get(String.class);
+        assertTrue(response.startsWith("Jersey"), "User-agent header should start with 'Jersey', but was " + response);
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/ManagedClientTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/ManagedClientTest.java
new file mode 100644
index 0000000..e5ba7d3
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/ManagedClientTest.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+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.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.DynamicFeature;
+import jakarta.ws.rs.container.ResourceInfo;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.FeatureContext;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+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 static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ManagedClientTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(ManagedClientTest.class.getName());
+
+    /**
+     * Managed client configuration for client A.
+     */
+    @ClientBinding(configClass = MyClientAConfig.class)
+    @Documented
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.FIELD, ElementType.PARAMETER})
+    public static @interface ClientA {
+    }
+
+    /**
+     * Managed client configuration for client B.
+     */
+    @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.
+     */
+    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>
+     */
+    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.
+     */
+    @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>
+     */
+    @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 Jetty11Http2ConnectorProvider());
+    }
+
+    /**
+     * 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));
+    }
+
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/MethodTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/MethodTest.java
new file mode 100644
index 0000000..2dabed5
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/MethodTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+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.jupiter.api.Test;
+
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.PATCH;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Logger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class MethodTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(MethodTest.class.getName());
+
+    private static final String PATH = "test";
+
+    @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;
+        }
+
+        @PATCH
+        public String patch(String entity) {
+            return entity;
+        }
+
+        @DELETE
+        public String delete() {
+            return "DELETE";
+        }
+    }
+
+    @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 Jetty11Http2ConnectorProvider());
+    }
+
+    @Test
+    public void testGet() {
+        Response response = target(PATH).request().get();
+        assertEquals("GET", response.readEntity(String.class));
+    }
+
+    @Test
+    public void testGetAsync() throws ExecutionException, InterruptedException {
+        Response response = target(PATH).request().async().get().get();
+        assertEquals("GET", response.readEntity(String.class));
+    }
+
+    @Test
+    public void testPost() {
+        Response response = target(PATH).request().post(Entity.entity("POST", MediaType.TEXT_PLAIN));
+        assertEquals("POST", response.readEntity(String.class));
+    }
+
+    @Test
+    public void testPostAsync() throws ExecutionException, InterruptedException {
+        Response response = target(PATH).request().async().post(Entity.entity("POST", MediaType.TEXT_PLAIN)).get();
+        assertEquals("POST", response.readEntity(String.class));
+    }
+
+    @Test
+    public void testPut() {
+        Response response = target(PATH).request().put(Entity.entity("PUT", MediaType.TEXT_PLAIN));
+        assertEquals("PUT", response.readEntity(String.class));
+    }
+
+    @Test
+    public void testPutAsync() throws ExecutionException, InterruptedException {
+        Response response = target(PATH).request().async().put(Entity.entity("PUT", MediaType.TEXT_PLAIN)).get();
+        assertEquals("PUT", response.readEntity(String.class));
+    }
+
+    @Test
+    public void testDelete() {
+        Response response = target(PATH).request().delete();
+        assertEquals("DELETE", response.readEntity(String.class));
+    }
+
+    @Test
+    public void testDeleteAsync() throws ExecutionException, InterruptedException {
+        Response response = target(PATH).request().async().delete().get();
+        assertEquals("DELETE", response.readEntity(String.class));
+    }
+
+    @Test
+    public void testPatch() {
+        Response response = target(PATH).request().method("PATCH", Entity.entity("PATCH", MediaType.TEXT_PLAIN));
+        assertEquals("PATCH", response.readEntity(String.class));
+    }
+
+    @Test
+    public void testOptionsWithEntity() {
+        Response response = target(PATH).request().build("OPTIONS", Entity.text("OPTIONS")).invoke();
+        assertEquals(200, response.getStatus());
+        response.close();
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/NoEntityTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/NoEntityTest.java
new file mode 100644
index 0000000..3414f2c
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/NoEntityTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+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.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.util.logging.Logger;
+
+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(Response.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 Jetty11Http2ConnectorProvider());
+    }
+
+    @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();
+        }
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/SyncResponseSizeTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/SyncResponseSizeTest.java
new file mode 100644
index 0000000..159738e
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/SyncResponseSizeTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.jetty11.connector.Jetty11ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.net.URI;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class SyncResponseSizeTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(SyncResponseSizeTest.class.getName());
+
+    private static final int maxBufferSize = 4 * 1024 * 1024; //4 MiB
+
+    @Path("/test")
+    public static class TimeoutResource {
+
+        private static final byte[] data = new byte[maxBufferSize];
+
+        static {
+            Byte b = "a".getBytes()[0];
+            for (int i = 0; i < maxBufferSize; i++) data[i] = b.byteValue();
+        }
+
+        @GET
+        @Path("/small")
+        public String getSmall() {
+            return "GET";
+        }
+
+        @GET
+        @Path("/big")
+        public String getBig() {
+            return new String(data);
+        }
+
+        @GET
+        @Path("/verybig")
+        public String getVeryBig() {
+            return new String(data) + "a";
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        ResourceConfig config = new ResourceConfig(TimeoutResource.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        return config;
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+    }
+
+    @Test
+    public void testDefaultSmall() {
+        Response r = target("test/small").request().get();
+        assertEquals(200, r.getStatus());
+        assertEquals("GET", r.readEntity(String.class));
+    }
+
+    @Test
+    public void testDefaultTooBig() {
+        final URI u = target().getUri();
+        ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+
+        Client c = ClientBuilder.newClient(config);
+        WebTarget t = c.target(u);
+        try {
+            t.path("test/big").request().get();
+            fail("Exception expected.");
+        } catch (ProcessingException e) {
+            // Buffering capacity ... exceeded.
+            assertTrue(ExecutionException.class.isInstance(e.getCause()));
+            assertTrue(IllegalArgumentException.class.isInstance(e.getCause().getCause()));
+        } finally {
+            c.close();
+        }
+    }
+
+    @Test
+    public void testCustomBig() {
+        final URI u = target().getUri();
+        ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+        config.property(Jetty11ClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE, maxBufferSize);
+
+        Client c = ClientBuilder.newClient(config);
+        WebTarget t = c.target(u);
+        try {
+            Response r = t.path("test/big").request().get();
+            String p = r.readEntity(String.class);
+            assertEquals(p.length(), maxBufferSize);
+        } catch (ProcessingException e) {
+            assertThat("Unexpected processing exception cause",
+                    e.getCause(), instanceOf(TimeoutException.class));
+        } finally {
+            c.close();
+        }
+    }
+
+    @Test
+    public void testCustomTooBig() {
+        final URI u = target().getUri();
+        ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+        config.property(Jetty11ClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE, maxBufferSize);
+
+        Client c = ClientBuilder.newClient(config);
+        WebTarget t = c.target(u);
+        try {
+            t.path("test/verybig").request().get();
+            fail("Exception expected.");
+        } catch (ProcessingException e) {
+            // Buffering capacity ... exceeded.
+            assertTrue(ExecutionException.class.isInstance(e.getCause()));
+            assertTrue(IllegalArgumentException.class.isInstance(e.getCause().getCause()));
+        } finally {
+            c.close();
+        }
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/TimeoutTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/TimeoutTest.java
new file mode 100644
index 0000000..e3745ef
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/TimeoutTest.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+import org.glassfish.jersey.CommonProperties;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.jetty11.connector.Jetty11ClientProperties;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.StreamingOutput;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+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 (InterruptedException e) {
+                e.printStackTrace();
+            }
+            return "GET";
+        }
+
+        /**
+         * Long-running streaming request
+         *
+         * @param count number of packets send
+         * @param pauseMillis pause between each packets
+         */
+        @GET
+        @Path("stream")
+        public Response streamsWithDelay(@QueryParam("start") @DefaultValue("0") int startMillis, @QueryParam("count") int count,
+                                         @QueryParam("pauseMillis") int pauseMillis) {
+            StreamingOutput streamingOutput = streamSlowly(startMillis, count, pauseMillis);
+
+            return Response.ok(streamingOutput)
+                    .build();
+        }
+    }
+
+    private static StreamingOutput streamSlowly(int startMillis, int count, int pauseMillis) {
+
+        return output -> {
+            try {
+                TimeUnit.MILLISECONDS.sleep(startMillis);
+            }
+            catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+            output.write("begin\n".getBytes(StandardCharsets.UTF_8));
+            output.flush();
+            for (int i = 0; i < count; i++) {
+                try {
+                    TimeUnit.MILLISECONDS.sleep(pauseMillis);
+                }
+                catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+
+                output.write(("message " + i + "\n").getBytes(StandardCharsets.UTF_8));
+                output.flush();
+            }
+            output.write("end".getBytes(StandardCharsets.UTF_8));
+        };
+    }
+
+    @Override
+    protected Application configure() {
+        ResourceConfig config = new ResourceConfig(TimeoutResource.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        return config;
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+    }
+
+    @Test
+    public void testFast() {
+        Response r = target("test").request().get();
+        assertEquals(200, r.getStatus());
+        assertEquals("GET", r.readEntity(String.class));
+    }
+
+    @Test
+    public void testSlow() {
+        final URI u = target().getUri();
+        ClientConfig config = new ClientConfig().property(ClientProperties.READ_TIMEOUT, 1_000);
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+        Client c = ClientBuilder.newClient(config);
+        WebTarget t = c.target(u);
+        try {
+            t.path("test/timeout").request().get();
+            fail("Timeout expected.");
+        } catch (ProcessingException e) {
+            assertThat("Unexpected processing exception cause",
+                    e.getCause(), instanceOf(TimeoutException.class));
+        } finally {
+            c.close();
+        }
+    }
+
+    @Test
+    public void testTimeoutInRequest() {
+        final URI u = target().getUri();
+        ClientConfig config = new ClientConfig();
+        config.connectorProvider(new Jetty11Http2ConnectorProvider());
+        Client c = ClientBuilder.newClient(config);
+        WebTarget t = c.target(u);
+        try {
+            t.path("test/timeout").request().property(ClientProperties.READ_TIMEOUT, 1_000).get();
+            fail("Timeout expected.");
+        } catch (ProcessingException e) {
+            assertThat("Unexpected processing exception cause",
+                    e.getCause(), instanceOf(TimeoutException.class));
+        } finally {
+            c.close();
+        }
+    }
+
+    /**
+     * Test accessing an operation that is streaming slowly
+     *
+     * @throws ProcessingException in case of a test error.
+     */
+    @Test
+    public void testSlowlyStreamedContentDoesNotReadTimeout() throws Exception {
+
+        int count = 5;
+        int pauseMillis = 50;
+
+        final Response response = target("test")
+                .property(ClientProperties.READ_TIMEOUT, 100L)
+                .property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1")
+                .path("stream")
+                .queryParam("count", count)
+                .queryParam("pauseMillis", pauseMillis)
+                .request().get();
+
+        assertTrue(response.readEntity(String.class).contains("end"));
+    }
+
+    @Test
+    public void testSlowlyStreamedContentDoesTotalTimeout() throws Exception {
+
+        int count = 5;
+        int pauseMillis = 50;
+
+        try {
+            target("test")
+                    .property(Jetty11ClientProperties.TOTAL_TIMEOUT, 100L)
+                    .property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1")
+                    .path("stream")
+                    .queryParam("count", count)
+                    .queryParam("pauseMillis", pauseMillis)
+                    .request().get();
+
+            fail("This operation should trigger total timeout");
+        } catch (ProcessingException e) {
+            assertEquals(TimeoutException.class, e.getCause().getClass());
+        }
+    }
+
+    /**
+     * Test accessing an operation that is streaming slowly
+     *
+     * @throws ProcessingException in case of a test error.
+     */
+    @Test
+    public void testSlowToStartStreamedContentDoesReadTimeout() throws Exception {
+
+        int start = 150;
+        int count = 5;
+        int pauseMillis = 50;
+
+        try {
+            target("test")
+                    .property(ClientProperties.READ_TIMEOUT, 100L)
+                    .property(CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER_SERVER, "-1")
+                    .path("stream")
+                    .queryParam("start", start)
+                    .queryParam("count", count)
+                    .queryParam("pauseMillis", pauseMillis)
+                    .request().get();
+            fail("This operation should trigger idle timeout");
+        } catch (ProcessingException e) {
+            assertEquals(TimeoutException.class, e.getCause().getClass());
+        }
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/TraceSupportTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/TraceSupportTest.java
new file mode 100644
index 0000000..815167e
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/TraceSupportTest.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+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.jupiter.api.Test;
+
+import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Request;
+import jakarta.ws.rs.core.Response;
+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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+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(// toLowerCase - http header field names are case insensitive
+                    responseEntity.contains(expectedFragment),
+                    "Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity);
+        }
+    }
+
+    @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(// toLowerCase - http header field names are case insensitive
+                    responseEntity.contains(expectedFragment),
+                    "Expected fragment '" + expectedFragment + "' not found in response:\n" + responseEntity);
+        }
+    }
+
+    @Test
+    public void testTraceWithEntity() throws Exception {
+        _testTraceWithEntity(false, false);
+    }
+
+    @Test
+    public void testAsyncTraceWithEntity() throws Exception {
+        _testTraceWithEntity(true, false);
+    }
+
+    @Test
+    public void testTraceWithEntityJettyConnector() throws Exception {
+        _testTraceWithEntity(false, true);
+    }
+
+    @Test
+    public void testAsyncTraceWithEntityJettyConnector() throws Exception {
+        _testTraceWithEntity(true, true);
+    }
+
+    private void _testTraceWithEntity(final boolean isAsync, final boolean useJettyConnection) throws Exception {
+        try {
+            WebTarget target = useJettyConnection ? getJettyClient().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 getJettyClient() {
+        return ClientBuilder.newClient(new ClientConfig().connectorProvider(new Jetty11Http2ConnectorProvider()));
+    }
+
+
+    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");
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/UnderlyingHttpClientAccessTest.java b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/UnderlyingHttpClientAccessTest.java
new file mode 100644
index 0000000..b56b536
--- /dev/null
+++ b/connectors/jetty11-http2-connector/src/test/java/org/glassfish/jersey/jetty11/http2/connector/UnderlyingHttpClientAccessTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2023 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.jetty11.http2.connector;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.glassfish.jersey.client.ClientConfig;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+public class UnderlyingHttpClientAccessTest {
+
+    /**
+     * Verifier of JERSEY-2424 fix.
+     */
+    @Test
+    public void testHttpClientInstanceAccess() {
+        final Client client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new Jetty11Http2ConnectorProvider()));
+        final HttpClient hcOnClient = Jetty11Http2ConnectorProvider.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 = Jetty11Http2ConnectorProvider.getHttpClient(target);
+
+        assertNotNull(hcOnClient, "HTTP client instance set on JerseyClient should not be null.");
+        assertNotNull(hcOnTarget, "HTTP client instance set on JerseyWebTarget should not be null.");
+        assertSame(hcOnClient, hcOnTarget, "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).");
+    }
+
+    @Test
+    public void testGetProvidedClientInstance() {
+        final HttpClient httpClient = new HttpClient();
+        final ClientConfig clientConfig = new ClientConfig()
+                .connectorProvider(new Jetty11Http2ConnectorProvider())
+                .register(new Jetty11Http2ClientSupplier(httpClient));
+        final Client client = ClientBuilder.newClient(clientConfig);
+        final WebTarget target = client.target("http://localhost/");
+        final HttpClient hcOnTarget = Jetty11Http2ConnectorProvider.getHttpClient(target);
+
+        assertThat("Instance provided to a ClientConfig differs from instance provided by JettyProvider",
+                httpClient, is(hcOnTarget));
+    }
+}
\ No newline at end of file
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java
new file mode 100644
index 0000000..471321f
--- /dev/null
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/Expect100ContinueConnectorExtension.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2023 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.netty.connector;
+
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpVersion;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.RequestEntityProcessing;
+import org.glassfish.jersey.client.internal.ConnectorExtension;
+
+import java.io.IOException;
+import java.net.ProtocolException;
+
+class Expect100ContinueConnectorExtension
+        implements ConnectorExtension<HttpRequest, IOException> {
+    private static final String EXCEPTION_MESSAGE = "Server rejected operation";
+    @Override
+    public void invoke(ClientRequest request, HttpRequest extensionParam) {
+
+        final long length = request.getLengthLong();
+        final RequestEntityProcessing entityProcessing = request.resolveProperty(
+                ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.class);
+
+        final Boolean expectContinueActivated = request.resolveProperty(
+                ClientProperties.EXPECT_100_CONTINUE, Boolean.class);
+        final Long expectContinueSizeThreshold = request.resolveProperty(
+                ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE,
+                ClientProperties.DEFAULT_EXPECT_100_CONTINUE_THRESHOLD_SIZE);
+
+        final boolean allowStreaming = length > expectContinueSizeThreshold
+                || entityProcessing == RequestEntityProcessing.CHUNKED;
+
+        if (extensionParam.protocolVersion().equals(HttpVersion.HTTP_1_0)
+                || !Boolean.TRUE.equals(expectContinueActivated)
+                || !request.hasEntity()
+                || !allowStreaming) {
+            return;
+        }
+        extensionParam.headers().add(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE);
+
+    }
+
+    @Override
+    public void postConnectionProcessing(HttpRequest extensionParam) {
+    }
+
+    @Override
+    public boolean handleException(ClientRequest request, HttpRequest extensionParam, IOException ex) {
+        final Boolean expectContinueActivated = request.resolveProperty(
+                ClientProperties.EXPECT_100_CONTINUE, Boolean.FALSE);
+
+        return expectContinueActivated
+                && (ex instanceof ProtocolException && ex.getMessage().equals(EXCEPTION_MESSAGE));
+    }
+}
\ No newline at end of file
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java
index d460867..4a9836d 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java
@@ -20,17 +20,24 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
 
-import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.core.MultivaluedMap;
 import jakarta.ws.rs.core.Response;
 
 import org.glassfish.jersey.client.ClientProperties;
 import org.glassfish.jersey.client.ClientRequest;
 import org.glassfish.jersey.client.ClientResponse;
+import org.glassfish.jersey.http.HttpHeaders;
+import org.glassfish.jersey.http.ResponseStatus;
 import org.glassfish.jersey.netty.connector.internal.NettyInputStream;
 import org.glassfish.jersey.netty.connector.internal.RedirectException;
 
@@ -38,13 +45,12 @@
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.SimpleChannelInboundHandler;
 import io.netty.handler.codec.http.HttpContent;
-import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpObject;
 import io.netty.handler.codec.http.HttpResponse;
-import io.netty.handler.codec.http.HttpResponseStatus;
 import io.netty.handler.codec.http.HttpUtil;
 import io.netty.handler.codec.http.LastHttpContent;
 import io.netty.handler.timeout.IdleStateEvent;
+import org.glassfish.jersey.uri.internal.JerseyUriBuilder;
 
 /**
  * Jersey implementation of Netty channel handler.
@@ -103,17 +109,27 @@
           jerseyResponse = null;
           int responseStatus = cr.getStatus();
           if (followRedirects
-                  && (responseStatus == HttpResponseStatus.MOVED_PERMANENTLY.code()
-                          || responseStatus == HttpResponseStatus.FOUND.code()
-                          || responseStatus == HttpResponseStatus.SEE_OTHER.code()
-                          || responseStatus == HttpResponseStatus.TEMPORARY_REDIRECT.code()
-                          || responseStatus == HttpResponseStatus.PERMANENT_REDIRECT.code())) {
+                  && (responseStatus == ResponseStatus.Redirect3xx.MOVED_PERMANENTLY_301.getStatusCode()
+                          || responseStatus == ResponseStatus.Redirect3xx.FOUND_302.getStatusCode()
+                          || responseStatus == ResponseStatus.Redirect3xx.SEE_OTHER_303.getStatusCode()
+                          || responseStatus == ResponseStatus.Redirect3xx.TEMPORARY_REDIRECT_307.getStatusCode()
+                          || responseStatus == ResponseStatus.Redirect3xx.PERMANENT_REDIRECT_308.getStatusCode())) {
               String location = cr.getHeaderString(HttpHeaders.LOCATION);
               if (location == null || location.isEmpty()) {
                   responseAvailable.completeExceptionally(new RedirectException(LocalizationMessages.REDIRECT_NO_LOCATION()));
               } else {
                   try {
                       URI newUri = URI.create(location);
+                      if (!newUri.isAbsolute()) {
+                          final URI originalUri = jerseyRequest.getUri();
+                          newUri = new JerseyUriBuilder()
+                                  .scheme(originalUri.getScheme())
+                                  .userInfo(originalUri.getUserInfo())
+                                  .host(originalUri.getHost())
+                                  .port(originalUri.getPort())
+                                  .uri(location)
+                                  .build();
+                      }
                       boolean alreadyRequested = !redirectUriHistory.add(newUri);
                       if (alreadyRequested) {
                           // infinite loop detection
@@ -126,6 +142,7 @@
                       } else {
                           ClientRequest newReq = new ClientRequest(jerseyRequest);
                           newReq.setUri(newUri);
+                          restrictRedirectRequest(newReq, cr);
                           connector.execute(newReq, redirectUriHistory, responseAvailable);
                       }
                   } catch (IllegalArgumentException e) {
@@ -165,7 +182,7 @@
             }
 
             // request entity handling.
-            if ((response.headers().contains(HttpHeaderNames.CONTENT_LENGTH) && HttpUtil.getContentLength(response) > 0)
+            if ((response.headers().contains(HttpHeaders.CONTENT_LENGTH) && HttpUtil.getContentLength(response) > 0)
                     || HttpUtil.isTransferEncodingChunked(response)) {
 
                 nis = new NettyInputStream();
@@ -218,4 +235,63 @@
            super.userEventTriggered(ctx, evt);
        }
     }
+
+    /*
+     * RFC 9110 Section 15.4
+     * https://httpwg.org/specs/rfc9110.html#rfc.section.15.4
+     */
+    private void restrictRedirectRequest(ClientRequest newRequest, ClientResponse response) {
+        final MultivaluedMap<String, Object> headers = newRequest.getHeaders();
+        final Boolean keepMethod = newRequest.resolveProperty(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, Boolean.TRUE);
+
+        if (Boolean.FALSE.equals(keepMethod) && newRequest.getMethod().equals(HttpMethod.POST)) {
+            switch (response.getStatus()) {
+                case 301 /* MOVED PERMANENTLY */:
+                case 302 /* FOUND */:
+                    removeContentHeaders(headers);
+                    newRequest.setMethod(HttpMethod.GET);
+                    newRequest.setEntity(null);
+                    break;
+            }
+        }
+
+        for (final Iterator<Map.Entry<String, List<Object>>> it = headers.entrySet().iterator(); it.hasNext(); ) {
+            final Map.Entry<String, List<Object>> entry = it.next();
+            if (ProxyHeaders.INSTANCE.test(entry.getKey())) {
+                it.remove();
+            }
+        }
+
+        headers.remove(HttpHeaders.IF_MATCH);
+        headers.remove(HttpHeaders.IF_NONE_MATCH);
+        headers.remove(HttpHeaders.IF_MODIFIED_SINCE);
+        headers.remove(HttpHeaders.IF_UNMODIFIED_SINCE);
+        headers.remove(HttpHeaders.AUTHORIZATION);
+        headers.remove(HttpHeaders.REFERER);
+        headers.remove(HttpHeaders.COOKIE);
+    }
+
+    private void removeContentHeaders(MultivaluedMap<String, Object> headers) {
+        for (final Iterator<Map.Entry<String, List<Object>>> it = headers.entrySet().iterator(); it.hasNext(); ) {
+            final Map.Entry<String, List<Object>> entry = it.next();
+            final String lowName = entry.getKey().toLowerCase(Locale.ROOT);
+            if (lowName.startsWith("content-")) {
+                it.remove();
+            }
+        }
+        headers.remove(HttpHeaders.LAST_MODIFIED);
+        headers.remove(HttpHeaders.TRANSFER_ENCODING);
+    }
+
+    /* package */ static class ProxyHeaders implements Predicate<String> {
+        static final ProxyHeaders INSTANCE = new ProxyHeaders();
+        private static final String HOST = HttpHeaders.HOST.toLowerCase(Locale.ROOT);
+        private static final String FORWARDED = HttpHeaders.FORWARDED.toLowerCase(Locale.ROOT);
+
+        @Override
+        public boolean test(String headerName) {
+            String lowName = headerName.toLowerCase(Locale.ROOT);
+            return lowName.startsWith("proxy-") || lowName.equals(HOST) || lowName.equals(FORWARDED);
+        }
+    }
 }
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java
new file mode 100644
index 0000000..8bceac6
--- /dev/null
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyExpectContinueHandler.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2023 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.netty.connector;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.http.DefaultFullHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpResponse;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpUtil;
+import org.glassfish.jersey.client.ClientRequest;
+
+import jakarta.ws.rs.ProcessingException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class JerseyExpectContinueHandler extends ChannelInboundHandlerAdapter {
+
+    private boolean isExpected;
+
+    private static final List<HttpResponseStatus> statusesToBeConsidered = Arrays.asList(HttpResponseStatus.CONTINUE,
+            HttpResponseStatus.UNAUTHORIZED, HttpResponseStatus.EXPECTATION_FAILED,
+            HttpResponseStatus.METHOD_NOT_ALLOWED, HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE);
+
+    private CompletableFuture<HttpResponseStatus> expectedFuture = new CompletableFuture<>();
+
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+        if (isExpected && msg instanceof HttpResponse) {
+            final HttpResponse response = (HttpResponse) msg;
+            if (statusesToBeConsidered.contains(response.status())) {
+                expectedFuture.complete(response.status());
+            }
+            if (!HttpResponseStatus.CONTINUE.equals(response.status())) {
+                ctx.fireChannelRead(msg); //bypass the message to the next handler in line
+            } else {
+                ctx.pipeline().remove(JerseyExpectContinueHandler.class);
+            }
+        } else {
+            if (!isExpected) {
+                ctx.pipeline().remove(JerseyExpectContinueHandler.class);
+            }
+            ctx.fireChannelRead(msg); //bypass the message to the next handler in line
+        }
+    }
+
+    CompletableFuture<HttpResponseStatus> processExpect100ContinueRequest(HttpRequest nettyRequest,
+                                                                          ClientRequest jerseyRequest,
+                                                                          Channel ch,
+                                                                          Integer timeout)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        //check for 100-Continue presence/availability
+        final Expect100ContinueConnectorExtension expect100ContinueExtension
+                = new Expect100ContinueConnectorExtension();
+
+        final DefaultFullHttpRequest nettyRequestHeaders =
+                new DefaultFullHttpRequest(nettyRequest.protocolVersion(), nettyRequest.method(), nettyRequest.uri());
+        nettyRequestHeaders.headers().setAll(nettyRequest.headers());
+
+        if (!nettyRequestHeaders.headers().contains(HttpHeaderNames.HOST)) {
+            nettyRequestHeaders.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost());
+        }
+
+        //If Expect:100-continue feature is enabled and client supports it, the nettyRequestHeaders will be
+        //enriched with the 'Expect:100-continue' header.
+        expect100ContinueExtension.invoke(jerseyRequest, nettyRequestHeaders);
+
+        final ChannelFuture expect100ContinueFuture = (HttpUtil.is100ContinueExpected(nettyRequestHeaders))
+                // Send only head of the HTTP request enriched with Expect:100-continue header.
+                ? ch.writeAndFlush(nettyRequestHeaders)
+                // Expect:100-Continue either is not supported or is turned off
+                : null;
+        isExpected = expect100ContinueFuture != null;
+        if (!isExpected) {
+            ch.pipeline().remove(JerseyExpectContinueHandler.class);
+        } else {
+            final HttpResponseStatus status = expectedFuture
+                    .get(timeout, TimeUnit.MILLISECONDS);
+
+            processExpectationStatus(status);
+        }
+        return expectedFuture;
+    }
+
+    private void processExpectationStatus(HttpResponseStatus status)
+            throws TimeoutException {
+        if (!statusesToBeConsidered.contains(status)) {
+            throw new ProcessingException(LocalizationMessages
+                    .UNEXPECTED_VALUE_FOR_EXPECT_100_CONTINUE_STATUSES(status.code()), null);
+        }
+        if (!expectedFuture.isDone() || HttpResponseStatus.EXPECTATION_FAILED.equals(status)) {
+            isExpected = false;
+            throw new TimeoutException(); // continue without expectations
+        }
+        if (!HttpResponseStatus.CONTINUE.equals(status)) {
+            throw new ProcessingException(LocalizationMessages
+                    .UNEXPECTED_VALUE_FOR_EXPECT_100_CONTINUE_STATUSES(status.code()), null);
+        }
+    }
+
+    boolean isExpected() {
+        return isExpected;
+    }
+}
\ No newline at end of file
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java
index 671b08f..9c79d12 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyClientProperties.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2023 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
@@ -28,11 +28,32 @@
 
     /**
      * <p>
-     *    This property determines the maximum number of idle connections that will be simultaneously kept alive
-     *    in total, rather than per destination. The default is 60. Specify 0 to disable.
+     *     Sets the endpoint identification algorithm to HTTPS.
      * </p>
+     * <p>
+     *     The default value is {@code true} (for HTTPS uri scheme).
+     * </p>
+     * <p>
+     *     The name of the configuration property is <tt>{@value}</tt>.
+     * </p>
+     * @since 2.35
+     * @see javax.net.ssl.SSLParameters#setEndpointIdentificationAlgorithm(String)
      */
-    public static final String MAX_CONNECTIONS_TOTAL = "jersey.config.client.maxTotalConnections";
+    public static final String ENABLE_SSL_HOSTNAME_VERIFICATION = "jersey.config.client.tls.enableHostnameVerification";
+
+    /**
+     * <p>
+     *     Filter the HTTP headers for requests (CONNECT) towards the proxy except for PROXY-prefixed and HOST headers when {@code true}.
+     * </p>
+     * <p>
+     *     The default value is {@code true} and the headers are filtered out.
+     * </p>
+     * <p>
+     *     The name of the configuration property is <tt>{@value}</tt>.
+     * </p>
+     * @since 2.41
+     */
+    public static final String FILTER_HEADERS_FOR_PROXY = "jersey.config.client.filter.headers.proxy";
 
     /**
      * <p>
@@ -56,23 +77,16 @@
 
     /**
      * <p>
-     *     Sets the endpoint identification algorithm to HTTPS.
+     *    This property determines the maximum number of idle connections that will be simultaneously kept alive
+     *    in total, rather than per destination. The default is 60. Specify 0 to disable.
      * </p>
-     * <p>
-     *     The default value is {@code true} (for HTTPS uri scheme).
-     * </p>
-     * <p>
-     *     The name of the configuration property is <tt>{@value}</tt>.
-     * </p>
-     * @since 2.35
-     * @see javax.net.ssl.SSLParameters#setEndpointIdentificationAlgorithm(String)
      */
-    public static final String ENABLE_SSL_HOSTNAME_VERIFICATION = "jersey.config.client.tls.enableHostnameVerification";
+    public static final String MAX_CONNECTIONS_TOTAL = "jersey.config.client.maxTotalConnections";
 
     /**
      * The maximal number of redirects during single request.
      * <p/>
-     * Value is expected to be positive {@link Integer}. Default value is {@value #DEFAULT_MAX_REDIRECTS}.
+     * Value is expected to be positive {@link Integer}. Default value is 5.
      * <p/>
      * HTTP redirection must be enabled by property {@link org.glassfish.jersey.client.ClientProperties#FOLLOW_REDIRECTS},
      * otherwise {@code MAX_REDIRECTS} is not applied.
@@ -82,4 +96,37 @@
      * @see org.glassfish.jersey.netty.connector.internal.RedirectException
      */
     public static final String MAX_REDIRECTS = "jersey.config.client.NettyConnectorProvider.maxRedirects";
+
+    /**
+     * <p>
+     *     Sets the HTTP POST method to be preserved on HTTP status 301 (MOVED PERMANENTLY) or status 302 (FOUND) when {@code true}
+     *     or redirected as GET when {@code false}.
+     * </p>
+     * <p>
+     *     The default value is {@code true} and the HTTP POST request is not redirected as GET.
+     * </p>
+     * <p>
+     *     The name of the configuration property is <tt>{@value}</tt>.
+     * </p>
+     * @since 2.41
+     */
+    public static final String PRESERVE_METHOD_ON_REDIRECT = "jersey.config.client.redirect.preserve.method";
+
+
+    /**
+     * This timeout is used for waiting for 100-Continue response when 100-Continue is sent by the client.
+     *
+     * @since 2.41
+     */
+    public static final String
+            EXPECT_100_CONTINUE_TIMEOUT = "jersey.config.client.request.expect.100.continue.timeout";
+
+    /**
+     * The default value of EXPECT_100_CONTINUE_TIMEOUT.
+     *
+     * @since 2.41
+     */
+    public static final Integer
+            DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE = 500;
+
 }
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
index 4b4f16f..ede8c36 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnector.java
@@ -31,13 +31,16 @@
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
 
-import javax.net.ssl.SSLEngine;
-import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLContext;
 
 import jakarta.ws.rs.ProcessingException;
 import jakarta.ws.rs.client.Client;
@@ -123,6 +126,7 @@
     private static final String PRUNE_INACTIVE_POOL = "prune_inactive_pool";
     private static final String READ_TIMEOUT_HANDLER = "read_timeout_handler";
     private static final String REQUEST_HANDLER = "request_handler";
+    private static final String EXPECT_100_CONTINUE_HANDLER = "expect_100_continue_handler";
 
     NettyConnector(Client client) {
 
@@ -189,6 +193,9 @@
     protected void execute(final ClientRequest jerseyRequest, final Set<URI> redirectUriHistory,
             final CompletableFuture<ClientResponse> responseAvailable) {
         Integer timeout = jerseyRequest.resolveProperty(ClientProperties.READ_TIMEOUT, 0);
+        final Integer expect100ContinueTimeout = jerseyRequest.resolveProperty(
+                NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT,
+                NettyClientProperties.DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE);
         if (timeout == null || timeout < 0) {
             throw new ProcessingException(LocalizationMessages.WRONG_READ_TIMEOUT(timeout));
         }
@@ -200,8 +207,10 @@
         int port = requestUri.getPort() != -1 ? requestUri.getPort() : "https".equals(requestUri.getScheme()) ? 443 : 80;
 
         try {
+            final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder()
+                    .request(jerseyRequest).setSNIAlways(true).build();
 
-            String key = requestUri.getScheme() + "://" + host + ":" + port;
+            String key = requestUri.getScheme() + "://" + sslConfig.getSNIHostName() + ":" + port;
             ArrayList<Channel> conns;
             synchronized (connections) {
                conns = connections.get(key);
@@ -231,9 +240,8 @@
                }
             }
 
-            Integer connectTimeout = jerseyRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, 0);
-
             if (chan == null) {
+               Integer connectTimeout = jerseyRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, 0);
                Bootstrap b = new Bootstrap();
 
                // http proxy
@@ -270,7 +278,7 @@
                      if ("https".equals(requestUri.getScheme())) {
                          // making client authentication optional for now; it could be extracted to configurable property
                          JdkSslContext jdkSslContext = new JdkSslContext(
-                                 client.getSslContext(),
+                                 getSslContext(client, jerseyRequest),
                                  true,
                                  (Iterable) null,
                                  IdentityCipherSuiteFilter.INSTANCE,
@@ -281,8 +289,7 @@
                          );
 
                          final int port = requestUri.getPort();
-                         final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder()
-                                 .request(jerseyRequest).setSNIAlways(true).build();
+
                          final SslHandler sslHandler = jdkSslContext.newHandler(
                                  ch.alloc(), sslConfig.getSNIHostName(), port <= 0 ? 443 : port, executorService
                          );
@@ -320,9 +327,11 @@
             final Channel ch = chan;
             JerseyClientHandler clientHandler =
                     new JerseyClientHandler(jerseyRequest, responseAvailable, responseDone, redirectUriHistory, this);
+            final JerseyExpectContinueHandler expect100ContinueHandler = new JerseyExpectContinueHandler();
             // read timeout makes sense really as an inactivity timeout
             ch.pipeline().addLast(READ_TIMEOUT_HANDLER,
                                   new IdleStateHandler(0, 0, timeout, TimeUnit.MILLISECONDS));
+            ch.pipeline().addLast(EXPECT_100_CONTINUE_HANDLER, expect100ContinueHandler);
             ch.pipeline().addLast(REQUEST_HANDLER, clientHandler);
 
             responseDone.whenComplete((_r, th) -> {
@@ -375,11 +384,13 @@
             }
 
             // headers
-            setHeaders(jerseyRequest, nettyRequest.headers());
+            if (!jerseyRequest.hasEntity()) {
+                setHeaders(jerseyRequest, nettyRequest.headers(), false);
 
-            // host header - http 1.1
-            if (!nettyRequest.headers().contains(HttpHeaderNames.HOST)) {
-                nettyRequest.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost());
+                // host header - http 1.1
+                if (!nettyRequest.headers().contains(HttpHeaderNames.HOST)) {
+                    nettyRequest.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost());
+                }
             }
 
             if (jerseyRequest.hasEntity()) {
@@ -407,23 +418,35 @@
 //                      // Set later after the entity is "written"
 //                      break;
                 }
+                try {
+                    expect100ContinueHandler.processExpect100ContinueRequest(nettyRequest, jerseyRequest,
+                            ch, expect100ContinueTimeout);
+                } catch (ExecutionException e) {
+                    responseDone.completeExceptionally(e);
+                } catch (TimeoutException e) {
+                    //Expect:100-continue allows timeouts by the spec
+                    //just removing the pipeline from processing
+                    if (ch.pipeline().context(JerseyExpectContinueHandler.class) != null) {
+                        ch.pipeline().remove(EXPECT_100_CONTINUE_HANDLER);
+                    }
+                }
 
-                // Send the HTTP request.
-                entityWriter.writeAndFlush(nettyRequest);
+                final CountDownLatch headersSet = new CountDownLatch(1);
+                final CountDownLatch contentLengthSet = new CountDownLatch(1);
 
                 jerseyRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {
                     @Override
                     public OutputStream getOutputStream(int contentLength) throws IOException {
+                        replaceHeaders(jerseyRequest, nettyRequest.headers()); // WriterInterceptor changes
+                        if (!nettyRequest.headers().contains(HttpHeaderNames.HOST)) {
+                            nettyRequest.headers().add(HttpHeaderNames.HOST, jerseyRequest.getUri().getHost());
+                        }
+                        headersSet.countDown();
+
                         return entityWriter.getOutputStream();
                     }
                 });
 
-                if (HttpUtil.isTransferEncodingChunked(nettyRequest)) {
-                    entityWriter.write(new HttpChunkedInput(entityWriter.getChunkedInput()));
-                } else {
-                    entityWriter.write(entityWriter.getChunkedInput());
-                }
-
                 executorService.execute(new Runnable() {
                     @Override
                     public void run() {
@@ -434,9 +457,8 @@
                             jerseyRequest.writeEntity();
 
                             if (entityWriter.getType() == NettyEntityWriter.Type.DELAYED) {
-                                replaceHeaders(jerseyRequest, nettyRequest.headers()); // WriterInterceptor changes
                                 nettyRequest.headers().set(HttpHeaderNames.CONTENT_LENGTH, entityWriter.getLength());
-                                entityWriter.flush();
+                                contentLengthSet.countDown();
                             }
 
                         } catch (IOException e) {
@@ -445,9 +467,23 @@
                     }
                 });
 
-                if (entityWriter.getType() != NettyEntityWriter.Type.DELAYED) {
-                    entityWriter.flush();
+                headersSet.await();
+                if (!expect100ContinueHandler.isExpected()) {
+                    // Send the HTTP request. Expect:100-continue processing is not applicable
+                    // in this case.
+                    entityWriter.writeAndFlush(nettyRequest);
                 }
+
+                if (HttpUtil.isTransferEncodingChunked(nettyRequest)) {
+                    entityWriter.write(new HttpChunkedInput(entityWriter.getChunkedInput()));
+                } else {
+                    entityWriter.write(entityWriter.getChunkedInput());
+                }
+
+                if (entityWriter.getType() == NettyEntityWriter.Type.DELAYED) {
+                    contentLengthSet.await();
+                }
+                entityWriter.flush();
             } else {
                 // Send the HTTP request.
                 ch.writeAndFlush(nettyRequest);
@@ -458,6 +494,11 @@
         }
     }
 
+    private SSLContext getSslContext(Client client, ClientRequest request) {
+        Supplier<SSLContext> supplier = request.resolveProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, Supplier.class);
+        return supplier == null ? client.getSslContext() : supplier.get();
+    }
+
     private String buildPathWithQueryParameters(URI requestUri) {
         if (requestUri.getRawQuery() != null) {
             return String.format("%s?%s", requestUri.getRawPath(), requestUri.getRawQuery());
@@ -510,7 +551,8 @@
 
     private static ProxyHandler createProxyHandler(ClientRequest jerseyRequest, SocketAddress proxyAddr,
                                                    String userName, String password, long connectTimeout) {
-        HttpHeaders httpHeaders = setHeaders(jerseyRequest, new DefaultHttpHeaders());
+        final Boolean filter = jerseyRequest.resolveProperty(NettyClientProperties.FILTER_HEADERS_FOR_PROXY, Boolean.TRUE);
+        HttpHeaders httpHeaders = setHeaders(jerseyRequest, new DefaultHttpHeaders(), Boolean.TRUE.equals(filter));
 
         ProxyHandler proxy = userName == null ? new HttpProxyHandler(proxyAddr, httpHeaders)
                 : new HttpProxyHandler(proxyAddr, userName, password, httpHeaders);
@@ -521,9 +563,12 @@
         return proxy;
     }
 
-    private static HttpHeaders setHeaders(ClientRequest jerseyRequest, HttpHeaders headers) {
+    private static HttpHeaders setHeaders(ClientRequest jerseyRequest, HttpHeaders headers, boolean proxyOnly) {
         for (final Map.Entry<String, List<String>> e : jerseyRequest.getStringHeaders().entrySet()) {
-            headers.add(e.getKey(), e.getValue());
+            final String key = e.getKey();
+            if (!proxyOnly || JerseyClientHandler.ProxyHeaders.INSTANCE.test(key) || additionalProxyHeadersToKeep(key)) {
+                headers.add(key, e.getValue());
+            }
         }
         return headers;
     }
@@ -534,4 +579,11 @@
         }
         return headers;
     }
+
+    /*
+     * Keep all X- headers (X-Forwarded-For,...) for proxy
+     */
+    private static boolean additionalProxyHeadersToKeep(String key) {
+        return key.length() > 2 && (key.charAt(0) == 'x' || key.charAt(0) == 'X') && (key.charAt(1) == '-');
+    }
 }
diff --git a/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties b/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties
index ba91c4f..7d6f9fc 100644
--- a/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties
+++ b/connectors/netty-connector/src/main/resources/org/glassfish/jersey/netty/connector/localization.properties
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2016, 2022 Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2016, 2023 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
@@ -22,3 +22,4 @@
 redirect.error.determining.location="Error determining redirect location: ({0})."
 redirect.infinite.loop="Infinite loop in chained redirects detected."
 redirect.limit.reached="Max chained redirect limit ({0}) exceeded."
+unexpected.value.for.expect.100.continue.statuses=Unexpected value: ("{0}").
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/BufferedTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/BufferedTest.java
index a01c04d..963dd86 100644
--- a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/BufferedTest.java
+++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/BufferedTest.java
@@ -51,12 +51,10 @@
     public static class BufferedTestResource {
         @POST
         public String post(@Context HttpHeaders headers, String entity) {
-            System.out.println("Remote");
             String ret = headers.getHeaderString(HEADER_1)
                     + headers.getHeaderString(HEADER_2)
                     + headers.getHeaderString(HEADER_3)
                     + entity;
-            System.out.println(ret);
             return ret;
         }
     }
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HttpHeadersTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HttpHeadersTest.java
index 2eb3658..ee752a5 100644
--- a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HttpHeadersTest.java
+++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/HttpHeadersTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2023 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
@@ -16,6 +16,7 @@
 
 package org.glassfish.jersey.netty.connector;
 
+import jakarta.ws.rs.GET;
 import jakarta.ws.rs.HeaderParam;
 import jakarta.ws.rs.POST;
 import jakarta.ws.rs.Path;
@@ -46,6 +47,15 @@
             assertEquals("client", xClient);
             return "POST";
         }
+
+        @GET
+        public String get(
+                @HeaderParam("Transfer-Encoding") String transferEncoding,
+                @HeaderParam("X-CLIENT") String xClient,
+                @HeaderParam("X-WRITER") String xWriter) {
+            assertEquals("client", xClient);
+            return "GET";
+        }
     }
 
     @Override
@@ -66,4 +76,13 @@
         assertTrue(response.hasEntity());
         response.close();
     }
+
+    @Test
+    public void testGet() {
+        Response response = target("test").request().header("X-CLIENT", "client").get();
+
+        assertEquals(200, response.getStatus());
+        assertTrue(response.hasEntity());
+        response.close();
+    }
 }
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/RedirectHeadersTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/RedirectHeadersTest.java
new file mode 100644
index 0000000..dd56e74
--- /dev/null
+++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/RedirectHeadersTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2023 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.netty.connector;
+
+import io.netty.handler.codec.http.HttpHeaderNames;
+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.server.ServerProperties;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.Response;
+import java.net.URI;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Logger;
+
+public class RedirectHeadersTest extends JerseyTest {
+
+    private static final Logger LOGGER = Logger.getLogger(RedirectHeadersTest.class.getName());
+    private static final String TEST_URL = "http://localhost:%d/test";
+    private static final AtomicReference<String> TEST_URL_REF = new AtomicReference<>();
+    private static final String ENTITY = "entity";
+
+    @BeforeEach
+    public void before() {
+        final String url = String.format(TEST_URL, getPort());
+        TEST_URL_REF.set(url);
+    }
+
+    @Path("/test")
+    public static class RedirectResource {
+        @GET
+        public String get(@QueryParam("value") String value) {
+            return "GET" + value;
+        }
+
+        @POST
+        public String echo(@QueryParam("value") String value, String entity) {
+            return entity + value;
+        }
+
+        @GET
+        @Path("headers2")
+        public String headers(@Context HttpHeaders headers) {
+            String encoding = headers.getHeaderString(HttpHeaders.CONTENT_ENCODING);
+            String auth = headers.getHeaderString(HttpHeaderNames.PROXY_AUTHORIZATION.toString());
+            return encoding + ":" + auth;
+        }
+
+        @POST
+        @Path("301")
+        public Response redirect301(String entity) {
+            return Response.status(Response.Status.MOVED_PERMANENTLY)
+                    .header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "?value=301"))
+                    .build();
+        }
+
+        @POST
+        @Path("302")
+        public Response redirect302(String entity) {
+            return Response.status(Response.Status.FOUND)
+                    .header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "?value=302"))
+                    .build();
+        }
+
+        @POST
+        @Path("307")
+        public Response redirect307(String entity) {
+            return Response.status(Response.Status.TEMPORARY_REDIRECT)
+                    .header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get()  + "?value=307"))
+                    .build();
+        }
+
+        @POST
+        @Path("308")
+        public Response redirectHeaders(String entity) {
+            return Response.status(308)
+                    .header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "?value=308"))
+                    .build();
+        }
+
+
+        @POST
+        @Path("headers1")
+        public Response redirect308(String whatever) {
+            return Response.status(301).header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "/headers2")).build();
+        }
+
+        @GET
+        @Path("relative")
+        public Response relative() {
+            return Response.status(301).header(HttpHeaders.LOCATION, URI.create("/test/headers2").toASCIIString()).build();
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        ResourceConfig config = new ResourceConfig(RedirectResource.class);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        config.property(ServerProperties.LOCATION_HEADER_RELATIVE_URI_RESOLUTION_DISABLED, Boolean.TRUE);
+        return config;
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.property(ClientProperties.FOLLOW_REDIRECTS, true);
+        config.connectorProvider(new NettyConnectorProvider());
+    }
+
+    @Test
+    void testPost() {
+        testPost("301");
+        testPost("302");
+        testPost("307");
+        testPost("308");
+    }
+
+    @Test
+    void testGet() {
+        Assertions.assertEquals("GET301", testGet("301"));
+        Assertions.assertEquals("GET302", testGet("302"));
+        Assertions.assertEquals(ENTITY + "307", testGet("307"));
+        Assertions.assertEquals(ENTITY + "308", testGet("308"));
+    }
+
+    @Test
+    void testHeaders() {
+        MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
+        headers.add(HttpHeaders.CONTENT_ENCODING, "gzip");
+        headers.add(HttpHeaderNames.PROXY_AUTHORIZATION.toString(), "basic aGVsbG86d29ybGQ=");
+        try (Response response = target("test")
+                .property(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, false)
+                .path("headers1").request().headers(headers).post(Entity.entity(ENTITY, MediaType.TEXT_PLAIN_TYPE))) {
+            Assertions.assertEquals(200, response.getStatus());
+            Assertions.assertEquals("null:null", response.readEntity(String.class));
+        }
+    }
+
+    @Test
+    void testRelative() {
+        MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
+        try (Response response = target("test")
+                .path("relative").request().headers(headers).get()) {
+            Assertions.assertEquals(200, response.getStatus());
+            Assertions.assertEquals("null:null", response.readEntity(String.class));
+        }
+    }
+
+    void testPost(String status) {
+        try (Response response = target("test").path(status).request().post(Entity.entity(ENTITY, MediaType.TEXT_PLAIN_TYPE))) {
+            Assertions.assertEquals(200, response.getStatus());
+            Assertions.assertEquals(ENTITY + status, response.readEntity(String.class));
+        }
+    }
+
+    String testGet(String status) {
+        try (Response response = target("test")
+                .property(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, false)
+                .path(status).request().post(Entity.entity(ENTITY, MediaType.TEXT_PLAIN_TYPE))) {
+            Assertions.assertEquals(200, response.getStatus());
+            return response.readEntity(String.class);
+        }
+    }
+}
diff --git a/connectors/pom.xml b/connectors/pom.xml
index 3ac6040..ad4afa3 100644
--- a/connectors/pom.xml
+++ b/connectors/pom.xml
@@ -40,6 +40,7 @@
         <module>helidon-connector</module>
         <module>jdk-connector</module>
         <module>jetty-connector</module>
+        <module>jetty11-http2-connector</module>
         <module>jetty11-connector</module>
         <module>jnh-connector</module>
         <module>netty-connector</module>