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

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

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

 

-Jackson JAX-RS Providers Version 2.18.0

+Jackson JAX-RS Providers Version 2.19.1

 * License: Apache License, 2.0

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

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

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
index 471321f..6072a52 100644
--- 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2025 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
@@ -30,20 +30,21 @@
 
 class Expect100ContinueConnectorExtension
         implements ConnectorExtension<HttpRequest, IOException> {
+
+    private final NettyConnectorProvider.Config.RW requestConfiguration;
+
+    Expect100ContinueConnectorExtension(NettyConnectorProvider.Config.RW requestConfiguration) {
+        this.requestConfiguration = requestConfiguration;
+    }
+
     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 RequestEntityProcessing entityProcessing = requestConfiguration.requestEntityProcessing(request);
+        final Boolean expectContinueActivated = requestConfiguration.expect100Continue(request);
+        final long expectContinueSizeThreshold = requestConfiguration.expect100ContinueThreshold(request);
         final boolean allowStreaming = length > expectContinueSizeThreshold
                 || entityProcessing == RequestEntityProcessing.CHUNKED;
 
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 7f5bb9b..1efebc2 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
@@ -31,7 +31,6 @@
 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;
@@ -56,17 +55,14 @@
  */
 class JerseyClientHandler extends SimpleChannelInboundHandler<HttpObject> {
 
-    private static final int DEFAULT_MAX_REDIRECTS = 5;
-
     // Modified only by the same thread. No need to synchronize it.
     private final Set<URI> redirectUriHistory;
     private final ClientRequest jerseyRequest;
     private final CompletableFuture<ClientResponse> responseAvailable;
     private final CompletableFuture<?> responseDone;
-    private final boolean followRedirects;
-    private final int maxRedirects;
     private final NettyConnector connector;
     private final NettyHttpRedirectController redirectController;
+    private final NettyConnectorProvider.Config.RW requestConfiguration;
 
     private NettyInputStream nis;
     private ClientResponse jerseyResponse;
@@ -74,19 +70,20 @@
     private boolean readTimedOut;
 
     JerseyClientHandler(ClientRequest request, CompletableFuture<ClientResponse> responseAvailable,
-                        CompletableFuture<?> responseDone, Set<URI> redirectUriHistory, NettyConnector connector) {
+                        CompletableFuture<?> responseDone, Set<URI> redirectUriHistory, NettyConnector connector,
+                        NettyConnectorProvider.Config.RW requestConfiguration) {
         this.redirectUriHistory = redirectUriHistory;
         this.jerseyRequest = request;
         this.responseAvailable = responseAvailable;
         this.responseDone = responseDone;
-        // Follow redirects by default
-        this.followRedirects = jerseyRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, true);
-        this.maxRedirects = jerseyRequest.resolveProperty(NettyClientProperties.MAX_REDIRECTS, DEFAULT_MAX_REDIRECTS);
+        this.requestConfiguration = requestConfiguration;
         this.connector = connector;
+        // Follow redirects by default
+        requestConfiguration.followRedirects(jerseyRequest);
+        requestConfiguration.maxRedirects(jerseyRequest);
 
-        final NettyHttpRedirectController customRedirectController = jerseyRequest
-                .resolveProperty(NettyClientProperties.HTTP_REDIRECT_CONTROLLER, NettyHttpRedirectController.class);
-        this.redirectController = customRedirectController == null ? new NettyHttpRedirectController() : customRedirectController;
+        this.redirectController = requestConfiguration.redirectController(jerseyRequest);
+        this.redirectController.init(requestConfiguration);
     }
 
     @Override
@@ -112,7 +109,7 @@
           ClientResponse cr = jerseyResponse;
           jerseyResponse = null;
           int responseStatus = cr.getStatus();
-          if (followRedirects
+          if (Boolean.TRUE.equals(requestConfiguration.followRedirects())
                   && (responseStatus == ResponseStatus.Redirect3xx.MOVED_PERMANENTLY_301.getStatusCode()
                           || responseStatus == ResponseStatus.Redirect3xx.FOUND_302.getStatusCode()
                           || responseStatus == ResponseStatus.Redirect3xx.SEE_OTHER_303.getStatusCode()
@@ -139,16 +136,17 @@
                           // infinite loop detection
                           responseAvailable.completeExceptionally(
                                   new RedirectException(LocalizationMessages.REDIRECT_INFINITE_LOOP()));
-                      } else if (redirectUriHistory.size() > maxRedirects) {
+                      } else if (redirectUriHistory.size() > requestConfiguration.maxRedirects.get()) {
                           // maximal number of redirection
-                          responseAvailable.completeExceptionally(
-                                  new RedirectException(LocalizationMessages.REDIRECT_LIMIT_REACHED(maxRedirects)));
+                          responseAvailable.completeExceptionally(new RedirectException(
+                                  LocalizationMessages.REDIRECT_LIMIT_REACHED(requestConfiguration.maxRedirects.get())));
                       } else {
                           ClientRequest newReq = new ClientRequest(jerseyRequest);
                           newReq.setUri(newUri);
                           ctx.close();
                           if (redirectController.prepareRedirect(newReq, cr)) {
-                              final NettyConnector newConnector = new NettyConnector(newReq.getClient());
+                              final NettyConnector newConnector =
+                                      new NettyConnector(newReq.getClient(), connector.clientConfiguration);
                               newConnector.execute(newReq, redirectUriHistory, new CompletableFuture<ClientResponse>() {
                                   @Override
                                   public boolean complete(ClientResponse value) {
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 4b2e4c9..7dc3495 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
@@ -73,6 +73,11 @@
     public static final String IDLE_CONNECTION_PRUNE_TIMEOUT = "jersey.config.client.idleConnectionPruneTimeout";
 
     /**
+     * Enable or disable the Netty logging by {@code LoggingHandler(Level.DEBUG)}. Disabled by default.
+     */
+    public static final String LOGGING_ENABLED = "jersey.config.client.netty.loggingEnabled";
+
+    /**
      *  <p>
      *    This property determines the maximum number of idle connections that will be simultaneously kept alive, per destination.
      *    The default is 5.
@@ -157,7 +162,8 @@
         DEFAULT_HEADER_SIZE = 8192;
 
     /**
-     * Parameter which allows extending of the initial line length for the Netty connector
+     * Parameter which allows extending of the first line length of the HTTP header for the Netty connector.
+     * Taken from {@link io.netty.handler.codec.http.HttpClientCodec#HttpClientCodec(int, int, int)}.
      *
      * @since 2.44
      */
@@ -166,12 +172,12 @@
 
     /**
      * Default initial line length for Netty Connector.
-     * Taken from {@link io.netty.handler.codec.http.HttpClientCodec#HttpClientCodec(int, int, int)}
+     * Typically, set this to the same value as {@link #MAX_HEADER_SIZE}.
      *
      * @since 2.44
      */
     public static final Integer
-        DEFAULT_INITIAL_LINE_LENGTH = 4096;
+        DEFAULT_INITIAL_LINE_LENGTH = 8192;
 
     /**
      * Parameter which allows extending of the chunk size for the Netty connector
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectionController.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectionController.java
new file mode 100644
index 0000000..5d8725c
--- /dev/null
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectionController.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2025 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 org.glassfish.jersey.client.ClientRequest;
+
+import java.net.URI;
+
+/**
+ * Adjustable connection pooling controller.
+ */
+public class NettyConnectionController {
+    /**
+     * Get the group of connections to be pooled, purged idle, and reused together.
+     *
+     * @param clientRequest the HTTP client request.
+     * @param uri the uri for the HTTP client request.
+     * @param hostName the hostname for the request. Can differ from the hostname in the uri based on other request attributes.
+     * @param port the real port for the request. Can differ from the port in the uri based on other request attributes.
+     * @return the group of connections identifier.
+     */
+    public String getConnectionGroup(ClientRequest clientRequest, URI uri, String hostName, int port) {
+        return uri.getScheme() + "://" + hostName + ":" + port;
+    }
+}
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 a65560c..ba80967 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
@@ -18,8 +18,6 @@
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.net.InetSocketAddress;
-import java.net.SocketAddress;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -36,7 +34,6 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
-import java.util.function.Supplier;
 
 import javax.net.ssl.SSLContext;
 
@@ -56,10 +53,8 @@
 import io.netty.channel.socket.SocketChannel;
 import io.netty.channel.socket.nio.NioSocketChannel;
 import io.netty.handler.codec.http.DefaultFullHttpRequest;
-import io.netty.handler.codec.http.DefaultHttpHeaders;
 import io.netty.handler.codec.http.DefaultHttpRequest;
 import io.netty.handler.codec.http.HttpChunkedInput;
-import io.netty.handler.codec.http.HttpClientCodec;
 import io.netty.handler.codec.http.HttpContentDecompressor;
 import io.netty.handler.codec.http.HttpHeaderNames;
 import io.netty.handler.codec.http.HttpHeaders;
@@ -68,8 +63,8 @@
 import io.netty.handler.codec.http.HttpUtil;
 import io.netty.handler.codec.http.HttpVersion;
 import io.netty.handler.codec.http.LastHttpContent;
-import io.netty.handler.proxy.HttpProxyHandler;
-import io.netty.handler.proxy.ProxyHandler;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
 import io.netty.handler.ssl.ApplicationProtocolConfig;
 import io.netty.handler.ssl.ClientAuth;
 import io.netty.handler.ssl.IdentityCipherSuiteFilter;
@@ -81,7 +76,6 @@
 import io.netty.handler.timeout.IdleStateHandler;
 import io.netty.resolver.NoopAddressResolverGroup;
 import io.netty.util.concurrent.GenericFutureListener;
-import org.glassfish.jersey.client.ClientProperties;
 import org.glassfish.jersey.client.ClientRequest;
 import org.glassfish.jersey.client.ClientResponse;
 import org.glassfish.jersey.client.innate.ClientProxy;
@@ -106,6 +100,7 @@
     final EventLoopGroup group;
     final Client client;
     final HashMap<String, ArrayList<Channel>> connections = new HashMap<>();
+    final NettyConnectorProvider.Config.RW clientConfiguration;
 
     private static final LazyValue<String> NETTY_VERSION = Values.lazy(
         (Value<String>) () -> {
@@ -118,63 +113,29 @@
             return "Netty " + nettyVersion;
         });
 
-    // If HTTP keepalive is enabled the value of "http.maxConnections" determines the maximum number
-    // of idle connections that will be simultaneously kept alive, per destination.
-    private static final String HTTP_KEEPALIVE_STRING = System.getProperty("http.keepAlive");
-    // http.keepalive (default: true)
-    private static final Boolean HTTP_KEEPALIVE =
-            HTTP_KEEPALIVE_STRING == null ? Boolean.TRUE : Boolean.parseBoolean(HTTP_KEEPALIVE_STRING);
-
-    // http.maxConnections (default: 5)
-    private static final int DEFAULT_MAX_POOL_SIZE = 5;
-    private static final int MAX_POOL_SIZE = Integer.getInteger("http.maxConnections", DEFAULT_MAX_POOL_SIZE);
-    private static final int DEFAULT_MAX_POOL_IDLE = 60; // seconds
-    private static final int DEFAULT_MAX_POOL_SIZE_TOTAL = 60; // connections
-
-
-    private final Integer maxPoolSize; // either from system property, or from Jersey config, or default
-    private final Integer maxPoolSizeTotal; //either from Jersey config, or default
-    private final Integer maxPoolIdle; // either from Jersey config, or default
-
     static final String INACTIVE_POOLED_CONNECTION_HANDLER = "inactive_pooled_connection_handler";
     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) {
+    NettyConnector(Client client, NettyConnectorProvider.Config.RW connectorConfiguration) {
+        this.client = client;
+        this.clientConfiguration = connectorConfiguration.fromClient(client);
 
         final Configuration configuration = client.getConfiguration();
-        final Map<String, Object> properties = configuration.getProperties();
-        final Object threadPoolSize = properties.get(ClientProperties.ASYNC_THREADPOOL_SIZE);
-
-        if (threadPoolSize != null && threadPoolSize instanceof Integer && (Integer) threadPoolSize > 0) {
-            executorService = VirtualThreadUtil.withConfig(configuration).newFixedThreadPool((Integer) threadPoolSize);
-            this.group = new NioEventLoopGroup((Integer) threadPoolSize);
+        final Integer threadPoolSize = this.clientConfiguration.asyncThreadPoolSize();
+        if (threadPoolSize != null && threadPoolSize > 0) {
+            executorService = VirtualThreadUtil
+                                .withConfig(clientConfiguration.prefixedConfiguration(configuration))
+                                .newFixedThreadPool(threadPoolSize);
+            this.group = new NioEventLoopGroup(threadPoolSize);
         } else {
-            executorService = VirtualThreadUtil.withConfig(configuration).newCachedThreadPool();
+            executorService = VirtualThreadUtil
+                                .withConfig(clientConfiguration.prefixedConfiguration(configuration))
+                                .newCachedThreadPool();
             this.group = new NioEventLoopGroup();
         }
-
-        this.client = client;
-
-        final Object maxPoolSizeTotalProperty = properties.get(NettyClientProperties.MAX_CONNECTIONS_TOTAL);
-        final Object maxPoolIdleProperty = properties.get(NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT);
-        final Object maxPoolSizeProperty = properties.get(NettyClientProperties.MAX_CONNECTIONS);
-
-        maxPoolSizeTotal = maxPoolSizeTotalProperty != null ? (Integer) maxPoolSizeTotalProperty : DEFAULT_MAX_POOL_SIZE_TOTAL;
-        maxPoolIdle = maxPoolIdleProperty != null ? (Integer) maxPoolIdleProperty : DEFAULT_MAX_POOL_IDLE;
-        maxPoolSize = maxPoolSizeProperty != null
-                ? (Integer) maxPoolSizeProperty
-                : (HTTP_KEEPALIVE ? MAX_POOL_SIZE : DEFAULT_MAX_POOL_SIZE);
-
-        if (maxPoolSizeTotal < 0) {
-            throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_TOTAL(maxPoolSizeTotal));
-        }
-
-        if (maxPoolSize < 0) {
-            throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_SIZE(maxPoolSize));
-        }
     }
 
     @Override
@@ -207,25 +168,31 @@
 
     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));
+        final NettyConnectorProvider.Config.RW requestConfiguration =
+                clientConfiguration
+                        .fromRequest(jerseyRequest)
+                        .readTimeout(jerseyRequest)
+                        .expect100ContinueTimeout(jerseyRequest);
+        final int readTimeout = requestConfiguration.readTimeout();
+        if (readTimeout < 0) {
+            throw new ProcessingException(LocalizationMessages.WRONG_READ_TIMEOUT(readTimeout));
         }
 
         final CompletableFuture<?> responseDone = new CompletableFuture<>();
 
         final URI requestUri = jerseyRequest.getUri();
-        String host = requestUri.getHost();
-        int port = requestUri.getPort() != -1 ? requestUri.getPort() : "https".equals(requestUri.getScheme()) ? 443 : 80;
+        final String host = requestUri.getHost();
+        final int port = requestUri.getPort() != -1
+                ? requestUri.getPort()
+                : "https".equalsIgnoreCase(requestUri.getScheme()) ? 443 : 80;
 
         try {
-            final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder()
+            final SSLParamConfigurator sslConfig = SSLParamConfigurator.builder(requestConfiguration)
                     .request(jerseyRequest).setSNIAlways(true).setSNIHostName(jerseyRequest).build();
 
-            String key = requestUri.getScheme() + "://" + sslConfig.getSNIHostName() + ":" + port;
+            final String key = requestConfiguration
+                                .connectionController()
+                                .getConnectionGroup(jerseyRequest, requestUri, sslConfig.getSNIHostName(), port);
             ArrayList<Channel> conns;
             synchronized (connections) {
                conns = connections.get(key);
@@ -245,8 +212,8 @@
                   } catch (NoSuchElementException e) {
                       /*
                        *  Eat it.
-                       *  It could happen that the channel was closed, pipeline cleared and
-                       *  then it will fail to remove the names with this exception.
+                       *  It could happen that the channel was closed, pipeline cleared,
+                       *  and then it will fail to remove the names with this exception.
                        */
                   }
                   if (!chan.isOpen()) {
@@ -258,20 +225,15 @@
             final JerseyExpectContinueHandler expect100ContinueHandler = new JerseyExpectContinueHandler();
 
             if (chan == null) {
-               Integer connectTimeout = jerseyRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, 0);
+               requestConfiguration.connectTimeout(jerseyRequest);
                Bootstrap b = new Bootstrap();
 
                // http proxy
-               Optional<ClientProxy> proxy = ClientProxy.proxyFromRequest(jerseyRequest);
-               if (!proxy.isPresent()) {
-                   proxy = ClientProxy.proxyFromProperties(requestUri);
-               }
-               proxy.ifPresent(clientProxy -> {
+               final Optional<ClientProxy> handlerProxy = requestConfiguration.proxy(jerseyRequest, requestUri);
+               handlerProxy.ifPresent(clientProxy -> {
                    b.resolver(NoopAddressResolverGroup.INSTANCE); // request hostname resolved by the HTTP proxy
                });
 
-               final Optional<ClientProxy> handlerProxy = proxy;
-
                b.group(group)
                 .channel(NioSocketChannel.class)
                 .handler(new ChannelInitializer<SocketChannel>() {
@@ -283,19 +245,14 @@
 
                      // http proxy
                      handlerProxy.ifPresent(clientProxy -> {
-                         final URI u = clientProxy.uri();
-                         InetSocketAddress proxyAddr = new InetSocketAddress(u.getHost(),
-                                 u.getPort() == -1 ? 8080 : u.getPort());
-                         ProxyHandler proxy1 = createProxyHandler(jerseyRequest, proxyAddr,
-                                 clientProxy.userName(), clientProxy.password(), connectTimeout);
-                         p.addLast(proxy1);
+                         p.addLast(requestConfiguration.createProxyHandler(clientProxy, jerseyRequest));
                      });
 
                      // Enable HTTPS if necessary.
                      if ("https".equals(requestUri.getScheme())) {
                          // making client authentication optional for now; it could be extracted to configurable property
                          JdkSslContext jdkSslContext = new JdkSslContext(
-                                 getSslContext(client, jerseyRequest),
+                                 requestConfiguration.sslContext(client, jerseyRequest),
                                  true,
                                  (Iterable) null,
                                  IdentityCipherSuiteFilter.INSTANCE,
@@ -310,8 +267,7 @@
                          final SslHandler sslHandler = jdkSslContext.newHandler(
                                  ch.alloc(), sslConfig.getSNIHostName(), port <= 0 ? 443 : port, executorService
                          );
-                         if (ClientProperties.getValue(config.getProperties(),
-                                                       NettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION, true)) {
+                         if (requestConfiguration.isSslHostnameVerificationEnabled(config.getProperties())) {
                              sslConfig.setEndpointIdentificationAlgorithm(sslHandler.engine());
                          }
 
@@ -320,16 +276,10 @@
                          p.addLast(sslHandler);
                      }
 
-                     final Integer maxHeaderSize = ClientProperties.getValue(config.getProperties(),
-                                NettyClientProperties.MAX_HEADER_SIZE,
-                                NettyClientProperties.DEFAULT_HEADER_SIZE);
-                     final Integer maxChunkSize = ClientProperties.getValue(config.getProperties(),
-                                NettyClientProperties.MAX_CHUNK_SIZE,
-                                NettyClientProperties.DEFAULT_CHUNK_SIZE);
-                     final Integer maxInitialLineLength = ClientProperties.getValue(config.getProperties(),
-                                NettyClientProperties.MAX_INITIAL_LINE_LENGTH,
-                                NettyClientProperties.DEFAULT_INITIAL_LINE_LENGTH);
-                     p.addLast(new HttpClientCodec(maxInitialLineLength, maxHeaderSize, maxChunkSize));
+                     if (requestConfiguration.loggingEnabled.get()) {
+                         p.addLast(new LoggingHandler(LogLevel.INFO));
+                     }
+                     p.addLast(requestConfiguration.createHttpClientCodec(config.getProperties()));
                      p.addLast(EXPECT_100_CONTINUE_HANDLER, expect100ContinueHandler);
                      p.addLast(new ChunkedWriteHandler());
                      p.addLast(new HttpContentDecompressor());
@@ -337,8 +287,8 @@
                 });
 
                // connect timeout
-               if (connectTimeout > 0) {
-                   b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout);
+               if (requestConfiguration.connectTimeout() > 0) {
+                   b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, requestConfiguration.connectTimeout());
                }
 
                // Make the connection attempt.
@@ -357,12 +307,12 @@
             // assert: it is ok to abort the entire response, if responseDone is completed exceptionally - in particular, nothing
             //         will leak
             final Channel ch = chan;
-            JerseyClientHandler clientHandler =
-                    new JerseyClientHandler(jerseyRequest, responseAvailable, responseDone, redirectUriHistory, this);
+            JerseyClientHandler clientHandler = new JerseyClientHandler(
+                    jerseyRequest, responseAvailable, responseDone, redirectUriHistory, this, requestConfiguration);
 
             // read timeout makes sense really as an inactivity timeout
             ch.pipeline().addLast(READ_TIMEOUT_HANDLER,
-                                  new IdleStateHandler(0, 0, timeout, TimeUnit.MILLISECONDS));
+                                  new IdleStateHandler(0, 0, requestConfiguration.readTimeout(), TimeUnit.MILLISECONDS));
             ch.pipeline().addLast(REQUEST_HANDLER, clientHandler);
 
             responseDone.whenComplete((_r, th) -> {
@@ -370,7 +320,8 @@
                ch.pipeline().remove(clientHandler);
 
                if (th == null) {
-                  ch.pipeline().addLast(INACTIVE_POOLED_CONNECTION_HANDLER, new IdleStateHandler(0, 0, maxPoolIdle));
+                  ch.pipeline().addLast(INACTIVE_POOLED_CONNECTION_HANDLER,
+                          new IdleStateHandler(0, 0, requestConfiguration.maxPoolIdle.get()));
                   ch.pipeline().addLast(PRUNE_INACTIVE_POOL, new PruneIdlePool(connections, key));
                   boolean added = true;
                   synchronized (connections) {
@@ -381,7 +332,9 @@
                         connections.put(key, conns1);
                      } else {
                         synchronized (conns1) {
-                           if ((maxPoolSizeTotal == 0 || connections.size() < maxPoolSizeTotal) && conns1.size() < maxPoolSize) {
+                           if ((requestConfiguration.maxPoolSizeTotal.get() == 0
+                                   || connections.size() < requestConfiguration.maxPoolSizeTotal.get())
+                                   && conns1.size() < requestConfiguration.maxPoolSize.get()) {
                               conns1.add(ch);
                            } else { // else do not add the Channel to the idle pool
                               added = false;
@@ -433,7 +386,7 @@
                     };
                 ch.closeFuture().addListener(closeListener);
 
-                final NettyEntityWriter entityWriter = nettyEntityWriter(jerseyRequest, ch);
+                final NettyEntityWriter entityWriter = nettyEntityWriter(jerseyRequest, ch, requestConfiguration);
                 switch (entityWriter.getType()) {
                     case CHUNKED:
                         HttpUtil.setTransferEncodingChunked(nettyRequest, true);
@@ -493,7 +446,7 @@
                 });
 
                 headersSet.await();
-                new Expect100ContinueConnectorExtension().invoke(jerseyRequest, nettyRequest);
+                new Expect100ContinueConnectorExtension(requestConfiguration).invoke(jerseyRequest, nettyRequest);
 
                 boolean continueExpected = HttpUtil.is100ContinueExpected(nettyRequest);
                 boolean expectationsFailed  = false;
@@ -503,7 +456,7 @@
                     expect100ContinueHandler.attachCountDownLatch(expect100ContinueLatch);
                     //send expect request, sync and wait till either response or timeout received
                     entityWriter.writeAndFlush(nettyRequest);
-                    expect100ContinueLatch.await(expect100ContinueTimeout, TimeUnit.MILLISECONDS);
+                    expect100ContinueLatch.await(requestConfiguration.expect100ContTimeout.get(), TimeUnit.MILLISECONDS);
                     try {
                         expect100ContinueHandler.processExpectationStatus();
                     } catch (TimeoutException e) {
@@ -543,13 +496,10 @@
         }
     }
 
-    /* package */ NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel) {
-        return NettyEntityWriter.getInstance(clientRequest, channel);
-    }
-
-    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();
+    /* package */ NettyEntityWriter nettyEntityWriter(
+            ClientRequest clientRequest, Channel channel, NettyConnectorProvider.Config.RW requestConfiguration) {
+        return NettyEntityWriter
+                .getInstance(clientRequest, channel, () -> requestConfiguration.requestEntityProcessing(clientRequest));
     }
 
     private String buildPathWithQueryParameters(URI requestUri) {
@@ -602,21 +552,7 @@
        }
     }
 
-    private static ProxyHandler createProxyHandler(ClientRequest jerseyRequest, SocketAddress proxyAddr,
-                                                   String userName, String password, long connectTimeout) {
-        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);
-        if (connectTimeout > 0) {
-            proxy.setConnectTimeoutMillis(connectTimeout);
-        }
-
-        return proxy;
-    }
-
-    private static HttpHeaders setHeaders(ClientRequest jerseyRequest, HttpHeaders headers, boolean proxyOnly) {
+    /* package */ static HttpHeaders setHeaders(ClientRequest jerseyRequest, HttpHeaders headers, boolean proxyOnly) {
         for (final Map.Entry<String, List<String>> e : jerseyRequest.getStringHeaders().entrySet()) {
             final String key = e.getKey();
             if (!proxyOnly || JerseyClientHandler.ProxyHeaders.INSTANCE.test(key) || additionalProxyHeadersToKeep(key)) {
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorConfiguration.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorConfiguration.java
new file mode 100644
index 0000000..99f763c
--- /dev/null
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorConfiguration.java
@@ -0,0 +1,503 @@
+/*
+ * Copyright (c) 2025 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.DefaultHttpHeaders;
+import io.netty.handler.codec.http.HttpClientCodec;
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.proxy.HttpProxyHandler;
+import io.netty.handler.proxy.ProxyHandler;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.innate.ClientProxy;
+import org.glassfish.jersey.internal.util.collection.Ref;
+import org.glassfish.jersey.client.innate.ConnectorConfiguration;
+
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Client;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.util.Map;
+
+class NettyConnectorConfiguration<N extends NettyConnectorConfiguration<N>> extends ConnectorConfiguration<N> {
+
+    /* package */ final NullableRef<NettyConnectionController> connectionController = NullableRef.empty();
+    /* package */ final NullableRef<Boolean> enableHostnameVerification = NullableRef.empty();
+    /* package */ final Ref<Integer> expect100ContTimeout = NullableRef.empty();
+    /* package */ final NullableRef<Boolean> filterHeadersForProxy = NullableRef.empty();
+    /* package */ final NullableRef<Integer> firstHttpHeaderLineLength = NullableRef.empty();
+    /* package */ final Ref<Boolean> loggingEnabled = NullableRef.empty();
+    /* package */ final NullableRef<Integer> maxChunkSize = NullableRef.empty();
+    /* package */ final NullableRef<Integer> maxHeaderSize = NullableRef.empty();
+    // either from Jersey config, or default
+    /* package */ final Ref<Integer> maxPoolSizeTotal = NullableRef.empty();
+    // either from Jersey config, or default
+    /* package */ final Ref<Integer> maxPoolIdle = NullableRef.empty();
+    // either from system property, or from Jersey config, or default
+    /* package */ final Ref<Integer> maxPoolSize = NullableRef.empty();
+    /* package */ final Ref<Integer> maxRedirects = NullableRef.empty();
+    /* package */ final NullableRef<Boolean> preserveMethodOnRedirect = NullableRef.empty();
+    /* package */ final NullableRef<NettyHttpRedirectController> redirectController = NullableRef.empty();
+
+    // If HTTP keepalive is enabled the value of "http.maxConnections" determines the maximum number
+    // of idle connections that will be simultaneously kept alive, per destination.
+    private static final String HTTP_KEEPALIVE_STRING = System.getProperty("http.keepAlive");
+    // http.keepalive (default: true)
+    private static final Boolean HTTP_KEEPALIVE =
+            HTTP_KEEPALIVE_STRING == null ? Boolean.TRUE : Boolean.parseBoolean(HTTP_KEEPALIVE_STRING);
+
+    // http.maxConnections (default: 5)
+    private static final int DEFAULT_MAX_POOL_SIZE = 5;
+    private static final int MAX_POOL_SIZE = Integer.getInteger("http.maxConnections", DEFAULT_MAX_POOL_SIZE);
+    private static final int DEFAULT_MAX_POOL_IDLE = 60; // seconds
+    private static final int DEFAULT_MAX_POOL_SIZE_TOTAL = 60; // connections
+
+    private static final int DEFAULT_MAX_REDIRECTS = 5;
+
+    /**
+     * Set the connection pooling controller for the Netty Connector.
+     *
+     * @param controller the connection pooling controller.
+     * @return updated configuration.
+     */
+    public N connectionController(NettyConnectionController controller) {
+        connectionController.set(controller);
+        return self();
+    }
+
+    /**
+     * This setting determines waiting time in milliseconds for 100-Continue response when 100-Continue is sent by the client.
+     * The property {@link NettyClientProperties#EXPECT_100_CONTINUE_TIMEOUT} has precedence over this setting.
+     *
+     * @param millis the timeout for 100-Continue response.
+     * @return updated configuration.
+     */
+    public N expect100ContinueTimeout(int millis) {
+        expect100ContTimeout.set(millis);
+        return self();
+    }
+
+    /**
+     * Enable or disable the endpoint identification algorithm to HTTPS. The property
+     * {@link NettyClientProperties#ENABLE_SSL_HOSTNAME_VERIFICATION} takes precedence over this setting.
+     *
+     * @param enable enable or disable the hostname verification.
+     * @return updated configuration.
+     */
+    public N enableSslHostnameVerification(boolean enable) {
+        enableHostnameVerification.set(enable);
+        return self();
+    }
+
+    /**
+     * Enable or disable the Netty logging by {@code LoggingHandler(Level.DEBUG)}. Disabled by default.
+     * The property {@link NettyClientProperties#LOGGING_ENABLED} takes precedence over this setting.
+     *
+     * @param enable to enable or disable.
+     * @return updated configuration.
+     */
+    public N enableLoggingHandler(boolean enable) {
+        loggingEnabled.set(enable);
+        return self();
+    }
+
+    /**
+     * Filter the HTTP headers for requests (CONNECT) towards the proxy except for PROXY-prefixed
+     * and HOST headers when {@code true}.
+     * The property {@link NettyClientProperties#FILTER_HEADERS_FOR_PROXY} has precedence over this setting.
+     *
+     * @param filter to filter or not. The default is {@code true}.
+     * @return updated configuration.
+     */
+    public N filterHeadersForProxy(boolean filter) {
+        filterHeadersForProxy.set(filter);
+        return self();
+    }
+
+    /**
+     * This property determines the number of seconds the idle connections are kept in the pool before pruned.
+     * The default is 60. Specify 0 to disable. The property {@link NettyClientProperties#IDLE_CONNECTION_PRUNE_TIMEOUT}
+     * has precedence over this setting.
+     *
+     * @param seconds the timeout in seconds.
+     * @return updated configuration.
+     */
+    public N idleConnectionPruneTimeout(int seconds) {
+        maxPoolIdle.set(seconds);
+        return self();
+    }
+
+    /**
+     * Set the maximum length of the first line of the HTTP header.
+     * The property {@link NettyClientProperties#MAX_INITIAL_LINE_LENGTH} has precedence over this setting.
+     *
+     * @param length the length of the first line of the HTTP header.
+     * @return updated configuration.
+     */
+    public N initialHttpHeaderLineLength(int length) {
+        firstHttpHeaderLineLength.set(length);
+        return self();
+    }
+
+    /**
+     * Set the maximum chunk size for the Netty connector. The property {@link NettyClientProperties#MAX_CHUNK_SIZE}
+     * has precedence over this setting.
+     *
+     * @param size the new size of chunks.
+     * @return updated configuration.
+     */
+    public N maxChunkSize(int size) {
+        maxChunkSize.set(size);
+        return self();
+    }
+
+    /**
+     * This setting determines the maximum number of idle connections that will be simultaneously kept alive, per destination.
+     * The default is 5. The property {@link NettyClientProperties#MAX_CONNECTIONS} takes precedence over this setting.
+     *
+     * @param maxCount maximum number of idle connections per destination.
+     * @return updated configuration.
+     */
+    public N maxConnectionsPerDestination(int maxCount) {
+        maxPoolSize.set(maxCount);
+        return self();
+    }
+
+    /**
+     * Set the maximum header size in bytes for the HTTP headers processed by Netty.
+     * The property {@link NettyClientProperties#MAX_HEADER_SIZE} has precedence over this setting.
+     *
+     * @param size the new maximum header size.
+     * @return updated configuration.
+     */
+    public N maxHeaderSize(int size) {
+        maxHeaderSize.set(size);
+        return self();
+    }
+
+    /**
+     * Set the maximum number of redirects to prevent infinite redirect loop. The default is 5.
+     * The property {@link NettyClientProperties#MAX_REDIRECTS} has precedence over this setting.
+     *
+     * @param max the maximum number of redirects.
+     * @return updated configuration.
+     */
+    public N maxRedirects(int max) {
+        maxRedirects.set(max);
+        return self();
+    }
+
+    /**
+     * Set the maximum number of idle connections that will be simultaneously kept alive. The property
+     * {@link NettyClientProperties#MAX_CONNECTIONS_TOTAL} has precedence over this setting.
+     *
+     * @param max the maximum number of idle connections.
+     * @return updated configuration.
+     */
+    public N maxTotalConnections(int max) {
+        maxPoolSizeTotal.set(max);
+        return self();
+    }
+
+    /**
+     * Set the preservation of methods during HTTP redirect.
+     * By default, the HTTP POST request are not transformed into HTTP GET for status 301 & 302.
+     * The property {@link NettyClientProperties#PRESERVE_METHOD_ON_REDIRECT} has precedence over this setting.
+     *
+     * @param preserve to preserve or not to preserve.
+     * @return updated configuration.
+     */
+    public N preserveMethodOnRedirect(boolean preserve) {
+        preserveMethodOnRedirect.set(preserve);
+        return self();
+    }
+
+    /**
+     * Set the Netty Connector HTTP redirect controller.
+     * The property {@link NettyClientProperties#HTTP_REDIRECT_CONTROLLER} has precedence over this setting.
+     *
+     * @param controller the HTTP redirect controller.
+     * @return updated configuration.
+     */
+    public N redirectController(NettyHttpRedirectController controller) {
+        redirectController.set(controller);
+        return self();
+    }
+
+    @SuppressWarnings("unchecked")
+    protected N self() {
+        return (N) this;
+    }
+
+    abstract static class ReadWrite<N extends ReadWrite<N>>
+            extends NettyConnectorConfiguration<N>
+            implements ConnectorConfiguration.Read<N> {
+
+        @Override
+        public <X extends ConnectorConfiguration<?>> void setNonEmpty(X otherCC) {
+            NettyConnectorConfiguration<?> other = (NettyConnectorConfiguration<?>) otherCC;
+            ConnectorConfiguration.Read.super.setNonEmpty(other);
+            this.connectionController.setNonEmpty(other.connectionController);
+            this.redirectController.setNonEmpty(other.redirectController);
+            this.connectionController.setNonEmpty(other.connectionController);
+            this.enableHostnameVerification.setNonEmpty(other.enableHostnameVerification);
+            ((NullableRef<Integer>) this.expect100ContTimeout).setNonEmpty((NullableRef<Integer>) other.expect100ContTimeout);
+            this.filterHeadersForProxy.setNonEmpty(other.filterHeadersForProxy);
+            this.firstHttpHeaderLineLength.setNonEmpty(other.firstHttpHeaderLineLength);
+            ((NullableRef<Boolean>) this.loggingEnabled).setNonEmpty((NullableRef<Boolean>) other.loggingEnabled);
+            this.maxChunkSize.setNonEmpty(other.maxChunkSize);
+            this.maxHeaderSize.setNonEmpty(other.maxHeaderSize);
+            ((NullableRef<Integer>) this.maxPoolIdle).setNonEmpty((NullableRef<Integer>) other.maxPoolIdle);
+            ((NullableRef<Integer>) this.maxPoolSize).setNonEmpty((NullableRef<Integer>) other.maxPoolSize);
+            ((NullableRef<Integer>) this.maxPoolSizeTotal).setNonEmpty((NullableRef<Integer>) other.maxPoolSizeTotal);
+            ((NullableRef<Integer>) this.maxRedirects).setNonEmpty((NullableRef<Integer>) other.maxRedirects);
+            this.preserveMethodOnRedirect.setNonEmpty(other.preserveMethodOnRedirect);
+            this.redirectController.setNonEmpty(other.redirectController);
+        }
+
+        @Override
+        public N init() {
+            Read.super.init();
+            enableSslHostnameVerification(Boolean.TRUE);
+            expect100ContinueTimeout(NettyClientProperties.DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE);
+            filterHeadersForProxy(Boolean.TRUE);
+            initialHttpHeaderLineLength(NettyClientProperties.DEFAULT_INITIAL_LINE_LENGTH);
+            enableLoggingHandler(Boolean.FALSE);
+            maxChunkSize(NettyClientProperties.DEFAULT_CHUNK_SIZE);
+            maxHeaderSize(NettyClientProperties.DEFAULT_HEADER_SIZE);
+            // either from Jersey config, or default
+            maxTotalConnections(DEFAULT_MAX_POOL_SIZE_TOTAL);
+            // either from Jersey config, or default
+            maxPoolIdle.set(DEFAULT_MAX_POOL_IDLE);
+            // either from system property, or from Jersey config, or default
+            maxPoolSize.set(HTTP_KEEPALIVE ? MAX_POOL_SIZE : DEFAULT_MAX_POOL_SIZE);
+            maxRedirects(DEFAULT_MAX_REDIRECTS);
+            preserveMethodOnRedirect(Boolean.TRUE);
+            return self();
+        }
+
+        /**
+         * Get the preset {@link NettyConnectionController} or create an instance of the default one, if not preset.
+         * @return the {@link NettyConnectionController} instance.
+         */
+        /* package */ NettyConnectionController connectionController() {
+            return connectionController.isPresent() ? connectionController.get() : new NettyConnectionController();
+        }
+
+        /**
+         * Update {@link #expect100ContinueTimeout(int) expect 100-Continue timeout} based on current http request properties.
+         *
+         * @param clientRequest the current http client request.
+         * @return updated configuration.
+         */
+        /* package */ N expect100ContinueTimeout(ClientRequest clientRequest) {
+            expect100ContTimeout.set(
+                    clientRequest.resolveProperty(
+                            prefixed(NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT), expect100ContTimeout.get()));
+            return this.self();
+        }
+
+        /**
+         * Return value of {@link #enableSslHostnameVerification(boolean)} setting either from the configuration of from the
+         * HTTP client request properties. The default is {@code true}.
+         *
+         * @param properties the HTTP client request properties.
+         * @return the value of SSL hostname verification setting.
+         */
+        /* package */ boolean isSslHostnameVerificationEnabled(Map<String, Object> properties) {
+            return ClientProperties.getValue(properties,
+                    prefixed(NettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION),
+                    enableHostnameVerification.get());
+        }
+
+        /**
+         * Update {@link #maxRedirects(int)} value from the HTTP Client request.
+         * @param request the HTTP Client request.
+         * @return maximum redirects value.
+         */
+        /* package */ int maxRedirects(ClientRequest request) {
+            maxRedirects.set(
+                    request.resolveProperty(prefixed(NettyClientProperties.MAX_REDIRECTS), maxRedirects.get()));
+            return maxRedirects.get();
+        }
+
+        /**
+         * Update the {@link #preserveMethodOnRedirect(boolean) preservation} of HTTP method during HTTP redirect
+         * by HTTP client request properties.
+         *
+         * @param request HTTP client request.
+         * @return the value of preservation.
+         */
+        /* package */ boolean preserveMethodOnRedirect(ClientRequest request) {
+            preserveMethodOnRedirect.set(
+                    request.resolveProperty(
+                            prefixed(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT), preserveMethodOnRedirect.get()));
+            return preserveMethodOnRedirect.get();
+        }
+
+        /**
+         * Get the instance of preset {@link NettyHttpRedirectController} either from configuration,
+         * or from the HTTP client request, or the default if non set.
+         * @param request the HTTP client request.
+         * @return an instance of {@link NettyHttpRedirectController}.
+         */
+        /* package */ NettyHttpRedirectController redirectController(ClientRequest request) {
+            NettyHttpRedirectController customRedirectController =
+                    request.resolveProperty(
+                            prefixed(NettyClientProperties.HTTP_REDIRECT_CONTROLLER), NettyHttpRedirectController.class);
+            if (customRedirectController == null) {
+                customRedirectController = redirectController.get();
+            }
+            if (customRedirectController == null) {
+                customRedirectController = new NettyHttpRedirectController();
+            }
+
+            return customRedirectController;
+        }
+
+        /**
+         * <p>
+         *  Return a new instance of configuration updated by the merged settings from this and client properties.
+         *  Only properties unresolved during the request are updated.
+         * </p><p>
+         *  {@code This} is meant to be settings from the connector.
+         *  The priorities should go DEFAULTS -> CONNECTOR -> CLIENT -> REQUEST.
+         * </p>
+         *
+         * @param client the REST client.
+         * @return a new instance of configuration.
+         */
+        /* package */ N fromClient(Client client) {
+            final Map<String, Object> properties = client.getConfiguration().getProperties();
+            final N clientConfiguration = copyFromClient(client.getConfiguration());
+
+            final Object threadPoolSize = properties.get(clientConfiguration.prefixed(ClientProperties.ASYNC_THREADPOOL_SIZE));
+            if (threadPoolSize instanceof Integer && (Integer) threadPoolSize > 0) {
+                clientConfiguration.asyncThreadPoolSize((Integer) threadPoolSize);
+            }
+
+            final Object maxPoolSizeTotalProperty = properties.get(
+                    clientConfiguration.prefixed(NettyClientProperties.MAX_CONNECTIONS_TOTAL));
+            final Object maxPoolIdleProperty = properties.get(
+                    clientConfiguration.prefixed(NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT));
+            final Object maxPoolSizeProperty = properties.get(
+                    clientConfiguration.prefixed(NettyClientProperties.MAX_CONNECTIONS));
+
+            if (maxPoolSizeTotalProperty != null) {
+                clientConfiguration.maxPoolSizeTotal.set((Integer) maxPoolSizeTotalProperty);
+            }
+
+            if (maxPoolIdleProperty != null) {
+                clientConfiguration.maxPoolIdle.set((Integer) maxPoolIdleProperty);
+            }
+
+            if (maxPoolSizeProperty != null) {
+                clientConfiguration.maxPoolSize.set((Integer) maxPoolSizeProperty);
+            }
+
+            if (clientConfiguration.maxPoolSizeTotal.get() < 0) {
+                throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_TOTAL(maxPoolSizeTotal.get()));
+            }
+
+            if (clientConfiguration.maxPoolSize.get() < 0) {
+                throw new ProcessingException(LocalizationMessages.WRONG_MAX_POOL_SIZE(maxPoolSize.get()));
+            }
+
+            final Object logging = properties.get(clientConfiguration.prefixed(NettyClientProperties.LOGGING_ENABLED));
+            if (logging instanceof Boolean) {
+                clientConfiguration.loggingEnabled.set((Boolean) logging);
+            } else if (logging instanceof String) {
+                clientConfiguration.loggingEnabled.set(Boolean.valueOf((String) logging));
+            }
+
+            return clientConfiguration;
+        }
+
+        /**
+         * <p>
+         *  Return a new instance of configuration updated by the merged settings from this and HTTP client request properties.
+         *  Only properties unresolved during the request are updated.
+         * </p><p>
+         *  {@code This} is meant to be settings from the connector.
+         *  The priorities should go DEFAULTS -> CONNECTOR -> CLIENT -> REQUEST.
+         * </p>
+
+         * @param request the HTTP client request.
+         * @return a new instance of configuration.
+         */
+        /* package */ N fromRequest(ClientRequest request) {
+            final N requestConfiguration = copyFromRequest(request);
+
+            final Boolean logging = request.resolveProperty(prefixed(NettyClientProperties.LOGGING_ENABLED), Boolean.class);
+            if (logging != null) {
+                requestConfiguration.loggingEnabled.set(logging);
+            }
+
+            return requestConfiguration;
+        }
+
+
+        /**
+         * Create an instance of {@link HttpClientCodec} based on preset settings {@link #initialHttpHeaderLineLength(int)},
+         * {@link #maxHeaderSize} and {@link #maxChunkSize}. The settings can be preset in the configuration or
+         * on the HTTP client request.
+         *
+         * @param properties The HTTP client request properties.
+         * @return the {@link HttpClientCodec} instance.
+         */
+        /* package */ HttpClientCodec createHttpClientCodec(Map<String, Object> properties) {
+            firstHttpHeaderLineLength.set(ClientProperties.getValue(properties,
+                    prefixed(NettyClientProperties.MAX_INITIAL_LINE_LENGTH), firstHttpHeaderLineLength.get()));
+            maxHeaderSize.set(
+                    ClientProperties.getValue(properties, prefixed(NettyClientProperties.MAX_HEADER_SIZE), maxHeaderSize.get()));
+            maxChunkSize.set(
+                    ClientProperties.getValue(properties, prefixed(NettyClientProperties.MAX_CHUNK_SIZE), maxChunkSize.get()));
+
+            return new HttpClientCodec(firstHttpHeaderLineLength.get(), maxHeaderSize.get(), maxChunkSize.get());
+        }
+
+        /**
+         * Create an instance of {@link ProxyHandler} based on HTTP request URI's port and address,
+         * the preset proxy {@link #proxyUri(URI) uri}, {@link #proxyUserName(String) username},
+         * and {@link #proxyPassword(String) password}.
+         *
+         * Can filter headers {@link #filterHeadersForProxy(boolean)}.
+         *
+         * @param clientProxy the Jersey {@link ClientProxy} instance.
+         * @param jerseyRequest the HTTP client request containing HTTP headers to be filtered.
+         * @return a Netty {@link ProxyHandler} instance.
+         */
+        /* package */ ProxyHandler createProxyHandler(ClientProxy clientProxy, ClientRequest jerseyRequest) {
+            final URI u = clientProxy.uri();
+            InetSocketAddress proxyAddr = new InetSocketAddress(u.getHost(), u.getPort() == -1 ? 8080 : u.getPort());
+
+            filterHeadersForProxy.set(jerseyRequest
+                    .resolveProperty(prefixed(NettyClientProperties.FILTER_HEADERS_FOR_PROXY), filterHeadersForProxy.get()));
+            HttpHeaders httpHeaders = NettyConnector.setHeaders(
+                    jerseyRequest, new DefaultHttpHeaders(), Boolean.TRUE.equals(filterHeadersForProxy.get()));
+
+            ProxyHandler proxy = clientProxy.userName() == null ? new HttpProxyHandler(proxyAddr, httpHeaders)
+                    : new HttpProxyHandler(proxyAddr, clientProxy.userName(), clientProxy.password(), httpHeaders);
+            if (connectTimeout.get() > 0) {
+                proxy.setConnectTimeoutMillis(connectTimeout.get());
+            }
+
+            return proxy;
+        }
+    }
+
+}
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorProvider.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorProvider.java
index cdf90dd..46f27a7 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorProvider.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyConnectorProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2025 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,8 +51,53 @@
 @Beta
 public class NettyConnectorProvider implements ConnectorProvider {
 
+    private final Config config;
+
+    public NettyConnectorProvider() {
+        this.config = config();
+    }
+
+    private NettyConnectorProvider(Config config) {
+        this.config = config;
+    }
+
     @Override
     public Connector getConnector(Client client, Configuration runtimeConfig) {
-        return new NettyConnector(client);
+        return new NettyConnector(client, config.rw());
+    }
+
+    /**
+     * Instantiate a builder allowing to configure the NettyConnectorProvider.
+     * @return a new {@link Config} instance.
+     */
+    public static Config config() {
+        return new Config();
+    }
+
+    public static final class Config extends NettyConnectorConfiguration<Config> {
+
+        private Config() {
+        }
+
+        /* package */ RW rw() {
+            RW rw = new RW();
+            rw.setNonEmpty(this);
+            return rw;
+        }
+
+        public NettyConnectorProvider build() {
+            return new NettyConnectorProvider(this);
+        }
+
+        /* package */ static class RW extends ReadWrite<RW> {
+            @Override
+            public RW instance() {
+                return new RW();
+            }
+
+            public RW me() {
+                return this;
+            }
+        }
     }
 }
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttpRedirectController.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttpRedirectController.java
index 670be3e..6c4c41c 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttpRedirectController.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/NettyHttpRedirectController.java
@@ -34,9 +34,15 @@
  */
 public class NettyHttpRedirectController {
 
+    private NettyConnectorProvider.Config.RW configuration;
+
+    /* package */ void init(NettyConnectorProvider.Config.RW configuration) {
+        this.configuration = configuration;
+    }
+
     /**
      * Configure the HTTP request after HTTP Redirect response has been received.
-     * By default, the HTTP POST request is transformed into HTTP GET for status 301 & 302.
+     * By default, the HTTP POST request is not transformed into HTTP GET for status 301 & 302.
      * Also, HTTP Headers described by RFC 9110 Section 15.4 are removed from the new HTTP Request.
      *
      * @param request The new {@link ClientRequest} to be sent to the redirected URI.
@@ -44,7 +50,7 @@
      * @return {@code true} when the new request should be sent.
      */
     public boolean prepareRedirect(ClientRequest request, ClientResponse response) {
-        final Boolean keepMethod = request.resolveProperty(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, Boolean.TRUE);
+        final boolean keepMethod = configuration.preserveMethodOnRedirect(request);
 
         if (Boolean.FALSE.equals(keepMethod) && request.getMethod().equals(HttpMethod.POST)) {
             switch (response.getStatus()) {
diff --git a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/NettyEntityWriter.java b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/NettyEntityWriter.java
index bcd3fd8..f758f95 100644
--- a/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/NettyEntityWriter.java
+++ b/connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/internal/NettyEntityWriter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2025 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,9 @@
 
 package org.glassfish.jersey.netty.connector.internal;
 
+import io.netty.buffer.ByteBuf;
 import io.netty.channel.Channel;
 import io.netty.handler.stream.ChunkedInput;
-import org.glassfish.jersey.client.ClientProperties;
 import org.glassfish.jersey.client.ClientRequest;
 import org.glassfish.jersey.client.RequestEntityProcessing;
 
@@ -28,6 +28,7 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
 
 /**
  * The Entity Writer is used to write entity in Netty. One implementation is delayed,
@@ -60,7 +61,7 @@
 
     /**
      * Flushes the writen objects. Can throw IOException.
-     * @throws IOException
+     * @throws IOException exception.
      */
     void flush() throws IOException;
 
@@ -68,7 +69,7 @@
      * Get the netty Chunked Input to be written.
      * @return The Chunked input instance
      */
-    ChunkedInput getChunkedInput();
+    ChunkedInput<ByteBuf> getChunkedInput();
 
     /**
      * Get the {@link OutputStream} used to write an entity
@@ -78,20 +79,20 @@
 
     /**
      * Get the length of the entity written to the {@link OutputStream}
-     * @return
+     * @return length of the entity.
      */
     long getLength();
 
     /**
-     * Return Type of
-     * @return
+     * Return Type of the {@link NettyEntityWriter}.
+     * @return type of the writer.
      */
     Type getType();
 
-    static NettyEntityWriter getInstance(ClientRequest clientRequest, Channel channel) {
+    static NettyEntityWriter getInstance(
+            ClientRequest clientRequest, Channel channel, Supplier<RequestEntityProcessing> requestEntityProcessingSupplier) {
         final long lengthLong = clientRequest.getLengthLong();
-        final RequestEntityProcessing entityProcessing = clientRequest.resolveProperty(
-                ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.class);
+        final RequestEntityProcessing entityProcessing = requestEntityProcessingSupplier.get();
 
         if ((entityProcessing == null && lengthLong == -1) || entityProcessing == RequestEntityProcessing.CHUNKED) {
             return new DirectEntityWriter(channel, Type.CHUNKED);
@@ -129,7 +130,7 @@
         }
 
         @Override
-        public ChunkedInput getChunkedInput() {
+        public ChunkedInput<ByteBuf> getChunkedInput() {
             return stream;
         }
 
@@ -203,7 +204,7 @@
         }
 
         @Override
-        public ChunkedInput getChunkedInput() {
+        public ChunkedInput<ByteBuf> getChunkedInput() {
             return writer.getChunkedInput();
         }
 
@@ -226,7 +227,7 @@
         private class DelayedOutputStream extends OutputStream {
             private final List<WriteAction> actions = new ArrayList<>();
             private int writeLen = 0;
-            private AtomicBoolean streamFlushed = new AtomicBoolean(false);
+            private final AtomicBoolean streamFlushed = new AtomicBoolean(false);
 
             @Override
             public void write(int b) throws IOException {
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputClosedOnErrorTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputClosedOnErrorTest.java
index 1d0135d..22ce8d3 100644
--- a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputClosedOnErrorTest.java
+++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputClosedOnErrorTest.java
@@ -83,10 +83,11 @@
         ConnectorProvider provider = new ConnectorProvider() {
             @Override
             public Connector getConnector(Client client, Configuration runtimeConfig) {
-                return new NettyConnector(client) {
+                return new NettyConnector(client, NettyConnectorProvider.config().rw()) {
                     @Override
-                    NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel) {
-                        writer.set(super.nettyEntityWriter(clientRequest, channel));
+                    NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel,
+                                                        NettyConnectorProvider.Config.RW config) {
+                        writer.set(super.nettyEntityWriter(clientRequest, channel, config));
                         writerSetLatch.countDown();
                         return new NettyEntityWriter() {
                             private boolean slept = false;
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputWriteErrorSimulationTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputWriteErrorSimulationTest.java
index 78a3448..fed5f61 100644
--- a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputWriteErrorSimulationTest.java
+++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ChunkedInputWriteErrorSimulationTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2024, 2025 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
@@ -223,9 +223,12 @@
         return new ConnectorProvider() {
             @Override
             public Connector getConnector(Client client, Configuration runtimeConfig) {
-                return new NettyConnector(client) {
-                    NettyEntityWriter nettyEntityWriter(ClientRequest clientRequest, Channel channel) {
-                        NettyEntityWriter wrapped = NettyEntityWriter.getInstance(clientRequest, channel);
+                return new NettyConnector(client, NettyConnectorProvider.config().rw()) {
+                    @Override
+                    NettyEntityWriter nettyEntityWriter(
+                            ClientRequest clientRequest, Channel channel, NettyConnectorProvider.Config.RW config) {
+                        NettyEntityWriter wrapped = NettyEntityWriter.getInstance(
+                                clientRequest, channel, () -> config.requestEntityProcessing(clientRequest));
 
                         JerseyChunkedInput chunkedInput = (JerseyChunkedInput) wrapped.getChunkedInput();
                         try {
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ConnectorConfigTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ConnectorConfigTest.java
new file mode 100644
index 0000000..6dd9b03
--- /dev/null
+++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/ConnectorConfigTest.java
@@ -0,0 +1,1683 @@
+/*
+ * Copyright (c) 2025 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.HttpClientCodec;
+import io.netty.handler.codec.http.HttpRequest;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.ClientResponse;
+import org.glassfish.jersey.client.RequestEntityProcessing;
+import org.glassfish.jersey.client.innate.ClientProxy;
+import org.glassfish.jersey.http.HttpHeaders;
+import org.glassfish.jersey.internal.MapPropertiesDelegate;
+import org.glassfish.jersey.internal.util.collection.Ref;
+import org.glassfish.jersey.client.innate.ConnectorConfiguration;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import javax.net.ssl.SSLContext;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.core.Configuration;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class ConnectorConfigTest {
+
+    private static final String PREFIX = "test.";
+
+    private static ClientRequest createRequest(Client client) {
+        return new ClientRequest(URI.create("http://localhost:8080"),
+                (ClientConfig) client.getConfiguration(), new MapPropertiesDelegate()) {
+        };
+    }
+
+    @Test
+    public void testPrefixedConfig() {
+        ConnectorConfig.RW r = new ConnectorConfig.RW();
+        Configuration prefixed, unprefixed;
+
+        unprefixed = r.config();
+        unprefixed.getProperties().put(ClientProperties.CONNECT_TIMEOUT, 1000);
+        prefixed = r.prefix(PREFIX).prefixedConfiguration(unprefixed);
+        Assertions.assertNull(prefixed.getProperty(ClientProperties.CONNECT_TIMEOUT));
+        Assertions.assertNull(prefixed.getProperties().get(ClientProperties.CONNECT_TIMEOUT));
+
+        unprefixed = r.config();
+        unprefixed.getProperties().put(PREFIX + ClientProperties.CONNECT_TIMEOUT, 2000);
+        prefixed = r.prefix(PREFIX).prefixedConfiguration(unprefixed);
+        Assertions.assertEquals(2000, prefixed.getProperty(ClientProperties.CONNECT_TIMEOUT));
+        Assertions.assertEquals(2000, prefixed.getProperties().get(ClientProperties.CONNECT_TIMEOUT));
+
+        unprefixed = r.config();
+        prefixed = r.prefix(PREFIX).prefixedConfiguration(unprefixed);
+        unprefixed.getProperties().put(ClientProperties.CONNECT_TIMEOUT, 2000);
+        prefixed.getProperties().putAll(unprefixed.getProperties());
+        Assertions.assertNull(prefixed.getProperty(ClientProperties.CONNECT_TIMEOUT));
+        Assertions.assertNull(prefixed.getProperties().get(ClientProperties.CONNECT_TIMEOUT));
+
+        unprefixed = r.config();
+        prefixed = r.prefix(PREFIX).prefixedConfiguration(unprefixed);
+        unprefixed.getProperties().put(PREFIX + ClientProperties.CONNECT_TIMEOUT, 2000);
+        prefixed.getProperties().putAll(unprefixed.getProperties());
+        Assertions.assertEquals(2000, prefixed.getProperty(ClientProperties.CONNECT_TIMEOUT));
+        Assertions.assertEquals(2000, prefixed.getProperties().get(ClientProperties.CONNECT_TIMEOUT));
+
+        unprefixed = r.config();
+        prefixed = r.prefix(PREFIX).prefixedConfiguration(unprefixed);
+        prefixed.getProperties().put(PREFIX + ClientProperties.PROXY_USERNAME, "USERNAME");
+        Assertions.assertEquals("USERNAME", prefixed.getProperty(ClientProperties.PROXY_USERNAME));
+        prefixed.getProperties().put(ClientProperties.PROXY_PASSWORD, "PASSWORD");
+        Assertions.assertNull(prefixed.getProperty(ClientProperties.PROXY_PASSWORD));
+    }
+
+    @Test
+    public void testAsyncThreadPoolSize() {
+        AtomicInteger result = new AtomicInteger(0);
+        class RWAsync extends RW {
+            @Override
+            public Integer asyncThreadPoolSize() {
+                result.set(super.asyncThreadPoolSize());
+                return super.asyncThreadPoolSize();
+            }
+
+            @Override
+            public RWAsync instance() {
+                return new RWAsync();
+            }
+        }
+
+        Client client = ClientBuilder.newClient();
+        NettyConnectorProvider.Config.RW rw0 = new RWAsync().asyncThreadPoolSize(10);
+        new NettyConnector(client, rw0);
+        Assertions.assertEquals(10, result.get());
+
+        result.set(0);
+        NettyConnectorProvider.Config.RW rw1 = new RWAsync().asyncThreadPoolSize(20);
+        client.property(ClientProperties.CONNECTOR_CONFIGURATION, rw1);
+        new NettyConnector(client, rw0);
+        Assertions.assertEquals(20, result.get());
+
+        result.set(0);
+        client.property(ClientProperties.ASYNC_THREADPOOL_SIZE, 30);
+        new NettyConnector(client, rw0);
+        Assertions.assertEquals(30, result.get());
+    }
+
+    @Test
+    public void testConnectTimeout() {
+        final AtomicInteger result = new AtomicInteger(0);
+        class RWConnect extends NettyConnectorProvider.Config.RW {
+            @Override
+            public int connectTimeout() {
+                result.set(super.connectTimeout());
+                throw new IllegalStateException();
+            }
+
+            @Override
+            public RWConnect instance() {
+                return new RWConnect();
+            }
+        }
+        new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000)).request().apply();
+        Assertions.assertEquals(1000, result.get());
+
+        result.set(0);
+        new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000))
+                .client(c -> c.property(ClientProperties.CONNECT_TIMEOUT, 3000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000)))
+                .request().apply();
+        Assertions.assertEquals(3000, result.get());
+
+        result.set(0);
+        new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000))
+                .client(c -> c.property(ClientProperties.CONNECT_TIMEOUT, 3000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(4000)))
+                .apply();
+        Assertions.assertEquals(3000, result.get());
+
+        result.set(0);
+        new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(4000)))
+                .apply();
+        Assertions.assertEquals(4000, result.get());
+
+        result.set(0);
+        new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(4000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECT_TIMEOUT, 5000))
+                .apply();
+        Assertions.assertEquals(5000, result.get());
+
+        result.set(0);
+        new TestClient(new RWConnect().prefix(PREFIX)).rw(rw -> rw.connectTimeout(1000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWConnect().connectTimeout(2000).prefix(PREFIX)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWConnect().connectTimeout(4000)))
+                .apply();
+        Assertions.assertEquals(2000, result.get());
+
+        result.set(0);
+        new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000).prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWConnect().connectTimeout(4000).prefix(PREFIX)))
+                .request(r -> r.setProperty(ClientProperties.CONNECT_TIMEOUT, 5000))
+                .apply();
+        Assertions.assertEquals(4000, result.get());
+    }
+
+    @Test
+    public void testExpect100Continue() {
+        final AtomicReference<Boolean> result = new AtomicReference<>();
+        final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>();
+        class RWExpect extends RW {
+
+            @Override
+            public Boolean expect100Continue(ClientRequest request) {
+                result.set(super.expect100Continue(request));
+                return result.get();
+            }
+
+            @Override
+            public int connectTimeout() {
+                config.set(this);
+                return super.connectTimeout();
+            }
+
+            @Override
+            public RWExpect instance() {
+                return new RWExpect();
+            }
+        }
+
+        Request req = new TestClient(new RWExpect()).request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertNull(result.get());
+
+        result.set(null);
+        req = new TestClient(new RWExpect().expect100Continue(true)).request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(true, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWExpect().expect100Continue(true).prefix(PREFIX))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.EXPECT_100_CONTINUE, false))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(false, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWExpect().expect100Continue(true).prefix(PREFIX))
+                .request(r -> r.setProperty(
+                        PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWExpect().expect100Continue(false)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(false, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWExpect())
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWExpect().expect100Continue(true)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWExpect().expect100Continue(false)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(false, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWExpect().prefix(PREFIX))
+                .client(c -> c.property(PREFIX + ClientProperties.EXPECT_100_CONTINUE, true))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWExpect().expect100Continue(false)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(true, result.get());
+    }
+
+    @Test
+    public void testExpect100ContinueThreshold() {
+        final AtomicReference<Long> result = new AtomicReference<>();
+        final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>();
+        class RWExpect extends RW {
+
+            @Override
+            public long expect100ContinueThreshold(ClientRequest request) {
+                result.set(super.expect100ContinueThreshold(request));
+                return result.get();
+            }
+
+            @Override
+            public int connectTimeout() {
+                config.set(this);
+                return super.connectTimeout();
+            }
+
+            @Override
+            public RWExpect instance() {
+                return new RWExpect();
+            }
+        }
+
+        Request req = new TestClient(new RWExpect()).request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(ClientProperties.DEFAULT_EXPECT_100_CONTINUE_THRESHOLD_SIZE, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWExpect().expect100ContinueThreshold(1000)).request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(1000, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWExpect().expect100ContinueThreshold(1000).prefix(PREFIX))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 2000))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(2000, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWExpect().expect100ContinueThreshold(1000).prefix(PREFIX))
+                .request(r -> r.setProperty(
+                        PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWExpect().expect100ContinueThreshold(2000)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(2000, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWExpect())
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWExpect().expect100ContinueThreshold(1000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWExpect().expect100ContinueThreshold(2000)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(2000, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWExpect().prefix(PREFIX))
+                .client(c -> c.property(PREFIX + ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE, 1000))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWExpect().expect100ContinueThreshold(2000)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(1000, result.get());
+    }
+
+    @Test
+    public void testExpect100ContinueTimeout() {
+        final AtomicReference<Integer> result = new AtomicReference<>();
+        final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>();
+        class RWExpect extends RW {
+
+            @Override
+            public NettyConnectorProvider.Config.RW expect100ContinueTimeout(ClientRequest request) {
+                super.expect100ContinueTimeout(request);
+                result.set(this.expect100ContTimeout.get());
+                return this;
+            }
+
+            @Override
+            public int connectTimeout() {
+                config.set(this);
+                return super.connectTimeout();
+            }
+
+            @Override
+            public RWExpect instance() {
+                return new RWExpect();
+            }
+        }
+
+        Request req = new TestClient(new RWExpect()).request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(NettyClientProperties.DEFAULT_EXPECT_100_CONTINUE_TIMEOUT_VALUE, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWExpect().expect100ContinueTimeout(1000)).request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(1000, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWExpect().expect100ContinueTimeout(1000).prefix(PREFIX))
+                .request(r -> r.setProperty(PREFIX + NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 2000))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(2000, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWExpect().expect100ContinueTimeout(1000).prefix(PREFIX))
+                .request(r -> r.setProperty(
+                        PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWExpect().expect100ContinueTimeout(2000)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, (HttpRequest) null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(2000, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWExpect())
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWExpect().expect100ContinueTimeout(1000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWExpect().expect100ContinueTimeout(2000)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(2000, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWExpect().prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.EXPECT_100_CONTINUE_TIMEOUT, 1000))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWExpect().expect100ContinueTimeout(2000)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            new Expect100ContinueConnectorExtension(config.get()).invoke(req.request, null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(1000, result.get());
+    }
+
+    @Test
+    public void testFollowRedirects() {
+        final AtomicReference<Boolean> result = new AtomicReference<>();
+        final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>();
+        class RWFollow extends RW {
+
+            @Override
+            public boolean followRedirects() {
+                result.set(super.followRedirects());
+                return result.get();
+            }
+
+            @Override
+            public int connectTimeout() {
+                config.set(this);
+                return super.connectTimeout();
+            }
+
+            @Override
+            public RWFollow instance() {
+                return new RWFollow();
+            }
+        }
+
+        Request req;
+        req = new TestClient(new RWFollow()).request().apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        config.get().followRedirects();
+        Assertions.assertEquals(new RWFollow().copy().followRedirects(), result.get());
+
+        result.set(null);
+        req = new TestClient(new RWFollow().followRedirects(false)).request().apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        config.get().followRedirects();
+        Assertions.assertEquals(false, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWFollow().followRedirects(false).prefix(PREFIX))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.FOLLOW_REDIRECTS, true))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        config.get().followRedirects();
+        Assertions.assertEquals(true, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWFollow().followRedirects(false).prefix(PREFIX))
+                .request(r -> r.setProperty(
+                        PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWFollow().followRedirects(true)))
+                .apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        config.get().followRedirects();
+        Assertions.assertEquals(true, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWFollow())
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWFollow().followRedirects(false)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWFollow().followRedirects(true)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        config.get().followRedirects();
+        Assertions.assertEquals(true, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWFollow().prefix(PREFIX))
+                .client(c -> c.property(PREFIX + ClientProperties.FOLLOW_REDIRECTS, false))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWFollow().followRedirects(true)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        config.get().followRedirects();
+        Assertions.assertEquals(false, result.get());
+    }
+
+    @Test
+    public void testProxy() {
+        final AtomicReference<ClientProxy> result = new AtomicReference<>();
+        final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>();
+        class RWProxy extends RW {
+
+            @Override
+            public Optional<ClientProxy> proxy(ClientRequest request, URI requestUri) {
+                Optional<ClientProxy> proxy = super.proxy(request, requestUri);
+                result.set(proxy.orElse(null));
+                return proxy;
+            }
+
+            @Override
+            public RWProxy instance() {
+                return new RWProxy();
+            }
+        }
+        String proxyUri = "http://proxy.org:8080";
+        String userName = "USERNAME";
+        String password = "PASSWORD";
+
+        new TestClient(new RWProxy()).request().apply();
+        Assertions.assertNull(result.get());
+
+        result.set(null);
+        new TestClient(new RWProxy().proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password)).request().apply();
+        Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString());
+        Assertions.assertEquals(userName, result.get().userName());
+        Assertions.assertEquals(password, result.get().password());
+
+        result.set(null);
+        new TestClient(new RWProxy().prefix(PREFIX).proxyUri(proxyUri).proxyUserName("XXX").proxyPassword(password))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.PROXY_USERNAME, userName))
+                .request(r -> r.setProperty(ClientProperties.PROXY_PASSWORD, "XXX"))
+                .apply();
+        Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString());
+        Assertions.assertEquals(userName, result.get().userName());
+        Assertions.assertEquals(password, result.get().password());
+
+        result.set(null);
+        new TestClient(new RWProxy().prefix(PREFIX).proxyUri(proxyUri).proxyUserName("XXX").proxyPassword(password))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWProxy().proxyUserName(userName).proxyPassword(null)))
+                .apply();
+        Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString());
+        Assertions.assertEquals(userName, result.get().userName());
+        Assertions.assertNull(result.get().password());
+
+        result.set(null);
+        new TestClient(new RWProxy().prefix(PREFIX)
+                .proxy(new java.net.Proxy(java.net.Proxy.Type.HTTP,
+                        new InetSocketAddress(proxyUri.split("g:")[0] + "g", Integer.parseInt(proxyUri.split("g:")[1])))))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWProxy().proxyUserName(userName).proxyPassword(password).prefix(PREFIX)))
+                .apply();
+        Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString());
+        Assertions.assertEquals(userName, result.get().userName());
+        Assertions.assertEquals(password, result.get().password());
+    }
+
+    @Test
+    public void testReadTimeout() {
+        final AtomicInteger result = new AtomicInteger(0);
+        class RWRead extends NettyConnectorProvider.Config.RW {
+
+            @Override
+            public int readTimeout() {
+                result.set(super.readTimeout());
+                throw new IllegalStateException();
+            }
+
+            @Override
+            public RWRead instance() {
+                return new RWRead();
+            }
+        }
+        new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000)).request().apply();
+        Assertions.assertEquals(1000, result.get());
+
+        result.set(0);
+        new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000))
+                .client(c -> c.property(ClientProperties.READ_TIMEOUT, 3000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000)))
+                .request().apply();
+        Assertions.assertEquals(3000, result.get());
+
+        result.set(0);
+        new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000))
+                .client(c -> c.property(ClientProperties.READ_TIMEOUT, 3000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().connectTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(4000)))
+                .apply();
+        Assertions.assertEquals(3000, result.get());
+
+        result.set(0);
+        new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(4000)))
+                .apply();
+        Assertions.assertEquals(4000, result.get());
+
+        result.set(0);
+        new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(4000)))
+                .request(r -> r.setProperty(ClientProperties.READ_TIMEOUT, 5000))
+                .apply();
+        Assertions.assertEquals(5000, result.get());
+
+        result.set(0);
+        new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000).prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWRead().readTimeout(2000).prefix(PREFIX)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWRead().readTimeout(4000)))
+                .apply();
+        Assertions.assertEquals(2000, result.get());
+
+        result.set(0);
+        new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000).prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWRead().readTimeout(4000).prefix(PREFIX)))
+                .request(r -> r.setProperty(ClientProperties.READ_TIMEOUT, 5000))
+                .apply();
+        Assertions.assertEquals(4000, result.get());
+    }
+
+    @Test
+    public void testRequestEntityProcessing() {
+        final AtomicReference<RequestEntityProcessing> result = new AtomicReference<>();
+        final AtomicReference<RW> config = new AtomicReference<>();
+        class RWRP extends RW {
+
+            @Override
+            public RequestEntityProcessing requestEntityProcessing(ClientRequest request) {
+               result.set(super.requestEntityProcessing(request));
+               return result.get();
+            }
+
+            @Override
+            public RWRP instance() {
+                config.set(new RWRP());
+                return (RWRP) config.get();
+            }
+        }
+
+        Request req;
+        req = new TestClient(new RWRP().requestEntityProcessing(RequestEntityProcessing.CHUNKED))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        try {
+            req.connector.nettyEntityWriter(req.request, null, config.get());
+        } catch (NullPointerException ignore) {
+        }
+        Assertions.assertEquals(RequestEntityProcessing.CHUNKED, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWRP().prefix(PREFIX).requestEntityProcessing(RequestEntityProcessing.CHUNKED))
+                .request(r -> r.setEntity(PREFIX))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWRP().requestEntityProcessing(RequestEntityProcessing.BUFFERED).prefix(PREFIX)))
+                .apply();
+        try {
+            req.connector.nettyEntityWriter(req.request, null, config.get());
+        } catch (NullPointerException ignore) {
+        }
+        Assertions.assertEquals(RequestEntityProcessing.BUFFERED, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWRP().prefix(PREFIX).requestEntityProcessing(RequestEntityProcessing.CHUNKED))
+                .request(r -> r.setEntity(PREFIX))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.REQUEST_ENTITY_PROCESSING,
+                        RequestEntityProcessing.BUFFERED))
+                .apply();
+        try {
+            req.connector.nettyEntityWriter(req.request, null, config.get());
+        } catch (NullPointerException ignore) {
+        }
+        Assertions.assertEquals(RequestEntityProcessing.BUFFERED, result.get());
+    }
+
+    @Test
+    public void testSniHostName() {
+        final String sniHost = "sun.oracle.com";
+        final AtomicReference<String> sniRef = new AtomicReference<>();
+        final NettyConnectionController controller = new NettyConnectionController() {
+            @Override
+            public String getConnectionGroup(ClientRequest clientRequest, URI uri, String hostName, int port) {
+                sniRef.set(hostName);
+                return super.getConnectionGroup(clientRequest, uri, hostName, port);
+            }
+        };
+
+        final class RWSni extends RW {
+            @Override
+            public RWSni instance() {
+                return new RWSni();
+            }
+        }
+
+        new TestClient(new RWSni().connectionController(controller))
+                .request(r -> r.getRequestHeaders().add(HttpHeaders.HOST, sniHost))
+                .apply();
+        Assertions.assertEquals(sniHost, sniRef.get());
+
+        new TestClient(new RWSni().connectionController(controller).sniHostName(sniHost))
+                .request(r -> r.getRequestHeaders().add(HttpHeaders.HOST, "moon.oracle.com"))
+                .apply();
+        Assertions.assertEquals(sniHost, sniRef.get());
+
+        new TestClient(new RWSni().connectionController(controller).prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        NettyConnectorProvider.config().sniHostName("moon.oracle.com")))
+                .request(r -> r.getRequestHeaders().add(HttpHeaders.HOST, sniHost))
+                .apply();
+        Assertions.assertEquals(sniHost, sniRef.get());
+
+        new TestClient(new RWSni().connectionController(controller).prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        NettyConnectorProvider.config().sniHostName(sniHost).prefix(PREFIX)))
+                .request(r -> r.getRequestHeaders().add(HttpHeaders.HOST, "moon.oracle.com"))
+                .apply();
+        Assertions.assertEquals(sniHost, sniRef.get());
+
+        new TestClient(new RWSni().connectionController(controller).prefix(PREFIX))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        NettyConnectorProvider.config().sniHostName("moon.oracle.com")))
+                .request(r -> r.getRequestHeaders().add(HttpHeaders.HOST, sniHost))
+                .apply();
+        Assertions.assertEquals(sniHost, sniRef.get());
+
+        new TestClient(new RWSni().connectionController(controller).prefix(PREFIX))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        NettyConnectorProvider.config().sniHostName(sniHost).prefix(PREFIX)))
+                .request(r -> r.getRequestHeaders().add(HttpHeaders.HOST, "moon.oracle.com"))
+                .apply();
+        Assertions.assertEquals(sniHost, sniRef.get());
+    }
+
+    @Test
+    public void testSslContext() {
+        final SSLContext testContext = new SSLContext(null, null, null){};
+        final AtomicReference<SSLContext> result = new AtomicReference<>();
+        final AtomicReference<RW> config = new AtomicReference<>();
+        class RWSsl extends RW {
+
+            @Override
+            public SSLContext sslContext(Client client, ClientRequest request) {
+                result.set(super.sslContext(client, request));
+                return result.get();
+            }
+
+            @Override
+            public RWSsl instance() {
+                RWSsl rw = new RWSsl();
+                config.set(rw);
+                return rw;
+            }
+        }
+
+        Request req;
+        req = new TestClient(new RWSsl().sslContextSupplier(() -> testContext)).request().apply();
+        config.get().sslContext(req.client, req.request);
+        Assertions.assertEquals(testContext, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWSsl().prefix(PREFIX))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.SSL_CONTEXT_SUPPLIER,
+                        (Supplier<SSLContext>) () -> testContext)).apply();
+        config.get().sslContext(req.client, req.request);
+        Assertions.assertEquals(testContext, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWSsl().prefix(PREFIX))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWSsl().prefix(PREFIX).sslContextSupplier(() -> testContext))).apply();
+        config.get().sslContext(req.client, req.request);
+        Assertions.assertEquals(testContext, result.get());
+    }
+
+    @Test
+    public void testConnectionController() {
+        final NettyConnectionController connectionController = new NettyConnectionController();
+        final AtomicReference<NettyConnectionController> result = new AtomicReference<>();
+
+        class RWController extends RW {
+
+            @Override
+            NettyConnectionController connectionController() {
+                result.set(super.connectionController());
+                return result.get();
+            }
+
+            @Override
+            public RWController instance() {
+                return new RWController();
+            }
+        }
+
+        new TestClient(new RWController().connectionController(connectionController)).request().apply();
+        Assertions.assertEquals(connectionController, result.get());
+
+        result.set(null);
+        new TestClient(new RWController().prefix(PREFIX).connectionController(new NettyConnectionController()))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWController().connectionController(connectionController).prefix(PREFIX))).apply();
+        Assertions.assertEquals(connectionController, result.get());
+    }
+
+    @Test
+    public void testEnableSslHostnameVerification() {
+        final AtomicReference<Boolean> result = new AtomicReference<>();
+        final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>();
+        class RWEnabled extends RW {
+
+            @Override
+            boolean isSslHostnameVerificationEnabled(Map<String, Object> properties) {
+               result.set(super.isSslHostnameVerificationEnabled(properties));
+               return result.get();
+            }
+
+            @Override
+            public RWEnabled instance() {
+                RWEnabled enabled = new RWEnabled();
+                config.set(enabled);
+                return enabled;
+            }
+        }
+
+        Request req;
+        req = new TestClient(new RWEnabled()).request().apply();
+        config.get().isSslHostnameVerificationEnabled(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(new RWEnabled().copy().followRedirects(), result.get());
+
+        result.set(null);
+        req = new TestClient(new RWEnabled().enableSslHostnameVerification(false)).request().apply();
+        config.get().isSslHostnameVerificationEnabled(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(false, result.get());
+
+//        NOT PER REQUEST
+//        result.set(null);
+//        req = new TestClient(new RWEnabled().enableSslHostnameVerification(false).prefix(PREFIX))
+//                .request(r -> r.setProperty(PREFIX + NettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION, true))
+//                .apply();
+//        config.get().isSslHostnameVerificationEnabled(req.request.getConfiguration().getProperties());
+//        Assertions.assertEquals(true, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWEnabled().enableSslHostnameVerification(false).prefix(PREFIX))
+                .request(r -> r.setProperty(
+                        PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWEnabled().enableSslHostnameVerification(true)))
+                .apply();
+        config.get().isSslHostnameVerificationEnabled(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(true, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWEnabled())
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWEnabled().enableSslHostnameVerification(false)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWEnabled().enableSslHostnameVerification(true)))
+                .apply();
+        config.get().isSslHostnameVerificationEnabled(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(true, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWEnabled().prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.ENABLE_SSL_HOSTNAME_VERIFICATION, false))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWEnabled().enableSslHostnameVerification(true)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        config.get().isSslHostnameVerificationEnabled(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(false, result.get());
+    }
+
+    @Test
+    public void testMaxRedirects() {
+        final AtomicReference<Integer> result = new AtomicReference<>();
+        final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>();
+        class RWMaxRed extends RW {
+
+            @Override
+            int maxRedirects(ClientRequest request) {
+                result.set(super.maxRedirects(request));
+                return result.get();
+            }
+
+            @Override
+            public RWMaxRed instance() {
+                RWMaxRed max = new RWMaxRed();
+                config.set(max);
+                return max;
+            }
+        }
+
+        Request req = new TestClient(new RWMaxRed()).request().apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        Assertions.assertEquals(new RWMaxRed().copy().maxRedirects(req.request), result.get());
+
+        result.set(null);
+        req = new TestClient(new RWMaxRed().maxRedirects(2)).request().apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        Assertions.assertEquals(2, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWMaxRed().maxRedirects(2).prefix(PREFIX))
+                .request(r -> r.setProperty(PREFIX + NettyClientProperties.MAX_REDIRECTS, 3))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        Assertions.assertEquals(3, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWMaxRed().maxRedirects(2).prefix(PREFIX))
+                .request(r -> r.setProperty(
+                        PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWMaxRed().maxRedirects(3)))
+                .apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        Assertions.assertEquals(3, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWMaxRed())
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWMaxRed().maxRedirects(2)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWMaxRed().maxRedirects(3)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        Assertions.assertEquals(3, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWMaxRed().prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.MAX_REDIRECTS, 2))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWMaxRed().maxRedirects(3)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        Assertions.assertEquals(2, result.get());
+    }
+
+    @Test
+    public void testRedirectController() {
+        final NettyHttpRedirectController controller = new NettyHttpRedirectController();
+        final AtomicReference<NettyHttpRedirectController> result = new AtomicReference<>();
+        final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>();
+        class RWController extends RW {
+
+            @Override
+            NettyHttpRedirectController redirectController(ClientRequest request) {
+                result.set(super.redirectController(request));
+                return result.get();
+            }
+
+            @Override
+            public RWController instance() {
+                RWController max = new RWController();
+                config.set(max);
+                return max;
+            }
+        }
+
+        Request req;
+
+        result.set(null);
+        req = new TestClient(new RWController().redirectController(controller)).request().apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        config.get().redirectController(req.request);
+        Assertions.assertEquals(controller, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWController().redirectController(new NettyHttpRedirectController()).prefix(PREFIX))
+                .request(r -> r.setProperty(PREFIX + NettyClientProperties.HTTP_REDIRECT_CONTROLLER, controller))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        config.get().redirectController(req.request);
+        Assertions.assertEquals(controller, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWController().redirectController(new NettyHttpRedirectController()).prefix(PREFIX))
+                .request(r -> r.setProperty(
+                        PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWController().redirectController(controller)))
+                .apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        config.get().redirectController(req.request);
+        Assertions.assertEquals(controller, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWController())
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWController().redirectController(new NettyHttpRedirectController())))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWController().redirectController(controller)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+//        config.get().redirectController(req.request);
+        Assertions.assertEquals(controller, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWController().prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.HTTP_REDIRECT_CONTROLLER, controller))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWController().redirectController(new NettyHttpRedirectController())))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+//        config.get().redirectController(req.request);
+        Assertions.assertEquals(controller, result.get());
+    }
+
+    @Test
+    public void testPreserveMethodOnRedirect() {
+        final AtomicReference<Boolean> result = new AtomicReference<>();
+        final AtomicReference<NettyHttpRedirectController> controller = new AtomicReference<>();
+        final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>();
+        class RWController extends RW {
+
+            @Override
+            boolean preserveMethodOnRedirect(ClientRequest request) {
+                result.set(super.preserveMethodOnRedirect(request));
+                return result.get();
+            }
+
+            @Override
+            NettyHttpRedirectController redirectController(ClientRequest request) {
+                controller.set(super.redirectController(request));
+                return controller.get();
+            }
+
+            @Override
+            public RWController instance() {
+                RWController max = new RWController();
+                config.set(max);
+                return max;
+            }
+        }
+
+        Request req;
+        req = new TestClient(new RWController()).request().apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        try {
+            controller.get().prepareRedirect(req.request, null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(new RWController().copy().preserveMethodOnRedirect(req.request), result.get());
+
+        result.set(null);
+        req = new TestClient(new RWController().preserveMethodOnRedirect(false)).request().apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        try {
+            controller.get().prepareRedirect(req.request, null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(false, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWController().preserveMethodOnRedirect(true).prefix(PREFIX))
+                .request(r -> r.setProperty(PREFIX + NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, false))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        try {
+            controller.get().prepareRedirect(req.request, null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(false, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWController().preserveMethodOnRedirect(true).prefix(PREFIX))
+                .request(r -> r.setProperty(
+                        PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWController().preserveMethodOnRedirect(false)))
+                .apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        try {
+            controller.get().prepareRedirect(req.request, null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(false, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWController())
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWController().preserveMethodOnRedirect(true)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWController().preserveMethodOnRedirect(false)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        try {
+            controller.get().prepareRedirect(req.request, null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(false, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWController().prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, false))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWController().preserveMethodOnRedirect(true)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        new JerseyClientHandler(req.request, new CompletableFuture<ClientResponse>(),
+                new CompletableFuture<Object>(), new HashSet<>(), req.connector, config.get());
+        try {
+            controller.get().prepareRedirect(req.request, null);
+        } catch (NullPointerException expected) {
+        }
+        Assertions.assertEquals(false, result.get());
+    }
+
+    @Test
+    public void testFilterHeadersForProxy() {
+        final AtomicReference<ClientProxy> proxy = new AtomicReference<>();
+        final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>();
+        class RWProxy extends RW {
+            @Override
+            public Optional<ClientProxy> proxy(ClientRequest request, URI requestUri) {
+                Optional<ClientProxy> oProxy = super.proxy(request, requestUri);
+                proxy.set(oProxy.orElse(null));
+                return oProxy;
+            }
+
+            @Override
+            public RWProxy instance() {
+                RWProxy rw = new RWProxy();
+                config.set(rw);
+                return rw;
+            }
+        }
+        String proxyUri = "http://proxy.org:8080";
+        String userName = "USERNAME";
+        String password = "PASSWORD";
+
+        Request req;
+        req = new TestClient(new RWProxy().proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password)).request().apply();
+        config.get().createProxyHandler(proxy.get(), req.request);
+        Assertions.assertEquals(((Ref<Boolean>) new RWProxy().copy().filterHeadersForProxy).get(),
+                ((Ref<Boolean>) config.get().filterHeadersForProxy).get());
+
+        config.set(null);
+        req = new TestClient(new RWProxy().proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password)
+                .filterHeadersForProxy(false)).request().apply();
+        config.get().createProxyHandler(proxy.get(), req.request);
+        Assertions.assertEquals(false, ((Ref<Boolean>) config.get().filterHeadersForProxy).get());
+
+        config.set(null);
+        req = new TestClient(new RWProxy().proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password)
+                .prefix(PREFIX))
+                .request(r -> r.setProperty(PREFIX + NettyClientProperties.FILTER_HEADERS_FOR_PROXY, false))
+                .apply();
+        config.get().createProxyHandler(proxy.get(), req.request);
+        Assertions.assertEquals(false, ((Ref<Boolean>) config.get().filterHeadersForProxy).get());
+
+        config.set(null);
+        new TestClient(new RWProxy().proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password)
+                .prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.FILTER_HEADERS_FOR_PROXY, false))
+                .request().apply();
+        config.get().createProxyHandler(proxy.get(), req.request);
+        Assertions.assertEquals(false, ((Ref<Boolean>) config.get().filterHeadersForProxy).get());
+
+        config.set(null);
+        new TestClient(new RWProxy().proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password)
+                .prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        NettyConnectorProvider.config().filterHeadersForProxy(false).prefix(PREFIX)))
+                .request().apply();
+        config.get().createProxyHandler(proxy.get(), req.request);
+        Assertions.assertEquals(false, ((Ref<Boolean>) config.get().filterHeadersForProxy).get());
+
+        config.set(null);
+        new TestClient(new RWProxy().proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password)
+                .prefix(PREFIX))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        NettyConnectorProvider.config().filterHeadersForProxy(false).prefix(PREFIX)))
+                .apply();
+        config.get().createProxyHandler(proxy.get(), req.request);
+        Assertions.assertEquals(false, ((Ref<Boolean>) config.get().filterHeadersForProxy).get());
+    }
+
+    @Test
+    public void testFirstHttpHeaderLineLength() {
+        final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>();
+        final AtomicReference<Integer> result = new AtomicReference<>();
+        class RWFHHLL extends RW {
+
+            @Override
+            HttpClientCodec createHttpClientCodec(Map<String, Object> properties) {
+                HttpClientCodec codec = super.createHttpClientCodec(properties);
+                result.set(firstHttpHeaderLineLength.get());
+                return codec;
+            }
+
+            @Override
+            public RWFHHLL instance() {
+                RWFHHLL rw = new RWFHHLL();
+                config.set(rw);
+                return rw;
+            }
+        }
+
+        Request req;
+        req = new TestClient(new RWFHHLL()).request().apply();
+        config.get().createHttpClientCodec(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(NettyClientProperties.DEFAULT_INITIAL_LINE_LENGTH, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWFHHLL().initialHttpHeaderLineLength(5555)).request().apply();
+        config.get().createHttpClientCodec(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(5555, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWFHHLL().initialHttpHeaderLineLength(1111).prefix(PREFIX))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWFHHLL().initialHttpHeaderLineLength(5555).prefix(PREFIX))).apply();
+        config.get().createHttpClientCodec(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(5555, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWFHHLL().initialHttpHeaderLineLength(1111).prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.MAX_INITIAL_LINE_LENGTH, 5555))
+                .request().apply();
+        config.get().createHttpClientCodec(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(5555, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWFHHLL().prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.MAX_INITIAL_LINE_LENGTH, 5555))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWFHHLL().initialHttpHeaderLineLength(8888).prefix(PREFIX)))
+                .apply();
+        config.get().createHttpClientCodec(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(5555, result.get());
+    }
+
+    @Test
+    public void testLoggingHandler() {
+        final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>();
+        class RWLog extends RW {
+            @Override
+            public RWLog instance() {
+                RWLog rw = new RWLog();
+                config.set(rw);
+                return rw;
+            }
+        }
+
+        new TestClient(new RWLog()).request().apply();
+        Assertions.assertFalse(config.get().loggingEnabled.get());
+
+        new TestClient(new RWLog().enableLoggingHandler(true)).request().apply();
+        Assertions.assertTrue(config.get().loggingEnabled.get());
+
+        new TestClient(new RWLog().enableLoggingHandler(false).prefix(PREFIX))
+                .request(r -> r.setProperty(NettyClientProperties.LOGGING_ENABLED, true))
+                .apply();
+        Assertions.assertFalse(config.get().loggingEnabled.get());
+
+        new TestClient(new RWLog().enableLoggingHandler(false).prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.LOGGING_ENABLED, true))
+                .request()
+                .apply();
+        Assertions.assertTrue(config.get().loggingEnabled.get());
+
+        new TestClient(new RWLog().enableLoggingHandler(false).prefix(PREFIX))
+                .request(r -> r.setProperty(PREFIX + NettyClientProperties.LOGGING_ENABLED, true))
+                .apply();
+        Assertions.assertTrue(config.get().loggingEnabled.get());
+
+        new TestClient(new RWLog().enableLoggingHandler(false).prefix(PREFIX))
+                .client(c -> c.property(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWLog().enableLoggingHandler(true)))
+                .request()
+                .apply();
+        Assertions.assertTrue(config.get().loggingEnabled.get());
+
+        new TestClient(new RWLog().enableLoggingHandler(false).prefix(PREFIX))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWLog().enableLoggingHandler(true).prefix(PREFIX)))
+                .apply();
+        Assertions.assertTrue(config.get().loggingEnabled.get());
+    }
+
+    @Test
+    public void testMaxHeaderSize() {
+        final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>();
+        final AtomicReference<Integer> result = new AtomicReference<>();
+        class RWSize extends RW {
+
+            @Override
+            HttpClientCodec createHttpClientCodec(Map<String, Object> properties) {
+                HttpClientCodec codec = super.createHttpClientCodec(properties);
+                result.set(maxHeaderSize.get());
+                return codec;
+            }
+
+            @Override
+            public RWSize instance() {
+                RWSize rw = new RWSize();
+                config.set(rw);
+                return rw;
+            }
+        }
+
+        Request req;
+        req = new TestClient(new RWSize()).request().apply();
+        config.get().createHttpClientCodec(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(NettyClientProperties.DEFAULT_HEADER_SIZE, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWSize().maxHeaderSize(5555)).request().apply();
+        config.get().createHttpClientCodec(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(5555, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWSize().maxHeaderSize(1111).prefix(PREFIX))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWSize().maxHeaderSize(5555).prefix(PREFIX))).apply();
+        config.get().createHttpClientCodec(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(5555, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWSize().maxHeaderSize(1111).prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.MAX_HEADER_SIZE, 5555))
+                .request().apply();
+        config.get().createHttpClientCodec(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(5555, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWSize().prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.MAX_HEADER_SIZE, 5555))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWSize().maxHeaderSize(8888).prefix(PREFIX)))
+                .apply();
+        config.get().createHttpClientCodec(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(5555, result.get());
+    }
+
+    @Test
+    public void testMaxChunkSize() {
+        final AtomicReference<NettyConnectorProvider.Config.RW> config = new AtomicReference<>();
+        final AtomicReference<Integer> result = new AtomicReference<>();
+        class RWSize extends RW {
+
+            @Override
+            HttpClientCodec createHttpClientCodec(Map<String, Object> properties) {
+                HttpClientCodec codec = super.createHttpClientCodec(properties);
+                result.set(maxChunkSize.get());
+                return codec;
+            }
+
+            @Override
+            public RWSize instance() {
+                RWSize rw = new RWSize();
+                config.set(rw);
+                return rw;
+            }
+        }
+
+        Request req;
+        req = new TestClient(new RWSize()).request().apply();
+        config.get().createHttpClientCodec(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(NettyClientProperties.DEFAULT_CHUNK_SIZE, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWSize().maxChunkSize(5555)).request().apply();
+        config.get().createHttpClientCodec(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(5555, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWSize().maxChunkSize(1111).prefix(PREFIX))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWSize().maxChunkSize(5555).prefix(PREFIX))).apply();
+        config.get().createHttpClientCodec(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(5555, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWSize().maxChunkSize(1111).prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.MAX_CHUNK_SIZE, 5555))
+                .request().apply();
+        config.get().createHttpClientCodec(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(5555, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWSize().prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.MAX_CHUNK_SIZE, 5555))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWSize().maxChunkSize(8888).prefix(PREFIX)))
+                .apply();
+        config.get().createHttpClientCodec(req.request.getConfiguration().getProperties());
+        Assertions.assertEquals(5555, result.get());
+    }
+
+    @Test
+    public void testMaxTotalConnections() {
+        final AtomicReference<Integer> result = new AtomicReference<>();
+        class RWConn extends RW {
+
+            @Override
+            NettyConnectorProvider.Config.RW fromClient(Client client) {
+                NettyConnectorProvider.Config.RW rw = super.fromClient(client);
+                result.set(rw.maxPoolSizeTotal.get());
+                return rw;
+            }
+
+            @Override
+            public RWConn instance() {
+                return new RWConn();
+            }
+        }
+
+        new TestClient(new RWConn().maxTotalConnections(55)).request().apply();
+        Assertions.assertEquals(55, result.get());
+
+        result.set(null);
+        new TestClient(new RWConn().prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.MAX_CONNECTIONS_TOTAL, 55))
+                .request().apply();
+        Assertions.assertEquals(55, result.get());
+
+        result.set(null);
+        new TestClient(new RWConn().prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        NettyConnectorProvider.config().prefix(PREFIX).maxTotalConnections(55)))
+                .request().apply();
+        Assertions.assertEquals(55, result.get());
+
+        result.set(null);
+        new TestClient(new RWConn().prefix(PREFIX))
+                // NOT PER REQUEST
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        NettyConnectorProvider.config().prefix(PREFIX).maxTotalConnections(55)))
+                .apply();
+        Assertions.assertEquals(new RWConn().copy().maxPoolSizeTotal.get(), result.get());
+    }
+
+    @Test
+    public void testMaxConnectionsPerDestination() {
+        final AtomicReference<Integer> result = new AtomicReference<>();
+        class RWConn extends RW {
+
+            @Override
+            NettyConnectorProvider.Config.RW fromClient(Client client) {
+                NettyConnectorProvider.Config.RW rw = super.fromClient(client);
+                result.set(rw.maxPoolSize.get());
+                return rw;
+            }
+
+            @Override
+            public RWConn instance() {
+                return new RWConn();
+            }
+        }
+
+        new TestClient(new RWConn().maxConnectionsPerDestination(15)).request().apply();
+        Assertions.assertEquals(15, result.get());
+
+        result.set(null);
+        new TestClient(new RWConn().prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.MAX_CONNECTIONS, 15))
+                .request().apply();
+        Assertions.assertEquals(15, result.get());
+
+        result.set(null);
+        new TestClient(new RWConn().prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        NettyConnectorProvider.config().prefix(PREFIX).maxConnectionsPerDestination(15)))
+                .request().apply();
+        Assertions.assertEquals(15, result.get());
+
+        result.set(null);
+        new TestClient(new RWConn().prefix(PREFIX))
+                // NOT PER REQUEST
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        NettyConnectorProvider.config().prefix(PREFIX).maxConnectionsPerDestination(15)))
+                .apply();
+        Assertions.assertEquals(new RWConn().copy().maxPoolSize.get(), result.get());
+    }
+
+    @Test
+    public void testIdleConnectionPruneTimeout() {
+        final AtomicReference<Integer> result = new AtomicReference<>();
+        class RWConn extends RW {
+
+            @Override
+            NettyConnectorProvider.Config.RW fromClient(Client client) {
+                NettyConnectorProvider.Config.RW rw = super.fromClient(client);
+                result.set(rw.maxPoolIdle.get());
+                return rw;
+            }
+
+            @Override
+            public RWConn instance() {
+                return new RWConn();
+            }
+        }
+
+        new TestClient(new RWConn().idleConnectionPruneTimeout(15)).request().apply();
+        Assertions.assertEquals(15, result.get());
+
+        result.set(null);
+        new TestClient(new RWConn().prefix(PREFIX))
+                .client(c -> c.property(PREFIX + NettyClientProperties.IDLE_CONNECTION_PRUNE_TIMEOUT, 15))
+                .request().apply();
+        Assertions.assertEquals(15, result.get());
+
+        result.set(null);
+        new TestClient(new RWConn().prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        NettyConnectorProvider.config().prefix(PREFIX).idleConnectionPruneTimeout(15)))
+                .request().apply();
+        Assertions.assertEquals(15, result.get());
+
+        result.set(null);
+        new TestClient(new RWConn().prefix(PREFIX))
+                // NOT PER REQUEST
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        NettyConnectorProvider.config().prefix(PREFIX).idleConnectionPruneTimeout(15)))
+                .apply();
+        Assertions.assertEquals(new RWConn().copy().maxPoolIdle.get(), result.get());
+    }
+
+    @Test
+    public void testPrecedence() {
+        NettyConnectorProvider.Config.RW builderLower = NettyConnectorProvider.config().rw();
+        builderLower.maxTotalConnections(55);
+
+        NettyConnectorProvider.Config.RW builderUpper = builderLower.copy();
+        builderUpper.maxTotalConnections(56);
+        Assertions.assertEquals(56, builderUpper.maxPoolSizeTotal.get());
+
+        Client client = ClientBuilder.newClient();
+        client.property(NettyClientProperties.MAX_CONNECTIONS_TOTAL, 57);
+        NettyConnectorProvider.Config.RW result = builderUpper.fromClient(client);
+        Assertions.assertEquals(57, result.maxPoolSizeTotal.get());
+        Assertions.assertEquals(60, result.maxPoolIdle.get());
+    }
+
+    private static class ConnectorConfig extends ConnectorConfiguration<ConnectorConfig> {
+        private static class RW extends ConnectorConfiguration<RW> implements Read<RW> {
+            @Override
+            public RW instance() {
+                return new RW();
+            }
+
+            @Override
+            public RW me() {
+                return this;
+            }
+
+            public Configuration config() {
+                Map<String, Object> empty = new HashMap<>();
+                Configuration configuration = (Configuration) Proxy.newProxyInstance(
+                        getClass().getClassLoader(),
+                        new Class[]{Configuration.class}, new InvocationHandler() {
+                            @Override
+                            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+                                switch (method.getName()) {
+                                    case "getProperties":
+                                        return empty;
+                                    case "getProperty":
+                                        return empty.get(args[0]);
+                                }
+                                return null;
+                            }
+                        });
+
+                return configuration;
+            }
+        }
+    }
+
+    private static class RW extends NettyConnectorProvider.Config.RW {
+        @Override
+        public int connectTimeout() {
+            throw new IllegalStateException();
+        }
+    }
+
+
+    private static class TestClient {
+        private final NettyConnectorProvider.Config.RW rw;
+        private final Client client;
+
+        private TestClient(NettyConnectorProvider.Config.RW rw) {
+            this.rw = rw;
+            this.client = ClientBuilder.newClient(new ClientConfig().connectorProvider(new NettyConnectorProvider()));
+        }
+
+        public TestClient client(Consumer<Client> consumer) {
+            consumer.accept(client);
+            return this;
+        }
+
+        public TestClient rw(Consumer<NettyConnectorProvider.Config.RW> consumer) {
+            consumer.accept(rw);
+            return this;
+        }
+
+        public Request request() {
+            return new Request(client, rw);
+        }
+
+        public Request request(Consumer<ClientRequest> consumer) {
+            return request().request(consumer);
+        }
+    }
+
+    private static class Request {
+        final ClientRequest request;
+        final Client client;
+        final NettyConnectorProvider.Config.RW rw;
+        NettyConnector connector;
+
+        Request(Client client, NettyConnectorProvider.Config.RW rw) {
+            this.client = client;
+            this.rw = rw;
+            request = createRequest(client);
+        }
+
+        public Request request(Consumer<ClientRequest> consumer) {
+            consumer.accept(request);
+            return this;
+        }
+
+        public Request apply() {
+            try {
+                connector = new NettyConnector(client, rw);
+                connector.apply(request);
+            } catch (ProcessingException expected) {
+            }
+            return this;
+        }
+    }
+}
diff --git a/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/CustomConnectionControllerTest.java b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/CustomConnectionControllerTest.java
new file mode 100644
index 0000000..abf23e5
--- /dev/null
+++ b/connectors/netty-connector/src/test/java/org/glassfish/jersey/netty/connector/CustomConnectionControllerTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2025 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 org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientRequest;
+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 jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.net.URI;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class CustomConnectionControllerTest extends JerseyTest {
+    final AtomicBoolean hit = new AtomicBoolean(false);
+
+    @Path("/")
+    public static class CustomConnectionControllerTestResource {
+        @GET
+        public String get() {
+            return "ok";
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(CustomConnectionControllerTestResource.class);
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        NettyConnectorProvider provider = NettyConnectorProvider.config().connectionController(new NettyConnectionController() {
+            @Override
+            public String getConnectionGroup(ClientRequest clientRequest, URI uri, String hostName, int port) {
+                hit.set(true);
+                return super.getConnectionGroup(clientRequest, uri, hostName, port);
+            }
+        }).build();
+
+        config.connectorProvider(provider);
+    }
+
+    @Test
+    public void testCustomConnectionControllerIsInvoked() {
+        try (Response response = target().request().get()) {
+            Assertions.assertEquals(200, response.getStatus());
+        }
+        client().close();
+        Assertions.assertEquals(true, hit.get());
+
+    }
+}
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 267664d..559b7ad 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 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
@@ -155,6 +155,15 @@
     public static final String BACKGROUND_SCHEDULER_THREADPOOL_SIZE = "jersey.config.client.backgroundScheduler.threadPoolSize";
 
     /**
+     * The connector configuration object available through connector provider configuration methods.
+     *
+     * <p>
+     *  The name of the configuration property is <tt>{@value}</tt>.
+     * </p>
+     */
+    public static final String CONNECTOR_CONFIGURATION = "jersey.config.client.ConnectorConfiguration";
+
+    /**
      * If {@link org.glassfish.jersey.client.filter.EncodingFilter} is
      * registered, this property indicates the value of Content-Encoding
      * property the filter should be adding.
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 dd96928..b8ec60b 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, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2025 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,14 +20,12 @@
 import java.net.HttpURLConnection;
 import java.net.Proxy;
 import java.net.URL;
-import java.util.Map;
-import java.util.logging.Logger;
 
 import jakarta.ws.rs.client.Client;
 import jakarta.ws.rs.core.Configuration;
 
 import org.glassfish.jersey.client.internal.HttpUrlConnector;
-import org.glassfish.jersey.client.internal.LocalizationMessages;
+import org.glassfish.jersey.client.internal.HttpUrlConnectorConfiguration;
 import org.glassfish.jersey.client.spi.Connector;
 import org.glassfish.jersey.client.spi.ConnectorProvider;
 
@@ -110,26 +108,22 @@
      */
     public static final String SET_METHOD_WORKAROUND =
             "jersey.config.client.httpUrlConnection.setMethodWorkaround";
-    /**
-     * Default connection factory to be used.
-     */
-    private static final ConnectionFactory DEFAULT_CONNECTION_FACTORY = new DefaultConnectionFactory();
 
-    private static final Logger LOGGER = Logger.getLogger(HttpUrlConnectorProvider.class.getName());
-
-    private ConnectionFactory connectionFactory;
-    private int chunkSize;
-    private boolean useFixedLengthStreaming;
-    private boolean useSetMethodWorkaround;
+    protected final Config config;
 
     /**
      * Create new {@link java.net.HttpURLConnection}-based Jersey client connector provider.
      */
     public HttpUrlConnectorProvider() {
-        this.connectionFactory = DEFAULT_CONNECTION_FACTORY;
-        this.chunkSize = ClientProperties.DEFAULT_CHUNK_SIZE;
-        this.useFixedLengthStreaming = false;
-        this.useSetMethodWorkaround = false;
+        this.config = new Config();
+    }
+
+    private HttpUrlConnectorProvider(Config config) {
+        this.config = config;
+    }
+
+    public static Config config() {
+        return new Config();
     }
 
     /**
@@ -140,11 +134,7 @@
      * @throws java.lang.NullPointerException in case the supplied connectionFactory is {@code null}.
      */
     public HttpUrlConnectorProvider connectionFactory(final ConnectionFactory connectionFactory) {
-        if (connectionFactory == null) {
-            throw new NullPointerException(LocalizationMessages.NULL_INPUT_PARAMETER("connectionFactory"));
-        }
-
-        this.connectionFactory = connectionFactory;
+        config.connectionFactory(connectionFactory);
         return this;
     }
 
@@ -164,10 +154,7 @@
      * @throws java.lang.IllegalArgumentException in case the specified chunk size is negative.
      */
     public HttpUrlConnectorProvider chunkSize(final int chunkSize) {
-        if (chunkSize < 0) {
-            throw new IllegalArgumentException(LocalizationMessages.NEGATIVE_INPUT_PARAMETER("chunkSize"));
-        }
-        this.chunkSize = chunkSize;
+        config.chunkSize(chunkSize);
         return this;
     }
 
@@ -183,7 +170,7 @@
      * @return updated connector provider instance.
      */
     public HttpUrlConnectorProvider useFixedLengthStreaming() {
-        this.useFixedLengthStreaming = true;
+        config.useFixedLengthStreaming(true);
         return this;
     }
 
@@ -199,28 +186,18 @@
      * @return updated connector provider instance.
      */
     public HttpUrlConnectorProvider useSetMethodWorkaround() {
-        this.useSetMethodWorkaround = true;
+        config.useSetMethodWorkaround(true);
         return this;
     }
 
     @Override
-    public Connector getConnector(final Client client, final Configuration config) {
-        final Map<String, Object> properties = config.getProperties();
-
-        int computedChunkSize = ClientProperties.getValue(properties,
-                ClientProperties.CHUNKED_ENCODING_SIZE, chunkSize, Integer.class);
-        if (computedChunkSize < 0) {
-            LOGGER.warning(LocalizationMessages.NEGATIVE_CHUNK_SIZE(computedChunkSize, chunkSize));
-            computedChunkSize = chunkSize;
-        }
-
-        final boolean computedUseFixedLengthStreaming = ClientProperties.getValue(properties,
-                USE_FIXED_LENGTH_STREAMING, useFixedLengthStreaming, Boolean.class);
-        final boolean computedUseSetMethodWorkaround = ClientProperties.getValue(properties,
-                SET_METHOD_WORKAROUND, useSetMethodWorkaround, Boolean.class);
-
-        return createHttpUrlConnector(client, connectionFactory, computedChunkSize, computedUseFixedLengthStreaming,
-                                      computedUseSetMethodWorkaround);
+    public Connector getConnector(final Client client, final Configuration configuration) {
+        this.config.preInit(configuration);
+        return createHttpUrlConnector(client,
+                this.config.connectionFactory(),
+                this.config.chunkSize(),
+                this.config.fixLengthStreaming(),
+                this.config.setMethodWorkaround());
     }
 
     /**
@@ -230,7 +207,7 @@
      * @param connectionFactory   {@link javax.net.ssl.HttpsURLConnection} factory to be used when creating
      *                            connections.
      * @param chunkSize           chunk size to use when using HTTP chunked transfer coding.
-     * @param fixLengthStreaming  specify if the the {@link java.net.HttpURLConnection#setFixedLengthStreamingMode(int)
+     * @param fixLengthStreaming  specify if the {@link java.net.HttpURLConnection#setFixedLengthStreamingMode(int)
      *                            fixed-length streaming mode} on the underlying HTTP URL connection instances should
      *                            be used when sending requests.
      * @param setMethodWorkaround specify if the reflection workaround should be used to set HTTP URL connection method
@@ -240,12 +217,33 @@
     protected Connector createHttpUrlConnector(Client client, ConnectionFactory connectionFactory,
                                                int chunkSize, boolean fixLengthStreaming,
                                                boolean setMethodWorkaround) {
-        return new HttpUrlConnector(
-                client,
-                connectionFactory,
-                chunkSize,
-                fixLengthStreaming,
-                setMethodWorkaround);
+        return new HttpUrlConnector(client, client.getConfiguration(), config);
+    }
+
+    public static final class Config extends HttpUrlConnectorConfiguration<Config> {
+        public HttpUrlConnectorProvider build() {
+            return new HttpUrlConnectorProvider(this);
+        }
+
+        /* package */ ConnectionFactory connectionFactory() {
+            return connectionFactory.get();
+        }
+
+        /* package */ int chunkSize() {
+            return chunkSize.get();
+        }
+
+        /* package */ boolean fixLengthStreaming() {
+            return useFixedLengthStreaming.get();
+        }
+
+        /* package */ boolean setMethodWorkaround() {
+            return useSetMethodWorkaround.get();
+        }
+
+        /* package */ void preInit(Configuration configuration) {
+            super.preInit(configuration.getProperties());
+        }
     }
 
     /**
@@ -291,23 +289,6 @@
         }
     }
 
-    private static class DefaultConnectionFactory implements ConnectionFactory {
-
-        @Override
-        public HttpURLConnection getConnection(final URL url) throws IOException {
-            return connect(url, null);
-        }
-
-        @Override
-        public HttpURLConnection getConnection(URL url, Proxy proxy) throws IOException {
-            return connect(url, proxy);
-        }
-
-        private HttpURLConnection connect(URL url, Proxy proxy) throws IOException {
-            return (proxy == null) ? (HttpURLConnection) url.openConnection() : (HttpURLConnection) url.openConnection(proxy);
-        }
-    }
-
     @Override
     public boolean equals(final Object o) {
         if (this == o) {
@@ -318,22 +299,11 @@
         }
 
         final HttpUrlConnectorProvider that = (HttpUrlConnectorProvider) o;
-
-        if (chunkSize != that.chunkSize) {
-            return false;
-        }
-        if (useFixedLengthStreaming != that.useFixedLengthStreaming) {
-            return false;
-        }
-
-        return connectionFactory.equals(that.connectionFactory);
+        return config.equals(that.config);
     }
 
     @Override
     public int hashCode() {
-        int result = connectionFactory.hashCode();
-        result = 31 * result + chunkSize;
-        result = 31 * result + (useFixedLengthStreaming ? 1 : 0);
-        return result;
+        return config.hashCode();
     }
 }
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/ClientProxy.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/ClientProxy.java
index 68ab324..1d82b06 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/innate/ClientProxy.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/ClientProxy.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022, 2025 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,8 +16,8 @@
 package org.glassfish.jersey.client.innate;
 
 import org.glassfish.jersey.client.ClientProperties;
-import org.glassfish.jersey.client.ClientRequest;
 import org.glassfish.jersey.client.internal.LocalizationMessages;
+import org.glassfish.jersey.internal.PropertiesResolver;
 
 import jakarta.ws.rs.ProcessingException;
 import jakarta.ws.rs.core.Configuration;
@@ -31,6 +31,7 @@
 import java.util.Arrays;
 import java.util.Base64;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Optional;
 
 /**
@@ -42,16 +43,20 @@
       // do not instantiate
     };
 
-    public static Optional<ClientProxy> proxyFromRequest(ClientRequest request) {
+    public static Optional<ClientProxy> proxyFromRequest(PropertiesResolver request) {
         return getProxy(request);
     }
 
-    public static Optional<ClientProxy> proxyFromProperties(URI requestUri) {
+    public static Optional<ClientProxy> proxyFromUri(URI requestUri) {
         return getSystemPropertiesProxy(requestUri);
     }
 
+    public static Optional<ClientProxy> proxyFromProperties(Map<String, Object> properties) {
+        return getProxy(properties);
+    }
+
     public static Optional<ClientProxy> proxyFromConfiguration(Configuration configuration) {
-        return getProxy(configuration);
+        return getProxy(configuration.getProperties());
     }
 
     public static ClientProxy proxy(Proxy proxy) {
@@ -103,7 +108,7 @@
         return userName;
     };
 
-    private static Optional<ClientProxy> getProxy(ClientRequest request) {
+    private static Optional<ClientProxy> getProxy(PropertiesResolver request) {
         Object proxyUri = request.resolveProperty(ClientProperties.PROXY_URI, Object.class);
         if (proxyUri != null) {
             ClientProxy proxy = toProxy(proxyUri);
@@ -118,13 +123,13 @@
         return Optional.empty();
     }
 
-    private static Optional<ClientProxy> getProxy(Configuration config) {
-        Object proxyUri = config.getProperties().get(ClientProperties.PROXY_URI);
+    private static Optional<ClientProxy> getProxy(Map<String, Object> properties) {
+        Object proxyUri = properties.get(ClientProperties.PROXY_URI);
         if (proxyUri != null) {
             ClientProxy proxy = toProxy(proxyUri);
             if (proxy != null) {
-                proxy.userName = ClientProperties.getValue(config.getProperties(), ClientProperties.PROXY_USERNAME, String.class);
-                proxy.password = ClientProperties.getValue(config.getProperties(), ClientProperties.PROXY_PASSWORD, String.class);
+                proxy.userName = ClientProperties.getValue(properties, ClientProperties.PROXY_USERNAME, String.class);
+                proxy.password = ClientProperties.getValue(properties, ClientProperties.PROXY_PASSWORD, String.class);
                 return Optional.of(proxy);
             } else {
                 return Optional.empty();
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/ConnectorConfiguration.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/ConnectorConfiguration.java
new file mode 100644
index 0000000..48e9244
--- /dev/null
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/ConnectorConfiguration.java
@@ -0,0 +1,895 @@
+/*
+ * Copyright (c) 2025 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.innate;
+
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.RequestEntityProcessing;
+import org.glassfish.jersey.client.innate.http.SSLParamConfigurator;
+import org.glassfish.jersey.internal.PropertiesResolver;
+
+import javax.net.ssl.SSLContext;
+import jakarta.ws.rs.RuntimeType;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Configuration;
+import jakarta.ws.rs.core.Feature;
+import java.net.Proxy;
+import java.net.URI;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * Configuration object to use for configuring the client connectors and HTTP request processing.
+ * This configuration provides settings to be handled by the connectors, mainly declared by {@link ClientProperties}.
+ *
+ * @param <E> the connector configuration subtype.
+ */
+public class ConnectorConfiguration<E extends ConnectorConfiguration<E>> {
+    protected final NullableRef<Integer> connectTimeout = NullableRef.empty();
+    protected final NullableRef<Boolean> expect100Continue = NullableRef.empty();
+    protected final NullableRef<Long> expect100continueThreshold = NullableRef.empty();
+    protected final NullableRef<Boolean> followRedirects = NullableRef.empty();
+    protected final NullableRef<String> prefix = NullableRef.empty();
+    protected final NullableRef<Object> proxyUri = NullableRef.empty();
+    protected final NullableRef<String> proxyUserName = NullableRef.empty();
+    protected final NullableRef<String> proxyPassword = NullableRef.empty();
+    protected final NullableRef<Integer> readTimeout = NullableRef.empty();
+    protected final NullableRef<RequestEntityProcessing> requestEntityProcessing = NullableRef.empty();
+    protected final NullableRef<String> sniHostname = NullableRef.empty();
+    protected final NullableRef<Supplier<SSLContext>> sslContextSupplier = NullableRef.empty();
+    protected final NullableRef<Integer> threadPoolSize = NullableRef.empty();
+
+    /**
+     * Use factory methods provided by each connector supporting this configuration object and its subclass instead.
+     */
+    protected ConnectorConfiguration() {
+    }
+
+    /**
+     * Set the asynchronous thread-pool size. The property {@link ClientProperties#ASYNC_THREADPOOL_SIZE}
+     * has precedence over this setting.
+     *
+     * @param threadPoolSize the size of the asynchronous thread-pool.
+     * @return updated configuration.
+     */
+    public E asyncThreadPoolSize(int threadPoolSize) {
+        this.threadPoolSize.set(threadPoolSize);
+        return self();
+    }
+
+    /**
+     * Set connect timeout. The property {@link ClientProperties#CONNECT_TIMEOUT}
+     * has precedence over this setting.
+     *
+     * @param millis timeout in milliseconds.
+     * @return updated configuration.
+     */
+    public E connectTimeout(int millis) {
+        connectTimeout.set(millis);
+        return self();
+    }
+
+    /**
+     * Allows for HTTP Expect:100-Continue.
+     * The property {@link ClientProperties#EXPECT_100_CONTINUE} has precedence over this setting.
+     *
+     * @param enable allows for HTTP Expect:100-Continue or not.
+     * @return updated configuration.
+     */
+    public E expect100Continue(boolean enable) {
+        expect100Continue.set(enable);
+        return self();
+    }
+
+    /**
+     * Set the Expect:100-Continue content-length threshold size.
+     * The {@link ClientProperties#EXPECT_100_CONTINUE_THRESHOLD_SIZE} property has precedence over this setting.
+     *
+     * @param size the content-length threshold.
+     * @return updated configuration.
+     */
+    public E expect100ContinueThreshold(long size) {
+        expect100continueThreshold.set(size);
+        return self();
+    }
+
+    /**
+     * Set to follow redirects. The property {@link ClientProperties#FOLLOW_REDIRECTS} has precedence over this setting.
+     *
+     * @param follow to follow or not to follow.
+     * @return updated configuration.
+     */
+    public E followRedirects(boolean follow) {
+        followRedirects.set(follow);
+        return self();
+    }
+
+    /**
+     * <p>
+     * Set the prefix for the configuration properties used by Client/Request to configure and override the settings.
+     * For instance, if the prefix would be {@code com.example.MyProject.}, the property {@link #connectTimeout(int)}
+     * is overridden only by properties with key starting by the prefix,
+     * i.e. for {@link ClientProperties#CONNECT_TIMEOUT},
+     * the property key {@code com.example.MyProject.jersey.config.client.connectTimeout} would override the setting.
+     * </p>
+     * <p>
+     *     The prefix can be used to override the settings by the System property set specifically for the
+     *     prefixed connector. See {@link org.glassfish.jersey.CommonProperties#ALLOW_SYSTEM_PROPERTIES_PROVIDER}
+     *     for enabling System properties usage.
+     * </p>
+     * <p>
+     * The default configuration prefix is empty.
+     * </p>
+     *
+     * @param prefix the non-null prefix.
+     * @throws NullPointerException if the prefix is null.
+     * @return updated configuration.
+     */
+    public E prefix(String prefix) {
+        this.prefix.set(Objects.requireNonNull(prefix));
+        return self();
+    }
+
+    /**
+     * Set proxy password. The property {@link ClientProperties#PROXY_PASSWORD}
+     * has precedence over this setting.
+     *
+     * @param proxyPassword the proxy password.
+     * @return updated configuration.
+     */
+    public E proxyPassword(String proxyPassword) {
+        this.proxyPassword.set(proxyPassword);
+        return self();
+    }
+
+    /**
+     * Set proxy username. The property {@link ClientProperties#PROXY_USERNAME}
+     * has precedence over this setting.
+     *
+     * @param userName the proxy username.
+     * @return updated configuration.
+     */
+    public E proxyUserName(String userName) {
+        proxyUserName.set(userName);
+        return self();
+    }
+
+    /**
+     * Set proxy URI. The property {@link ClientProperties#PROXY_URI}
+     * has precedence over this setting.
+     *
+     * @param proxyUri the proxy URI.
+     * @return updated configuration.
+     */
+    public E proxyUri(String proxyUri) {
+        this.proxyUri.set(proxyUri);
+        return self();
+    }
+
+    /**
+     * Set proxy URI. The property {@link ClientProperties#PROXY_URI}
+     * has precedence over this setting.
+     *
+     * @param proxyUri the proxy URI.
+     * @return updated configuration.
+     */
+    public E proxyUri(URI proxyUri) {
+        this.proxyUri.set(proxyUri);
+        return self();
+    }
+
+    /**
+     * Set HTTP proxy. The property {@link ClientProperties#PROXY_URI}
+     * has precedence over this setting.
+     *
+     * @param proxy the HTTP proxy.
+     * @return updated configuration.
+     */
+    public E proxy(Proxy proxy) {
+        this.proxyUri.set(proxy);
+        return self();
+    }
+
+    /**
+     * Set read timeout. The property {@link ClientProperties#READ_TIMEOUT}
+     * has precedence over this setting.
+     *
+     * @param millis timeout in milliseconds.
+     * @return updated configuration.
+     */
+    public E readTimeout(int millis) {
+        readTimeout.set(millis);
+        return self();
+    }
+
+    /**
+     * Set the request entity processing type.
+     *
+     * @param requestEntityProcessing the request entity processing type.
+     * @return the updated configuration.
+     */
+    public E requestEntityProcessing(RequestEntityProcessing requestEntityProcessing) {
+        this.requestEntityProcessing.set(requestEntityProcessing);
+        return self();
+    }
+
+    public E sniHostName(String sniHostname) {
+        this.sniHostname.set(sniHostname);
+        return self();
+    }
+
+    /**
+     * Set the {@link SSLContext} supplier. The property {@link ClientProperties#SSL_CONTEXT_SUPPLIER} has precedence over
+     * this setting.
+     *
+     * @param sslContextSupplier the {@link SSLContext} supplier.
+     * @return the updated configuration.
+     */
+    public E sslContextSupplier(Supplier<SSLContext> sslContextSupplier) {
+        this.sslContextSupplier.set(sslContextSupplier);
+        return self();
+    }
+
+    /**
+     * Return type-cast self.
+     * @return self.
+     */
+    @SuppressWarnings("unchecked")
+    protected E self() {
+        return (E) this;
+    }
+
+    /**
+     * <p>
+     * A reference to a value. The reference can be empty, but unlike the {@code Optional}, once a value is set,
+     * it never can be empty again. The {@code null} value is treated as a non-empty value of null.
+     * </p><p>
+     * This {@code null}
+     * can be used to override some previous configuration value, to distinguish the intentional {@code null} override
+     * from an empty (non-set) configuration value.
+     * </p>
+     * @param <T> type of the value.
+     */
+    protected static class NullableRef<T> implements org.glassfish.jersey.internal.util.collection.Ref<T> {
+
+        private NullableRef() {
+            // use factory methods;
+        }
+
+        /**
+         * Return a new empty reference.
+         *
+         * @return an empty reference.
+         * @param <T> The type of the empty value.
+         */
+        public static <T> NullableRef<T> empty() {
+            return new NullableRef<>();
+        }
+
+        /**
+         * Return a reference of a given value.
+         *
+         * @param value the value this reference refers to.*
+         * @return a new reference to a given value.
+         * @param <T> type of the value.
+         */
+        public static <T> NullableRef<T> of(T value) {
+            NullableRef<T> ref = new NullableRef<>();
+            ref.set(value);
+            return ref;
+        }
+
+        private boolean empty = true;
+        private T ref = null;
+
+        @Override
+        public void set(T value) {
+            empty = false;
+            ref = value;
+        }
+
+        /**
+         * Set or replace the value if other value is set.
+         * @param other a reference to another value.
+         */
+        public void setNonEmpty(NullableRef<T> other) {
+            other.ifPresent(this::set);
+        }
+
+        @Override
+        public T get() {
+            return ref;
+        }
+
+        /**
+         * Run action if and only if the condition applies.
+         *
+         * @param predicate the condition to be met.
+         * @param action the action to run if condition is met.
+         */
+        public void iff(Predicate<T> predicate, Runnable action) {
+            if (predicate.test(ref)) {
+                action.run();
+            }
+        }
+
+        /**
+         * If it is empty, sets the {@code value} value. Keeps the original value, otherwise.
+         *
+         * @param value the value to be set if empty.
+         */
+        public void ifEmptySet(T value) {
+            if (empty) {
+                set(value);
+            }
+        }
+
+        /**
+         * If a value is present, performs the given action with the value,
+         * otherwise does nothing.
+         *
+         * @param action the action to be performed, if a value is present
+         * @throws NullPointerException if value is present and the given action is
+         *         {@code null}
+         */
+        public void ifPresent(Consumer<? super T> action) {
+            if (!empty) {
+                action.accept(ref);
+            }
+        }
+
+        /**
+         * If a value is present, performs the given action with the value,
+         * otherwise performs the given empty-based action.
+         *
+         * @param action the action to be performed, if a value is present
+         * @param emptyAction the empty-based action to be performed, if no value is
+         *        present
+         * @throws NullPointerException if a value is present and the given action
+         *         is {@code null}, or no value is present and the given empty-based
+         *         action is {@code null}.
+         */
+        public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) {
+            if (!empty) {
+                action.accept(ref);
+            } else {
+                emptyAction.run();
+            }
+        }
+
+        /**
+         * Return the value if present, the {@code other} otherwise.
+         *
+         * @param other the value if not present
+         * @return inner value if present or the other otherwise.
+         */
+        public T ifPresentOrElse(T other) {
+            return empty ? other : ref;
+        }
+
+        /**
+         * If a value is  not present, returns {@code true}, otherwise
+         * {@code false}.
+         *
+         * @return  {@code true} if a value is not present, otherwise {@code false}
+         */
+        public boolean isEmpty() {
+            return empty;
+        }
+
+        /**
+         * If a value is present, returns {@code true}, otherwise {@code false}.
+         *
+         * @return {@code true} if a value is present, otherwise {@code false}
+         */
+        public boolean isPresent() {
+            return !empty;
+        }
+
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(ref, empty);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+
+            if (!(o instanceof NullableRef)) {
+                return false;
+            }
+
+            NullableRef<?> that = (NullableRef<?>) o;
+            return Objects.equals(empty, that.empty) && Objects.equals(ref, that.ref);
+        }
+
+        @Override
+        public String toString() {
+            return empty ? "<empty>" : ref == null ? "<null>" : ref.toString();
+        }
+    }
+
+    protected interface Read<CC extends ConnectorConfiguration<CC> & Read<CC>>
+            extends SSLParamConfigurator.SSLParamConfiguratorConfiguration {
+
+        /**
+         * Set and replace the values of current configuration by values of other configuration
+         * if and only if the values of other configuration are set.
+         *
+         * @param other another configuration instance.
+         */
+        public default <X extends ConnectorConfiguration<?>> void setNonEmpty(X other) {
+            me().connectTimeout.setNonEmpty(other.connectTimeout);
+            me().expect100Continue.setNonEmpty(other.expect100Continue);
+            me().expect100continueThreshold.setNonEmpty(other.expect100continueThreshold);
+            me().followRedirects.setNonEmpty(other.followRedirects);
+            me().prefix.setNonEmpty(other.prefix);
+            me().proxyUri.setNonEmpty(other.proxyUri);
+            me().proxyUserName.setNonEmpty(other.proxyUserName);
+            me().proxyPassword.setNonEmpty(other.proxyPassword);
+            me().readTimeout.setNonEmpty(other.readTimeout);
+            me().requestEntityProcessing.setNonEmpty(other.requestEntityProcessing);
+            me().sniHostname.setNonEmpty(other.sniHostname);
+            me().sslContextSupplier.setNonEmpty(other.sslContextSupplier);
+            me().threadPoolSize.setNonEmpty(other.threadPoolSize);
+        }
+
+        /**
+         * Return the thread-pool size setting.
+         *
+         * @return the thread pool size setting.
+         */
+        public default Integer asyncThreadPoolSize() {
+            return me().threadPoolSize.get();
+        }
+
+        /**
+         * Update connect timeout value based on request properties settings.
+         *
+         * @param request the current HTTP client request.
+         * @return the updated configuration.
+         */
+        public default int connectTimeout(ClientRequest request) {
+            me().connectTimeout.set(
+                    request.resolveProperty(prefixed(ClientProperties.CONNECT_TIMEOUT), me().connectTimeout.get())
+            );
+            return me().connectTimeout.get();
+        }
+
+        /**
+         * Get the value of connect timeout setting.
+         *
+         * @return connect timeout value.
+         */
+        public default int connectTimeout() {
+            return me().connectTimeout.get();
+        }
+
+        /**
+         * Sets the default value. The default methods cannot be set on instances passed by the customers using
+         * {@link ClientProperties#CONNECTOR_CONFIGURATION} since they would override the values previously set
+         * by the connector configuration object.
+         * @return the initialized configuration object.
+         */
+        public default CC init() {
+            me().connectTimeout(0)
+                    .expect100ContinueThreshold(ClientProperties.DEFAULT_EXPECT_100_CONTINUE_THRESHOLD_SIZE)
+                    .followRedirects(Boolean.TRUE)
+                    .prefix("")
+                    .readTimeout(0);
+            return me();
+        }
+
+        /**
+         * Utility method to create a new instance of configuration to preserve the settings of previous configuration.
+         *
+         * @return a new instance of the configuration.
+         */
+        public default CC copy() {
+            CC config = instance();
+            config.init();
+            config.setNonEmpty(me());
+            return config;
+        }
+
+        public default CC copyFromClient(Configuration configuration) {
+            CC clientConfiguration = copy();
+            final Map<String, Object> properties = configuration.getProperties();
+            Object configProp = properties.get(clientConfiguration.prefixed(ClientProperties.CONNECTOR_CONFIGURATION));
+            if (configProp != null) {
+                ConnectorConfiguration<?> clientCfg = (ConnectorConfiguration<?>) configProp;
+                if (me().prefix.equals(clientCfg.prefix) || clientCfg.prefix.get() == null) {
+                    clientConfiguration.setNonEmpty(clientCfg);
+                }
+            } else {
+                configProp = properties.get(ClientProperties.CONNECTOR_CONFIGURATION);
+                if (configProp != null && me().prefix.equals(((ConnectorConfiguration<?>) configProp).prefix)) {
+                    clientConfiguration.setNonEmpty((ConnectorConfiguration<?>) configProp);
+                }
+            }
+            return clientConfiguration;
+        }
+
+        public default CC copyFromRequest(ClientRequest request) {
+            CC requestConfiguration = copy();
+            Object configProp = request.getProperty(prefixed(ClientProperties.CONNECTOR_CONFIGURATION));
+            if (configProp != null) {
+                ConnectorConfiguration<?> requestCfg = (ConnectorConfiguration<?>) configProp;
+                if (me().prefix.equals(requestCfg.prefix) || requestCfg.prefix.get() == null) {
+                    requestConfiguration.setNonEmpty(requestCfg);
+                }
+            } else {
+                configProp = request.getProperty(ClientProperties.CONNECTOR_CONFIGURATION);
+                if (configProp != null && me().prefix.equals(((ConnectorConfiguration<?>) configProp).prefix)) {
+                    requestConfiguration.setNonEmpty((ConnectorConfiguration<?>) configProp);
+                }
+            }
+            return requestConfiguration;
+        }
+
+        @Override
+        default String getSniHostNameProperty(Configuration configuration) {
+            Object property = configuration.getProperty(prefixed(ClientProperties.SNI_HOST_NAME));
+            if (property == null) {
+                property = configuration.getProperty(prefixed(ClientProperties.SNI_HOST_NAME.toLowerCase(Locale.ROOT)));
+            }
+            return property == null ? me().sniHostname.get() : (String) property;
+        }
+
+        /**
+         * Update the {@link #expect100Continue(boolean)} from the HTTP client request.
+         *
+         * @param request the HTTP client request.
+         * @return the Expect: 100-Continue support value.
+         */
+        public default Boolean expect100Continue(ClientRequest request) {
+            final Boolean expectContinueActivated =
+                    request.resolveProperty(prefixed(ClientProperties.EXPECT_100_CONTINUE), Boolean.class);
+            if (expectContinueActivated != null) {
+                me().expect100Continue.set(expectContinueActivated);
+            }
+            return me().expect100Continue.get();
+        }
+
+        /**
+         * Update the {@link #expect100ContinueThreshold(long)} from the HTTP client request.
+         *
+         * @param request the HTTP client request.
+         * @return the content length threshold size.
+         */
+        public default long expect100ContinueThreshold(ClientRequest request) {
+            me().expect100continueThreshold.set(
+                    request.resolveProperty(prefixed(ClientProperties.EXPECT_100_CONTINUE_THRESHOLD_SIZE),
+                            me().expect100continueThreshold.get())
+            );
+            return me().expect100continueThreshold.get();
+        }
+
+        /**
+         * Update the {@link #followRedirects(boolean)} setting from the HTTP client request. The default is {@code true}.
+         *
+         * @param request the HTTP client request.
+         * @return follow redirects setting.
+         */
+        public default boolean followRedirects(ClientRequest request) {
+            me().followRedirects.set(
+                    request.resolveProperty(prefixed(ClientProperties.FOLLOW_REDIRECTS), me().followRedirects.get())
+            );
+            return me().followRedirects.get();
+        }
+
+        /**
+         * Get the value of the follow redirects setting. The default is {@code true}.
+         *
+         * @return whether to follow redirects or not.
+         */
+        public default boolean followRedirects() {
+            return me().followRedirects.get();
+        }
+
+        public default Configuration prefixedConfiguration(Configuration configuration) {
+            return me().prefix.get().isEmpty() ? configuration : new PrefixedConfiguration(me().prefix.get(), configuration);
+        }
+
+        /**
+         * Create optional client proxy information based on the proxy information set in the configuration
+         * or the HTTP client request. The used settings are {@link #proxy(Proxy)},
+         * {@link #proxyUri(URI)}, {@link #proxyUri(String)}, {@link #proxyUserName(String)},
+         * and {@link #proxyPassword(String)}.
+         *
+         * @param request the HTTP client request,
+         * @param requestUri the HTTP request URI. It can differ from the URI used in the request, based on other
+         *                   information set by the HTTP client request.
+         * @return the optional client proxy.
+         */
+        public default Optional<ClientProxy> proxy(ClientRequest request, URI requestUri) {
+            Optional<ClientProxy> proxy = ClientProxy.proxyFromRequest(
+                    me().prefix.get().isEmpty()
+                        ? request
+                        : new PrefixedPropertiesResolver(me().prefix.get(), request)
+            );
+            if (!proxy.isPresent() && me().proxyUri.isPresent()) {
+                final Map<String, Object> properties = me().prefix.get().isEmpty()
+                        ? new HashMap<>()
+                        : new PrefixedMap<>(me().prefix.get(), new HashMap<>());
+                properties.put(me().prefix.get() + ClientProperties.PROXY_URI, me().proxyUri.get());
+                properties.put(me().prefix.get() + ClientProperties.PROXY_USERNAME, me().proxyUserName.get());
+                properties.put(me().prefix.get() + ClientProperties.PROXY_PASSWORD, me().proxyPassword.get());
+                request.getPropertyNames().forEach(k -> properties.put(k, request.getProperty(k)));
+                proxy = ClientProxy.proxyFromProperties(properties);
+            }
+            if (!proxy.isPresent()) {
+                proxy = ClientProxy.proxyFromUri(requestUri);
+            }
+            return proxy;
+        }
+
+        /**
+         * Update {@link #readTimeout(int) read timeout} based on the HTTP request properties.
+         *
+         * @param request the current HTTP client request.
+         * @return updated configuration.
+         */
+        public default CC readTimeout(ClientRequest request) {
+            me().readTimeout.set(request.resolveProperty(prefixed(ClientProperties.READ_TIMEOUT), me().readTimeout.get()));
+            return me();
+        }
+
+        /**
+         * Get the value of preset {@link #readTimeout(int)}.
+         *
+         * @return the read timeout milliseconds.
+         */
+        public default int readTimeout() {
+            return me().readTimeout.get();
+        }
+
+        /**
+         * Get the {@link RequestEntityProcessing} updated by the HTTP client request.
+         *
+         * @param request the HTTP client request.
+         * @return the RequestEntityProcessing type.
+         */
+        public default RequestEntityProcessing requestEntityProcessing(ClientRequest request) {
+            RequestEntityProcessing entityProcessing =
+                    request.resolveProperty(prefixed(ClientProperties.REQUEST_ENTITY_PROCESSING), RequestEntityProcessing.class);
+            if (entityProcessing == null) {
+                entityProcessing = me().requestEntityProcessing.get();
+            }
+            return entityProcessing;
+        }
+
+        @Override
+        default String resolveSniHostNameProperty(PropertiesResolver resolver) {
+            String property = resolver.resolveProperty(prefixed(ClientProperties.SNI_HOST_NAME), String.class);
+            if (property == null) {
+                property = resolver.resolveProperty(
+                        prefixed(ClientProperties.SNI_HOST_NAME.toLowerCase(Locale.ROOT)), String.class);
+            }
+            return property == null ? me().sniHostname.get() : property;
+        }
+
+        /**
+         * Get {@link SSLContext} either from the {@link ClientProperties#SSL_CONTEXT_SUPPLIER}, or from this configuration,
+         * or from the {@link Client#getSslContext()} in this order.
+         *
+         * @param client the client used to get the {@link SSLContext}.
+         * @param request the request used to get the {@link SSLContext}.
+         * @return the {@link SSLContext}.
+         */
+        public default SSLContext sslContext(Client client, ClientRequest request) {
+            @SuppressWarnings("unchecked")
+            Supplier<SSLContext> supplier =
+                    request.resolveProperty(prefixed(ClientProperties.SSL_CONTEXT_SUPPLIER), Supplier.class);
+            if (supplier == null) {
+                supplier = me().sslContextSupplier.get();
+            }
+            return supplier == null ? client.getSslContext() : supplier.get();
+        }
+
+        public default String prefixed(String propertyName) {
+            return me().prefix.get() + propertyName;
+        }
+
+        /**
+         * Return a new instance of configuration.
+         * @return a new instance of configuration.
+         */
+        public CC instance();
+
+        /**
+         * Return typed-cast self.
+         * @return self.
+         */
+        public CC me();
+    }
+
+
+    /**
+     * A properties map that works with prefixed properties.
+     *
+     * @param <V> Object type.
+     */
+    private static class PrefixedMap<V> implements Map<String, V> {
+        private final Map<String, V> inner;
+        private final String prefix;
+
+        private PrefixedMap(String prefix, Map<String, V> inner) {
+            this.inner = inner;
+            this.prefix = prefix;
+        }
+
+        @Override
+        public int size() {
+            return inner.size();
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return inner.isEmpty();
+        }
+
+        @Override
+        public boolean containsKey(Object key) {
+            return inner.containsKey(prefix + key);
+        }
+
+        @Override
+        public boolean containsValue(Object value) {
+            return inner.containsValue(value);
+        }
+
+        @Override
+        public V get(Object key) {
+            return inner.get(prefix + key);
+        }
+
+        @Override
+        public V put(String key, V value) {
+            return inner.put(key, value);
+        }
+
+        @Override
+        public V remove(Object key) {
+            return inner.remove(prefix + key);
+        }
+
+        @Override
+        public void putAll(Map<? extends String, ? extends V> m) {
+            inner.putAll(m);
+        }
+
+        @Override
+        public void clear() {
+            inner.clear();
+        }
+
+        @Override
+        public Set<String> keySet() {
+            return inner.keySet();
+        }
+
+        @Override
+        public Collection<V> values() {
+            return inner.values();
+        }
+
+        @Override
+        public Set<Entry<String, V>> entrySet() {
+            return inner.entrySet();
+        }
+    }
+
+    /**
+     * Properties resolver that resolves prefixed properties.
+     */
+    private static class PrefixedPropertiesResolver implements PropertiesResolver {
+        private final String prefix;
+        private final PropertiesResolver resolver;
+
+        private PrefixedPropertiesResolver(String prefix, PropertiesResolver resolver) {
+            this.prefix = prefix;
+            this.resolver = resolver;
+        }
+
+        @Override
+        public <T> T resolveProperty(String name, Class<T> type) {
+            return resolver.resolveProperty(prefix + name, type);
+        }
+
+        @Override
+        public <T> T resolveProperty(String name, T defaultValue) {
+            return resolver.resolveProperty(prefix + name, defaultValue);
+        }
+    }
+
+    protected static class PrefixedConfiguration implements Configuration {
+        private final String prefix;
+        private final Configuration inner;
+
+        private PrefixedConfiguration(String prefix, Configuration inner) {
+            this.prefix = prefix;
+            this.inner = inner;
+        }
+
+        @Override
+        public RuntimeType getRuntimeType() {
+            return inner.getRuntimeType();
+        }
+
+        @Override
+        public Map<String, Object> getProperties() {
+            return new PrefixedMap<>(prefix, inner.getProperties());
+        }
+
+        @Override
+        public Object getProperty(String name) {
+            return inner.getProperty(prefix + name);
+        }
+
+        @Override
+        public Collection<String> getPropertyNames() {
+            return inner.getPropertyNames();
+        }
+
+        @Override
+        public boolean isEnabled(Feature feature) {
+            return inner.isEnabled(feature);
+        }
+
+        @Override
+        public boolean isEnabled(Class<? extends Feature> featureClass) {
+            return inner.isEnabled(featureClass);
+        }
+
+        @Override
+        public boolean isRegistered(Object component) {
+            return inner.isRegistered(component);
+        }
+
+        @Override
+        public boolean isRegistered(Class<?> componentClass) {
+            return inner.isRegistered(componentClass);
+        }
+
+        @Override
+        public Map<Class<?>, Integer> getContracts(Class<?> componentClass) {
+            return inner.getContracts(componentClass);
+        }
+
+        @Override
+        public Set<Class<?>> getClasses() {
+            return inner.getClasses();
+        }
+
+        @Override
+        public Set<Object> getInstances() {
+            return inner.getInstances();
+        }
+    }
+}
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
index 212e83d..93e1cb7 100644
--- a/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
+++ b/core-client/src/main/java/org/glassfish/jersey/client/innate/http/SSLParamConfigurator.java
@@ -20,7 +20,6 @@
 import org.glassfish.jersey.client.ClientRequest;
 import org.glassfish.jersey.http.HttpHeaders;
 import org.glassfish.jersey.internal.PropertiesResolver;
-import org.glassfish.jersey.internal.guava.InetAddresses;
 
 import javax.net.ssl.SSLEngine;
 import javax.net.ssl.SSLParameters;
@@ -52,6 +51,11 @@
         private String sniHostNameHeader = null;
         private String sniHostPrecedence = null;
         private boolean setAlways = false;
+        private final SSLParamConfiguratorConfiguration configConfiguration;
+
+        public Builder(SSLParamConfiguratorConfiguration configuration) {
+            this.configConfiguration = configuration;
+        }
 
 
         /**
@@ -61,7 +65,7 @@
          */
         public Builder request(ClientRequest clientRequest) {
             this.sniHostNameHeader = getSniHostNameHeader(clientRequest.getHeaders());
-            this.sniHostPrecedence = resolveSniHostNameProperty(clientRequest);
+            this.sniHostPrecedence = configConfiguration.resolveSniHostNameProperty(clientRequest);
             this.uri = clientRequest.getUri();
             return this;
         }
@@ -72,7 +76,7 @@
          * @return the builder instance
          */
         public Builder configuration(Configuration configuration) {
-            this.sniHostPrecedence = getSniHostNameProperty(configuration);
+            this.sniHostPrecedence = this.configConfiguration.getSniHostNameProperty(configuration);
             return this;
         }
 
@@ -141,7 +145,7 @@
          * @return the builder instance.
          */
         public Builder setSNIHostName(Configuration configuration) {
-            return setSNIHostName(getSniHostNameProperty(configuration));
+            return setSNIHostName(this.configConfiguration.getSniHostNameProperty(configuration));
         }
 
         /**
@@ -160,7 +164,7 @@
          * @return the builder instance.
          */
         public Builder setSNIHostName(PropertiesResolver resolver) {
-            return setSNIHostName(resolveSniHostNameProperty(resolver));
+            return setSNIHostName(configConfiguration.resolveSniHostNameProperty(resolver));
         }
 
         /**
@@ -180,22 +184,6 @@
             final String hostHeader = hostHeaders.get(0).toString();
             return hostHeader;
         }
-
-        private static String resolveSniHostNameProperty(PropertiesResolver resolver) {
-            String property = resolver.resolveProperty(ClientProperties.SNI_HOST_NAME, String.class);
-            if (property == null) {
-                property = resolver.resolveProperty(ClientProperties.SNI_HOST_NAME.toLowerCase(Locale.ROOT), String.class);
-            }
-            return property;
-        }
-
-        private static String getSniHostNameProperty(Configuration configuration) {
-            Object property = configuration.getProperty(ClientProperties.SNI_HOST_NAME);
-            if (property == null) {
-                property = configuration.getProperty(ClientProperties.SNI_HOST_NAME.toLowerCase(Locale.ROOT));
-            }
-            return (String) property;
-        }
     }
 
     private SSLParamConfigurator(SSLParamConfigurator.Builder builder) {
@@ -212,7 +200,14 @@
      * Create a new instance of TlsSupport class
      **/
     public static SSLParamConfigurator.Builder builder() {
-        return new SSLParamConfigurator.Builder();
+        return new SSLParamConfigurator.Builder(DEFAULT_CONFIGURATION);
+    }
+
+    /**
+     * Create a new instance of TlsSupport class
+     **/
+    public static SSLParamConfigurator.Builder builder(SSLParamConfiguratorConfiguration configuration) {
+        return new SSLParamConfigurator.Builder(configuration);
     }
 
     /**
@@ -290,4 +285,25 @@
         sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
         sslEngine.setSSLParameters(sslParameters);
     }
+
+    public static interface SSLParamConfiguratorConfiguration {
+        public default String getSniHostNameProperty(Configuration configuration) {
+            Object property = configuration.getProperty(ClientProperties.SNI_HOST_NAME);
+            if (property == null) {
+                property = configuration.getProperty(ClientProperties.SNI_HOST_NAME.toLowerCase(Locale.ROOT));
+            }
+            return (String) property;
+        }
+
+        public default String resolveSniHostNameProperty(PropertiesResolver resolver) {
+            String property = resolver.resolveProperty(ClientProperties.SNI_HOST_NAME, String.class);
+            if (property == null) {
+                property = resolver.resolveProperty(ClientProperties.SNI_HOST_NAME.toLowerCase(Locale.ROOT), String.class);
+            }
+            return property;
+        }
+    }
+
+    private static final SSLParamConfiguratorConfiguration DEFAULT_CONFIGURATION = new SSLParamConfiguratorConfiguration() {
+    };
 }
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 630cb96..e1fd816 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 2025 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,6 +52,7 @@
 import javax.net.ssl.SSLSocketFactory;
 import jakarta.ws.rs.ProcessingException;
 import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Configuration;
 import jakarta.ws.rs.core.MultivaluedMap;
 import jakarta.ws.rs.core.Response;
 
@@ -81,7 +82,6 @@
 public class HttpUrlConnector implements Connector {
 
     private static final Logger LOGGER = Logger.getLogger(HttpUrlConnector.class.getName());
-    private static final String ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY = "sun.net.http.allowRestrictedHeaders";
     // Avoid multi-thread uses of HttpsURLConnection.getDefaultSSLSocketFactory() because it does not implement a
     // proper lazy-initialization. See https://github.com/jersey/jersey/issues/3293
     private static final LazyValue<SSLSocketFactory> DEFAULT_SSL_SOCKET_FACTORY =
@@ -110,11 +110,7 @@
         }
     }
 
-    private final HttpUrlConnectorProvider.ConnectionFactory connectionFactory;
-    private final int chunkSize;
-    private final boolean fixLengthStreaming;
-    private final boolean setMethodWorkaround;
-    private final boolean isRestrictedHeaderPropertySet;
+    private final HttpUrlConnectorConfiguration.ReadWrite clientConfig;
     private Value<SSLSocketFactory> sslSocketFactory;
 
     // SSLContext#getSocketFactory not idempotent
@@ -131,7 +127,7 @@
      * @param client              JAX-RS client instance for which the connector is being created.
      * @param connectionFactory   {@link javax.net.ssl.HttpsURLConnection} factory to be used when creating connections.
      * @param chunkSize           chunk size to use when using HTTP chunked transfer coding.
-     * @param fixLengthStreaming  specify if the the {@link java.net.HttpURLConnection#setFixedLengthStreamingMode(int)
+     * @param fixLengthStreaming  specify if the {@link java.net.HttpURLConnection#setFixedLengthStreamingMode(int)
      *                            fixed-length streaming mode} on the underlying HTTP URL connection instances should be
      *                            used when sending requests.
      * @param setMethodWorkaround specify if the reflection workaround should be used to set HTTP URL connection method
@@ -144,29 +140,23 @@
             final boolean fixLengthStreaming,
             final boolean setMethodWorkaround) {
 
-        this.connectionFactory = connectionFactory;
-        this.chunkSize = chunkSize;
-        this.fixLengthStreaming = fixLengthStreaming;
-        this.setMethodWorkaround = setMethodWorkaround;
+        this(client, client.getConfiguration(),
+                HttpUrlConnectorProvider.config()
+                    .connectionFactory(connectionFactory)
+                    .chunkSize(chunkSize)
+                    .useFixedLengthStreaming(fixLengthStreaming)
+                    .useSetMethodWorkaround(setMethodWorkaround)
+        );
+    }
 
+    public HttpUrlConnector(Client client, Configuration configuration, HttpUrlConnectorConfiguration<?> config) {
+        this.clientConfig = config.rw().fromClient(configuration);
         this.sslSocketFactory = Values.lazy(new Value<SSLSocketFactory>() {
             @Override
             public SSLSocketFactory get() {
                 return client.getSslContext().getSocketFactory();
             }
         });
-
-        // check if sun.net.http.allowRestrictedHeaders system property has been set and log the result
-        // the property is being cached in the HttpURLConnection, so this is only informative - there might
-        // already be some connection(s), that existed before the property was set/changed.
-        isRestrictedHeaderPropertySet = Boolean.valueOf(AccessController.doPrivileged(
-                PropertiesHelper.getSystemProperty(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY, "false")
-        ));
-
-        LOGGER.config(isRestrictedHeaderPropertySet
-                        ? LocalizationMessages.RESTRICTED_HEADER_PROPERTY_SETTING_TRUE(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY)
-                        : LocalizationMessages.RESTRICTED_HEADER_PROPERTY_SETTING_FALSE(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY)
-        );
     }
 
     private static InputStream getInputStream(final HttpURLConnection uc, final ClientRequest clientRequest) throws IOException {
@@ -342,9 +332,15 @@
      * @param clientRequest the actual client request.
      * @param uc     http connection to be secured.
      */
-    private void secureConnection(
-            final ClientRequest clientRequest, final HttpURLConnection uc, final SSLParamConfigurator sniConfig) {
-        setSslContextFactory(clientRequest.getClient(), clientRequest);
+    private void secureConnection(final ClientRequest clientRequest,
+                                  final HttpURLConnection uc,
+                                  final SSLParamConfigurator sniConfig,
+                                  final HttpUrlConnectorConfiguration.ReadWrite config) {
+        if (config.isSslContextSupplier() || config.isPrefixed()) {
+            setSslContextFactory(clientRequest.getClient(), clientRequest, config.sslContext(clientRequest));
+        } else {
+            setSslContextFactory(clientRequest.getClient(), clientRequest);
+        }
         secureConnection(clientRequest.getClient(), uc); // keep this for compatibility
 
         if (sniConfig.isSNIRequired() && uc instanceof HttpsURLConnection) { // set SNI
@@ -355,9 +351,17 @@
         }
     }
 
+    @Deprecated
     protected void setSslContextFactory(Client client, ClientRequest request) {
-        final Supplier<SSLContext> supplier = request.resolveProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, Supplier.class);
+        setSslContextFactory(client, request, request.resolveProperty(ClientProperties.SSL_CONTEXT_SUPPLIER, Supplier.class));
+    }
 
+    protected void setSslContextFactory(Client client, ClientRequest request,
+                                        HttpUrlConnectorConfiguration.ReadWrite requestConfig) {
+        setSslContextFactory(client, request, requestConfig.sslContext(request));
+    }
+
+    private void setSslContextFactory(Client client, ClientRequest request, Supplier<SSLContext> supplier) {
         if (supplier != null) {
             sslSocketFactory = Values.lazy(new Value<SSLSocketFactory>() { // lazy for double-check locking if multiple requests
                 @Override
@@ -375,9 +379,10 @@
     }
 
     private ClientResponse _apply(final ClientRequest request) throws IOException {
+        HttpUrlConnectorConfiguration.ReadWrite requestConfiguration = clientConfig.fromRequest(request);
+
         final HttpURLConnection uc;
-        final Optional<ClientProxy> proxy = ClientProxy.proxyFromRequest(request);
-        final SSLParamConfigurator sniConfig = SSLParamConfigurator.builder().request(request)
+        final SSLParamConfigurator sniConfig = SSLParamConfigurator.builder(requestConfiguration).request(request)
                 .setSNIHostName(request).build();
         final URI sniUri;
         if (sniConfig.isSNIRequired()) {
@@ -391,39 +396,45 @@
             DEFAULT_SSL_SOCKET_FACTORY.get();
         }
 
+        final Optional<ClientProxy> proxy = requestConfiguration.proxy(request, sniUri);
         proxy.ifPresent(clientProxy -> ClientProxy.setBasicAuthorizationHeader(request.getHeaders(), proxy.get()));
-        uc = this.connectionFactory.getConnection(sniUri.toURL(), proxy.isPresent() ? proxy.get().proxy() : null);
+        uc = ((Supplier<HttpUrlConnectorProvider.ConnectionFactory>) requestConfiguration.connectionFactory)
+                .get().getConnection(sniUri.toURL(), proxy.isPresent() ? proxy.get().proxy() : null);
         uc.setDoInput(true);
 
         final String httpMethod = request.getMethod();
-        if (request.resolveProperty(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, setMethodWorkaround)) {
+        if (requestConfiguration.isMethodWorkaround(request)) {
             setRequestMethodViaJreBugWorkaround(uc, httpMethod);
         } else {
             uc.setRequestMethod(httpMethod);
         }
 
-        uc.setInstanceFollowRedirects(request.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, true));
+        uc.setInstanceFollowRedirects(requestConfiguration.followRedirects(request));
 
-        uc.setConnectTimeout(request.resolveProperty(ClientProperties.CONNECT_TIMEOUT, uc.getConnectTimeout()));
+        if (requestConfiguration.connectTimeout() == 0) {
+            requestConfiguration.connectTimeout(uc.getConnectTimeout());
+        }
+        uc.setConnectTimeout(requestConfiguration.connectTimeout(request));
 
-        uc.setReadTimeout(request.resolveProperty(ClientProperties.READ_TIMEOUT, uc.getReadTimeout()));
+        if (requestConfiguration.readTimeout() == 0) {
+            requestConfiguration.readTimeout(uc.getReadTimeout());
+        }
+        uc.setReadTimeout(requestConfiguration.readTimeout(request).readTimeout());
 
-        secureConnection(request, uc, sniConfig);
+        secureConnection(request, uc, sniConfig, requestConfiguration);
 
         final Object entity = request.getEntity();
         Exception storedException = null;
         try {
             if (entity != null) {
-                RequestEntityProcessing entityProcessing = request.resolveProperty(
-                        ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.class);
-
+                RequestEntityProcessing entityProcessing = requestConfiguration.requestEntityProcessing(request);
                 final long length = request.getLengthLong();
 
-                if (entityProcessing == null || entityProcessing != RequestEntityProcessing.BUFFERED) {
-                    if (fixLengthStreaming && length > 0) {
+                if (entityProcessing != RequestEntityProcessing.BUFFERED) {
+                    if (requestConfiguration.useFixedLengthStreaming.get() && length > 0) {
                         uc.setFixedLengthStreamingMode(length);
                     } else if (entityProcessing == RequestEntityProcessing.CHUNKED) {
-                        uc.setChunkedStreamingMode(chunkSize);
+                        uc.setChunkedStreamingMode(requestConfiguration.chunkSize.get());
                     }
                 }
                 uc.setDoOutput(true);
@@ -438,13 +449,13 @@
                 processExtensions(request, uc);
 
                 request.setStreamProvider(contentLength -> {
-                    setOutboundHeaders(request.getStringHeaders(), uc);
+                    setOutboundHeaders(request.getStringHeaders(), uc, requestConfiguration);
                     return uc.getOutputStream();
                 });
                 request.writeEntity();
 
             } else {
-                setOutboundHeaders(request.getStringHeaders(), uc);
+                setOutboundHeaders(request.getStringHeaders(), uc, requestConfiguration);
             }
         } catch (IOException ioe) {
             storedException = handleException(request, ioe, uc);
@@ -496,7 +507,9 @@
         return responseContext;
     }
 
-    private void setOutboundHeaders(MultivaluedMap<String, String> headers, HttpURLConnection uc) {
+    private void setOutboundHeaders(MultivaluedMap<String, String> headers,
+                                    HttpURLConnection uc,
+                                    HttpUrlConnectorConfiguration.ReadWrite requestConfiguration) {
         boolean restrictedSent = false;
         for (Map.Entry<String, List<String>> header : headers.entrySet()) {
             String headerName = header.getKey();
@@ -520,14 +533,15 @@
                 uc.setRequestProperty(headerName, headerValue);
             }
             // if (at least one) restricted header was added and the allowRestrictedHeaders
-            if (!isRestrictedHeaderPropertySet && !restrictedSent) {
+            if (!requestConfiguration.isRestrictedHeaderPropertySet.get() && !restrictedSent) {
                 if (isHeaderRestricted(headerName, headerValue)) {
                     restrictedSent = true;
                 }
             }
         }
         if (restrictedSent) {
-            LOGGER.warning(LocalizationMessages.RESTRICTED_HEADER_POSSIBLY_IGNORED(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY));
+            LOGGER.warning(LocalizationMessages.RESTRICTED_HEADER_POSSIBLY_IGNORED(
+                    HttpUrlConnectorConfiguration.ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY));
         }
     }
 
@@ -635,7 +649,7 @@
 
     private static class SniSSLSocketFactory extends SSLSocketFactory {
         private final SSLSocketFactory socketFactory;
-        private ThreadLocal<SSLParamConfigurator> sniConfigs = new ThreadLocal<>();
+        private final ThreadLocal<SSLParamConfigurator> sniConfigs = new ThreadLocal<>();
 
         public void setSniConfig(SSLParamConfigurator sniConfigs) {
             this.sniConfigs.set(sniConfigs);
diff --git a/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnectorConfiguration.java b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnectorConfiguration.java
new file mode 100644
index 0000000..20c31f7
--- /dev/null
+++ b/core-client/src/main/java/org/glassfish/jersey/client/internal/HttpUrlConnectorConfiguration.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (c) 2025 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.internal;
+
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.HttpUrlConnectorProvider;
+import org.glassfish.jersey.client.innate.ConnectorConfiguration;
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+import org.glassfish.jersey.internal.util.collection.Ref;
+
+import javax.net.ssl.SSLContext;
+import jakarta.ws.rs.core.Configuration;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.Proxy;
+import java.net.URL;
+import java.security.AccessController;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
+import java.util.logging.Logger;
+
+public abstract class HttpUrlConnectorConfiguration<C extends HttpUrlConnectorConfiguration<C>>
+        extends ConnectorConfiguration<C> {
+    private static final Logger LOGGER = Logger.getLogger(HttpUrlConnectorConfiguration.class.getName());
+    /* package */ static final String ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY = "sun.net.http.allowRestrictedHeaders";
+    /**
+     * Default connection factory to be used.
+     */
+    protected static final HttpUrlConnectorProvider.ConnectionFactory DEFAULT_CONNECTION_FACTORY =
+            new DefaultConnectionFactory();
+
+    protected NullableRef<HttpUrlConnectorProvider.ConnectionFactory> connectionFactory = NullableRef.empty();
+    protected Ref<Integer> chunkSize = NullableRef.empty();
+    /* package */ Ref<Boolean> isRestrictedHeaderPropertySet = NullableRef.empty();
+    protected Ref<Boolean> useFixedLengthStreaming = NullableRef.empty();
+    protected Ref<Boolean> useSetMethodWorkaround = NullableRef.empty();
+
+    protected void preInit(Map<String, Object> properties) {
+        connectionFactory.ifEmptySet(DEFAULT_CONNECTION_FACTORY);
+        ((NullableRef<Integer>) chunkSize).ifEmptySet(ClientProperties.DEFAULT_CHUNK_SIZE);
+        ((NullableRef<Boolean>) useFixedLengthStreaming).ifEmptySet(Boolean.FALSE);
+        ((NullableRef<Boolean>) useSetMethodWorkaround).ifEmptySet(Boolean.FALSE);
+
+        int computedChunkSize = ClientProperties.getValue(properties,
+                _prefixed(ClientProperties.CHUNKED_ENCODING_SIZE), chunkSize.get(), Integer.class);
+        if (computedChunkSize < 0) {
+            LOGGER.warning(LocalizationMessages.NEGATIVE_CHUNK_SIZE(computedChunkSize, chunkSize.get()));
+        } else {
+            chunkSize.set(computedChunkSize);
+        }
+
+        useFixedLengthStreaming(ClientProperties.getValue(properties,
+                _prefixed(HttpUrlConnectorProvider.USE_FIXED_LENGTH_STREAMING),
+                useFixedLengthStreaming.get(), Boolean.class));
+        useSetMethodWorkaround(ClientProperties.getValue(properties,
+                _prefixed(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND),
+                useSetMethodWorkaround.get(), Boolean.class));
+    }
+
+    private String _prefixed(String property) {
+        return prefix.ifPresentOrElse("") + property;
+    }
+
+    /**
+     * Set a custom {@link java.net.HttpURLConnection} factory.
+     *
+     * @param connectionFactory custom HTTP URL connection factory. Must not be {@code null}.
+     * @return updated configuration.
+     * @throws java.lang.NullPointerException in case the supplied connectionFactory is {@code null}.
+     */
+    public C connectionFactory(final HttpUrlConnectorProvider.ConnectionFactory connectionFactory) {
+        if (connectionFactory == null) {
+            throw new NullPointerException(LocalizationMessages.NULL_INPUT_PARAMETER("connectionFactory"));
+        }
+
+        this.connectionFactory.set(connectionFactory);
+        return self();
+    }
+
+    /**
+     * Set chunk size for requests transferred using a
+     * <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1">HTTP chunked transfer coding</a>.
+     *
+     * If no value is set, the default chunk size of 4096 bytes will be used.
+     * <p>
+     * Note that this programmatically set value can be overridden by
+     * setting the {@link org.glassfish.jersey.client.ClientProperties#CHUNKED_ENCODING_SIZE} property
+     * specified in the Jersey client instance configuration.
+     * </p>
+     *
+     * @param chunkSize chunked transfer coding chunk size to be used.
+     * @return updated configuration.
+     * @throws java.lang.IllegalArgumentException in case the specified chunk size is negative.
+     */
+    public C chunkSize(final int chunkSize) {
+        if (chunkSize < 0) {
+            throw new IllegalArgumentException(LocalizationMessages.NEGATIVE_INPUT_PARAMETER("chunkSize"));
+        }
+        this.chunkSize.set(chunkSize);
+        return self();
+    }
+
+    /**
+     * Instruct the provided connectors to use the {@link java.net.HttpURLConnection#setFixedLengthStreamingMode(int)
+     * fixed-length streaming mode} on the underlying HTTP URL connection instance when sending requests.
+     * See {@link HttpUrlConnectorProvider#USE_FIXED_LENGTH_STREAMING} property documentation for more details.
+     * <p>
+     * Note that this programmatically set value can be overridden by
+     * setting the {@code USE_FIXED_LENGTH_STREAMING} property specified in the Jersey client instance configuration.
+     * </p>
+     *
+     * @return updated configuration.
+     */
+    public C useFixedLengthStreaming(boolean use) {
+        this.useFixedLengthStreaming.set(use);
+        return self();
+    }
+
+    /**
+     * Instruct the provided connectors to use reflection when setting the
+     * HTTP method value. See {@link HttpUrlConnectorProvider#SET_METHOD_WORKAROUND} property documentation for more details.
+     * <p>
+     * Note that this programmatically set value can be overridden by
+     * setting the {@code SET_METHOD_WORKAROUND} property specified in the Jersey client instance configuration
+     * or in the request properties.
+     * </p>
+     *
+     * @return updated configuration.
+     */
+    public C useSetMethodWorkaround(boolean use) {
+        this.useSetMethodWorkaround.set(use);
+        return self();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final HttpUrlConnectorConfiguration<?> that = (HttpUrlConnectorConfiguration<?>) o;
+
+        if (!chunkSize.equals(that.chunkSize)) {
+            return false;
+        }
+        if (!useFixedLengthStreaming.equals(that.useFixedLengthStreaming)) {
+            return false;
+        }
+        if (!useSetMethodWorkaround.equals(that.useSetMethodWorkaround)) {
+            return false;
+        }
+        if (!isRestrictedHeaderPropertySet.equals(that.isRestrictedHeaderPropertySet)) {
+            return false;
+        }
+
+        return connectionFactory.equals(that.connectionFactory);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(connectionFactory, chunkSize, useFixedLengthStreaming,
+                useSetMethodWorkaround, isRestrictedHeaderPropertySet);
+    }
+
+    /* package */ ReadWrite rw() {
+        final ReadWrite readWrite = this instanceof ReadWrite ? ((ReadWrite) this).instance() : new ReadWrite();
+        readWrite.setNonEmpty(this);
+        return readWrite;
+    }
+
+    protected static class ReadWrite
+        extends HttpUrlConnectorConfiguration<ReadWrite>
+        implements ConnectorConfiguration.Read<ReadWrite> {
+
+        @Override
+        public <X extends ConnectorConfiguration<?>> void setNonEmpty(X otherC) {
+            HttpUrlConnectorConfiguration<?> other = (HttpUrlConnectorConfiguration<?>) otherC;
+            Read.super.setNonEmpty(other);
+
+            this.connectionFactory.setNonEmpty(other.connectionFactory);
+            ((NullableRef<Integer>) this.chunkSize).setNonEmpty((NullableRef<Integer>) other.chunkSize);
+            ((NullableRef<Boolean>) this.isRestrictedHeaderPropertySet).setNonEmpty(
+                    (NullableRef<Boolean>) other.isRestrictedHeaderPropertySet);
+            ((NullableRef<Boolean>) this.useFixedLengthStreaming).setNonEmpty(
+                    (NullableRef<Boolean>) other.useFixedLengthStreaming);
+            ((NullableRef<Boolean>) this.useSetMethodWorkaround).setNonEmpty((NullableRef<Boolean>) other.useSetMethodWorkaround);
+        }
+
+        @Override
+        public ReadWrite init() {
+            ConnectorConfiguration.Read.super.init();
+            preInit(Collections.emptyMap());
+            isRestrictedHeaderPropertySet.set(Boolean.FALSE);
+            return self();
+        }
+
+        ReadWrite fromClient(Configuration configuration) {
+            ReadWrite clientConfiguration = copyFromClient(configuration);
+            clientConfiguration.preInit(configuration.getProperties());
+
+            // check if sun.net.http.allowRestrictedHeaders system property has been set and log the result
+            // the property is being cached in the HttpURLConnection, so this is only informative - there might
+            // already be some connection(s), that existed before the property was set/changed.
+            isRestrictedHeaderPropertySet.set(Boolean.valueOf(AccessController.doPrivileged(
+                    PropertiesHelper.getSystemProperty(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY, "false")
+            )));
+
+            LOGGER.config(isRestrictedHeaderPropertySet.get()
+                    ? LocalizationMessages.RESTRICTED_HEADER_PROPERTY_SETTING_TRUE(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY)
+                    : LocalizationMessages.RESTRICTED_HEADER_PROPERTY_SETTING_FALSE(ALLOW_RESTRICTED_HEADERS_SYSTEM_PROPERTY)
+            );
+
+            return clientConfiguration;
+        }
+
+        ReadWrite fromRequest(ClientRequest request) {
+            ReadWrite requestConfiguration = copyFromRequest(request);
+            requestConfiguration.chunkSize(
+                    request.resolveProperty(prefixed(ClientProperties.CHUNKED_ENCODING_SIZE),
+                                            requestConfiguration.chunkSize.get()));
+            requestConfiguration.useFixedLengthStreaming(
+                    request.resolveProperty(prefixed(HttpUrlConnectorProvider.USE_FIXED_LENGTH_STREAMING),
+                                            requestConfiguration.useFixedLengthStreaming.get()));
+            requestConfiguration.useSetMethodWorkaround(
+                    request.resolveProperty(prefixed(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND),
+                                            requestConfiguration.useSetMethodWorkaround.get()));
+
+            return requestConfiguration;
+        }
+
+        @Override
+        public ReadWrite instance() {
+            return new ReadWrite();
+        }
+
+        @Override
+        public ReadWrite me() {
+            return this;
+        }
+
+        public boolean isMethodWorkaround(ClientRequest request) {
+            return request.resolveProperty(
+                    prefixed(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND), useSetMethodWorkaround.get());
+        }
+
+        public boolean isPrefixed() {
+            return !prefix.get().isEmpty();
+        }
+
+        public boolean isSslContextSupplier() {
+            return sslContextSupplier.get() != null;
+        }
+
+        /**
+         * Get {@link SSLContext} either from the {@link ClientProperties#SSL_CONTEXT_SUPPLIER}, or from this configuration.
+         *
+         * @param request the request used to get the {@link SSLContext}.
+         * @return the {@link SSLContext} supplier.
+         */
+        public Supplier<SSLContext> sslContext(ClientRequest request) {
+            @SuppressWarnings("unchecked")
+            Supplier<SSLContext> supplier =
+                    request.resolveProperty(prefixed(ClientProperties.SSL_CONTEXT_SUPPLIER), Supplier.class);
+            if (supplier == null) {
+                supplier = self().sslContextSupplier.get();
+            }
+            return supplier;
+        }
+
+        @Override
+        public ReadWrite self() {
+            return this;
+        }
+    }
+
+    private static class DefaultConnectionFactory implements HttpUrlConnectorProvider.ConnectionFactory {
+
+        @Override
+        public HttpURLConnection getConnection(final URL url) throws IOException {
+            return connect(url, null);
+        }
+
+        @Override
+        public HttpURLConnection getConnection(URL url, Proxy proxy) throws IOException {
+            return connect(url, proxy);
+        }
+
+        private HttpURLConnection connect(URL url, Proxy proxy) throws IOException {
+            return (proxy == null) ? (HttpURLConnection) url.openConnection() : (HttpURLConnection) url.openConnection(proxy);
+        }
+    }
+}
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java b/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java
index ae487bf..9557c29 100644
--- a/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java
+++ b/core-client/src/test/java/org/glassfish/jersey/client/AbortTest.java
@@ -51,6 +51,7 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -222,6 +223,43 @@
         }
     }
 
+    @Test
+    public void testResponseContextCaseInsensitiveKeys() {
+        try (Response response = ClientBuilder.newClient()
+                .register(new ClientRequestFilter() {
+                    @Override
+                    public void filter(ClientRequestContext requestContext) throws IOException {
+                        requestContext.abortWith(Response.ok()
+                                .header("header1", "value")
+                                .header("header1", "value1 , value2")
+                                .header("header1", "Value3,white space ")
+                                .header("header2", "Value4;;Value5")
+                                .build());
+                    }
+                })
+                .register(new ClientResponseFilter() {
+                    @Override
+                    public void filter(ClientRequestContext requestContext, ClientResponseContext context) {
+                        Assertions.assertTrue(context.getHeaderString("header1").contains("value"));
+                        Assertions.assertTrue(context.getHeaderString("HEADER1").contains("value2"));
+                        //White space in value not trimmed
+                        Assertions.assertFalse(context.getHeaderString("header1").contains("whitespace"));
+                        //Multiple character separator
+                        Assertions.assertTrue(context.getHeaderString("header2").contains("Value5"));
+
+                        Assertions.assertTrue(context.getHeaders().containsKey("HEADer1"));
+                        Assertions.assertFalse(context.getHeaders().get("HEADer1").isEmpty());
+                        Assertions.assertFalse(context.getHeaders().remove("HeAdEr1").isEmpty());
+                        Assertions.assertFalse(context.getHeaders().containsKey("header1"));
+                    }
+                })
+                .target("http://localhost:8080")
+                .request()
+                .get()) {
+            Assertions.assertEquals(200, response.getStatus());
+        }
+    }
+
     private static class StringHeader extends AtomicReference<String> {
 
     }
diff --git a/core-client/src/test/java/org/glassfish/jersey/client/HttpUrlConnectorConfigurationTest.java b/core-client/src/test/java/org/glassfish/jersey/client/HttpUrlConnectorConfigurationTest.java
new file mode 100644
index 0000000..5e31e48
--- /dev/null
+++ b/core-client/src/test/java/org/glassfish/jersey/client/HttpUrlConnectorConfigurationTest.java
@@ -0,0 +1,793 @@
+/*
+ * Copyright (c) 2025 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.client.innate.ClientProxy;
+import org.glassfish.jersey.client.innate.ConnectorConfiguration;
+import org.glassfish.jersey.client.internal.HttpUrlConnector;
+import org.glassfish.jersey.client.internal.HttpUrlConnectorConfiguration;
+import org.glassfish.jersey.http.HttpHeaders;
+import org.glassfish.jersey.internal.MapPropertiesDelegate;
+import org.glassfish.jersey.internal.util.JdkVersion;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import javax.net.ssl.SSLContext;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.core.MultivaluedMap;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.ProtocolException;
+import java.net.URI;
+import java.net.URL;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class HttpUrlConnectorConfigurationTest {
+
+    public static final String PREFIX = "test.";
+
+    @Test
+    public void testConnectTimeout() {
+        final AtomicInteger result = new AtomicInteger(0);
+        class RWConnect extends Conf.RW {
+            @Override
+            public int connectTimeout(ClientRequest request) {
+                result.set(super.connectTimeout(request));
+                throw new IllegalStateException();
+            }
+
+            @Override
+            public RWConnect instance() {
+                return new RWConnect();
+            }
+
+        }
+        new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000)).request().apply();
+        Assertions.assertEquals(1000, result.get());
+
+        result.set(0);
+        new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000))
+                .client(c -> c.property(ClientProperties.CONNECT_TIMEOUT, 3000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000)))
+                .request().apply();
+        Assertions.assertEquals(3000, result.get());
+
+        result.set(0);
+        new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000))
+                .client(c -> c.property(ClientProperties.CONNECT_TIMEOUT, 3000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWConnect().connectTimeout(4000)))
+                .apply();
+        Assertions.assertEquals(3000, result.get());
+
+        result.set(0);
+        new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWConnect().connectTimeout(4000)))
+                .apply();
+        Assertions.assertEquals(4000, result.get());
+
+        result.set(0);
+        new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWConnect().connectTimeout(4000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECT_TIMEOUT, 5000))
+                .apply();
+        Assertions.assertEquals(5000, result.get());
+
+        result.set(0);
+        new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000).prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        HttpUrlConnectorProvider.config().connectTimeout(2000).prefix(PREFIX)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWConnect().connectTimeout(4000)))
+                .apply();
+        Assertions.assertEquals(2000, result.get());
+
+        result.set(0);
+        new TestClient(new RWConnect()).rw(rw -> rw.connectTimeout(1000).prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWConnect().connectTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        HttpUrlConnectorProvider.config().connectTimeout(4000).prefix(PREFIX)))
+                .request(r -> r.setProperty(ClientProperties.CONNECT_TIMEOUT, 5000))
+                .apply();
+        Assertions.assertEquals(4000, result.get());
+    }
+
+    @Test
+    public void testFollowRedirects() {
+        final AtomicReference<Boolean> result = new AtomicReference<>();
+        class RWFollow extends Conf.RW {
+
+            @Override
+            public boolean followRedirects(ClientRequest request) {
+                result.set(super.followRedirects(request));
+                throw new IllegalStateException();
+            }
+
+            @Override
+            public RWFollow instance() {
+                return new RWFollow();
+            }
+        }
+
+        Request req;
+        req = new TestClient(new RWFollow()).request().apply();
+        Assertions.assertEquals(((RWFollow) new RWFollow().copy()).followRedirects(), result.get());
+
+        result.set(null);
+        req = new TestClient(new RWFollow().followRedirects(false)).request().apply();
+        Assertions.assertEquals(false, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWFollow().prefix(PREFIX).followRedirects(false))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.FOLLOW_REDIRECTS, true))
+                .apply();
+        Assertions.assertEquals(true, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWFollow().prefix(PREFIX).followRedirects(false))
+                .request(r -> r.setProperty(
+                        PREFIX + ClientProperties.CONNECTOR_CONFIGURATION, new RWFollow().followRedirects(true)))
+                .apply();
+        Assertions.assertEquals(true, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWFollow())
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWFollow().followRedirects(false)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWFollow().followRedirects(true)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        Assertions.assertEquals(true, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWFollow().prefix(PREFIX))
+                .client(c -> c.property(PREFIX + ClientProperties.FOLLOW_REDIRECTS, false))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWFollow().followRedirects(true)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        Assertions.assertEquals(false, result.get());
+    }
+
+    @Test
+    public void testProxy() {
+        final AtomicReference<ClientProxy> result = new AtomicReference<>();
+        class RWProxy extends Conf.RW {
+
+            @Override
+            public Optional<ClientProxy> proxy(ClientRequest request, URI requestUri) {
+                Optional<ClientProxy> proxy = super.proxy(request, requestUri);
+                result.set(proxy.orElse(null));
+                throw new IllegalStateException();
+            }
+
+            @Override
+            public RWProxy instance() {
+                return new RWProxy();
+            }
+        }
+        String proxyUri = "http://proxy.org:8080";
+        String userName = "USERNAME";
+        String password = "PASSWORD";
+
+        new TestClient(new RWProxy()).request().apply();
+        Assertions.assertNull(result.get());
+
+        result.set(null);
+        new TestClient(new RWProxy()).rw(rw -> rw.proxyUri(proxyUri).proxyUserName(userName).proxyPassword(password))
+                .request().apply();
+        Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString());
+        Assertions.assertEquals(userName, result.get().userName());
+        Assertions.assertEquals(password, result.get().password());
+
+        result.set(null);
+        new TestClient(new RWProxy()).rw(rw -> rw.prefix(PREFIX).proxyUri(proxyUri).proxyUserName("XXX").proxyPassword(password))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.PROXY_USERNAME, userName))
+                .request(r -> r.setProperty(ClientProperties.PROXY_PASSWORD, "XXX"))
+                .apply();
+        Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString());
+        Assertions.assertEquals(userName, result.get().userName());
+        Assertions.assertEquals(password, result.get().password());
+
+        result.set(null);
+        new TestClient(new RWProxy()).rw(rw -> rw.prefix(PREFIX).proxyUri(proxyUri).proxyUserName("XXX").proxyPassword(password))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.CONNECTOR_CONFIGURATION,
+                        HttpUrlConnectorProvider.config().proxyUserName(userName).proxyPassword(null)))
+                .apply();
+        Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString());
+        Assertions.assertEquals(userName, result.get().userName());
+        Assertions.assertNull(result.get().password());
+
+        result.set(null);
+        new TestClient(new RWProxy().prefix(PREFIX)
+                .proxy(new java.net.Proxy(java.net.Proxy.Type.HTTP,
+                        new InetSocketAddress(proxyUri.split("g:")[0] + "g", Integer.parseInt(proxyUri.split("g:")[1])))))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        HttpUrlConnectorProvider.config().proxyUserName(userName).proxyPassword(password).prefix(PREFIX)))
+                .apply();
+        Assertions.assertEquals(proxyUri, result.get().uri().toASCIIString());
+        Assertions.assertEquals(userName, result.get().userName());
+        Assertions.assertEquals(password, result.get().password());
+    }
+
+    @Test
+    public void testReadTimeout() {
+        final AtomicInteger result = new AtomicInteger(0);
+        class RWRead extends Conf.RW {
+
+            @Override
+            public RWRead readTimeout(ClientRequest clientRequest) {
+                super.readTimeout(clientRequest);
+                result.set(readTimeout());
+                throw new IllegalStateException();
+            }
+
+            @Override
+            public RWRead instance() {
+                return new RWRead();
+            }
+        }
+        new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000)).request().apply();
+        Assertions.assertEquals(1000, result.get());
+
+        result.set(0);
+        new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000))
+                .client(c -> c.property(ClientProperties.READ_TIMEOUT, 3000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000)))
+                .request().apply();
+        Assertions.assertEquals(3000, result.get());
+
+        result.set(0);
+        new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000))
+                .client(c -> c.property(ClientProperties.READ_TIMEOUT, 3000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().connectTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(4000)))
+                .apply();
+        Assertions.assertEquals(3000, result.get());
+
+        result.set(0);
+        new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(4000)))
+                .apply();
+        Assertions.assertEquals(4000, result.get());
+
+        result.set(0);
+        new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(4000)))
+                .request(r -> r.setProperty(ClientProperties.READ_TIMEOUT, 5000))
+                .apply();
+        Assertions.assertEquals(5000, result.get());
+
+        result.set(0);
+        new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000).prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        HttpUrlConnectorProvider.config().readTimeout(2000).prefix(PREFIX)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWRead().readTimeout(4000)))
+                .apply();
+        Assertions.assertEquals(2000, result.get());
+
+        result.set(0);
+        new TestClient(new RWRead()).rw(rw -> rw.readTimeout(1000).prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION, new RWRead().readTimeout(2000)))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        HttpUrlConnectorProvider.config().readTimeout(4000).prefix(PREFIX)))
+                .request(r -> r.setProperty(ClientProperties.READ_TIMEOUT, 5000))
+                .apply();
+        Assertions.assertEquals(4000, result.get());
+    }
+
+    @Test
+    public void testRequestEntityProcessing() {
+        final AtomicReference<RequestEntityProcessing> result = new AtomicReference<>();
+        final AtomicReference<Conf.RW> config = new AtomicReference<>();
+        class RWRP extends Conf.RW {
+
+            @Override
+            public RequestEntityProcessing requestEntityProcessing(ClientRequest request) {
+                result.set(super.requestEntityProcessing(request));
+                throw new IllegalStateException();
+            }
+
+            @Override
+            public RWRP instance() {
+                config.set(new RWRP());
+                return (RWRP) config.get();
+            }
+        }
+
+        Request req;
+        req = new TestClient(new RWRP().requestEntityProcessing(RequestEntityProcessing.CHUNKED))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        Assertions.assertEquals(RequestEntityProcessing.CHUNKED, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWRP().prefix(PREFIX).requestEntityProcessing(RequestEntityProcessing.CHUNKED))
+                .request(r -> r.setEntity(PREFIX))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        HttpUrlConnectorProvider.config()
+                                .requestEntityProcessing(RequestEntityProcessing.BUFFERED).prefix(PREFIX)))
+                .apply();
+        Assertions.assertEquals(RequestEntityProcessing.BUFFERED, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWRP().prefix(PREFIX).requestEntityProcessing(RequestEntityProcessing.CHUNKED))
+                .request(r -> r.setEntity(PREFIX))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.REQUEST_ENTITY_PROCESSING,
+                        RequestEntityProcessing.BUFFERED))
+                .apply();
+        Assertions.assertEquals(RequestEntityProcessing.BUFFERED, result.get());
+    }
+
+    @Test
+    public void testSslContext() {
+        final SSLContext testContext = new SSLContext(null, null, null){};
+        final AtomicReference<SSLContext> result = new AtomicReference<>();
+        final AtomicReference<Conf.RW> config = new AtomicReference<>();
+        class RWSsl extends Conf.RW {
+
+            @Override
+            public SSLContext sslContext(Client client, ClientRequest request) {
+                result.set(super.sslContext(client, request));
+                return result.get();
+            }
+
+            @Override
+            public boolean isSslContextSupplier() {
+                throw new IllegalStateException();
+            }
+
+            @Override
+            public RWSsl instance() {
+                RWSsl rw = new RWSsl();
+                config.set(rw);
+                return rw;
+            }
+        }
+
+        Request req;
+        req = new TestClient(new RWSsl().sslContextSupplier(() -> testContext)).request().apply();
+        config.get().sslContext(req.client, req.request);
+        Assertions.assertEquals(testContext, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWSsl().prefix(PREFIX))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.SSL_CONTEXT_SUPPLIER,
+                        (Supplier<SSLContext>) () -> testContext)).apply();
+        config.get().sslContext(req.client, req.request);
+        Assertions.assertEquals(testContext, result.get());
+
+        result.set(null);
+        req = new TestClient(new RWSsl().prefix(PREFIX))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        new RWSsl().prefix(PREFIX).sslContextSupplier(() -> testContext))).apply();
+        config.get().sslContext(req.client, req.request);
+        Assertions.assertEquals(testContext, result.get());
+    }
+
+    @Test
+    public void testConnectionFactory() {
+        final AtomicReference<Boolean> result = new AtomicReference<>();
+        final HttpUrlConnectorProvider.ConnectionFactory connectionFactory = new HttpUrlConnectorProvider.ConnectionFactory() {
+            @Override
+            public HttpURLConnection getConnection(URL url) throws IOException {
+                result.set(true);
+                throw new IllegalStateException();
+            }
+        };
+
+        new TestClient(HttpUrlConnectorProvider.config().connectionFactory(connectionFactory)).request().apply();
+        Assertions.assertTrue(result.get());
+
+        result.set(null);
+        new TestClient(HttpUrlConnectorProvider.config().prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        HttpUrlConnectorProvider.config().connectionFactory(connectionFactory)))
+                .request().apply();
+        Assertions.assertNull(result.get());
+
+        result.set(null);
+        new TestClient(HttpUrlConnectorProvider.config().prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        HttpUrlConnectorProvider.config().connectionFactory(connectionFactory).prefix(PREFIX)))
+                .request().apply();
+        Assertions.assertTrue(result.get());
+
+        result.set(null);
+        new TestClient(HttpUrlConnectorProvider.config().prefix(PREFIX))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        HttpUrlConnectorProvider.config().connectionFactory(connectionFactory).prefix(PREFIX)))
+                .apply();
+        Assertions.assertTrue(result.get());
+
+        result.set(null);
+        Client client = ClientBuilder.newClient();
+        try {
+            HttpUrlConnectorProvider.config().build()
+                    .connectionFactory(connectionFactory)
+                    .getConnector(client, client.getConfiguration()).apply(Request.createRequest(client));
+        } catch (IllegalStateException ise) {
+            // expected
+        }
+        Assertions.assertTrue(result.get());
+    }
+
+    @Test
+    public void testChunkSize() {
+        final AtomicReference<Integer> result = new AtomicReference<>();
+        final HttpUrlConnectorProvider.ConnectionFactory connectionFactory = new HttpUrlConnectorProvider.ConnectionFactory() {
+            @Override
+            public HttpURLConnection getConnection(URL url) throws IOException {
+                return new HttpURLConnection(url) {
+                    @Override
+                    public void disconnect() {
+
+                    }
+
+                    @Override
+                    public boolean usingProxy() {
+                        return false;
+                    }
+
+                    @Override
+                    public void connect() throws IOException {
+
+                    }
+
+                    @Override
+                    public void setChunkedStreamingMode(int chunklen) {
+                        result.set(chunklen);
+                        throw new IllegalStateException();
+                    }
+                };
+            }
+        };
+
+        new TestClient(HttpUrlConnectorProvider.config()
+                .requestEntityProcessing(RequestEntityProcessing.CHUNKED)
+                .connectionFactory(connectionFactory))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        Assertions.assertEquals(ClientProperties.DEFAULT_CHUNK_SIZE, result.get());
+
+        result.set(null);
+        new TestClient(HttpUrlConnectorProvider.config()
+                .requestEntityProcessing(RequestEntityProcessing.CHUNKED)
+                .connectionFactory(connectionFactory)
+                .chunkSize(1000))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        Assertions.assertEquals(1000, result.get());
+
+        result.set(null);
+        new TestClient(HttpUrlConnectorProvider.config()
+                .requestEntityProcessing(RequestEntityProcessing.CHUNKED)
+                .connectionFactory(connectionFactory)
+                .chunkSize(1000)
+                .prefix(PREFIX))
+                .client(c -> c.property(PREFIX + ClientProperties.CHUNKED_ENCODING_SIZE, 2000))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        Assertions.assertEquals(2000, result.get());
+
+        result.set(null);
+        new TestClient(HttpUrlConnectorProvider.config()
+                .requestEntityProcessing(RequestEntityProcessing.CHUNKED)
+                .connectionFactory(connectionFactory)
+                .chunkSize(1000)
+                .prefix(PREFIX))
+                .request(r -> r.setProperty(PREFIX + ClientProperties.CHUNKED_ENCODING_SIZE, 2000))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        Assertions.assertEquals(2000, result.get());
+
+        result.set(null);
+        new TestClient(HttpUrlConnectorProvider.config()
+                .requestEntityProcessing(RequestEntityProcessing.CHUNKED)
+                .connectionFactory(connectionFactory)
+                .chunkSize(1000)
+                .prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        HttpUrlConnectorProvider.config().chunkSize(2000).prefix(PREFIX)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        Assertions.assertEquals(2000, result.get());
+
+        result.set(null);
+        new TestClient(HttpUrlConnectorProvider.config()
+                .requestEntityProcessing(RequestEntityProcessing.CHUNKED)
+                .connectionFactory(connectionFactory)
+                .chunkSize(1000)
+                .prefix(PREFIX))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        HttpUrlConnectorProvider.config().chunkSize(2000).prefix(PREFIX)))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        Assertions.assertEquals(2000, result.get());
+    }
+
+    @Test
+    public void testUseFixedLengthStreaming() {
+        final AtomicReference<Integer> result = new AtomicReference<>();
+        final class FixedLengthStreamingUrlConnection extends HttpURLConnection {
+            private FixedLengthStreamingUrlConnection(URL u) {
+                super(u);
+            }
+
+            @Override
+            public void disconnect() {
+
+            }
+
+            @Override
+            public boolean usingProxy() {
+                return false;
+            }
+
+            @Override
+            public void connect() throws IOException {
+
+            }
+
+            @Override
+            public void setFixedLengthStreamingMode(long contentLength) {
+                super.setFixedLengthStreamingMode(contentLength);
+                result.set((int) contentLength);
+                throw new IllegalStateException();
+            }
+        }
+        final HttpUrlConnectorProvider.ConnectionFactory connectionFactory = new HttpUrlConnectorProvider.ConnectionFactory() {
+            @Override
+            public HttpURLConnection getConnection(URL url) throws IOException {
+                return new FixedLengthStreamingUrlConnection(url);
+            }
+        };
+
+        new TestClient(HttpUrlConnectorProvider.config()
+                .connectionFactory(connectionFactory))
+                .request(r -> r.getRequestHeaders().add(HttpHeaders.CONTENT_LENGTH, String.valueOf(PREFIX.length())))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        Assertions.assertNull(result.get());
+
+        result.set(null);
+        new TestClient(HttpUrlConnectorProvider.config()
+                .connectionFactory(connectionFactory)
+                .useFixedLengthStreaming(true))
+                .request(r -> r.getRequestHeaders().add(HttpHeaders.CONTENT_LENGTH, String.valueOf(PREFIX.length())))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        Assertions.assertEquals(PREFIX.length(), result.get());
+
+        result.set(null);
+        new TestClient(HttpUrlConnectorProvider.config()
+                .connectionFactory(connectionFactory)
+                .useFixedLengthStreaming(false)
+                .prefix(PREFIX))
+                .client(c -> c.property(PREFIX + HttpUrlConnectorProvider.USE_FIXED_LENGTH_STREAMING, true))
+                .request(r -> r.getRequestHeaders().add(HttpHeaders.CONTENT_LENGTH, String.valueOf(PREFIX.length())))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        Assertions.assertEquals(PREFIX.length(), result.get());
+
+        result.set(null);
+        new TestClient(HttpUrlConnectorProvider.config()
+                .connectionFactory(connectionFactory)
+                .useFixedLengthStreaming(false)
+                .prefix(PREFIX))
+                .request(r -> r.setProperty(PREFIX + HttpUrlConnectorProvider.USE_FIXED_LENGTH_STREAMING, true))
+                .request(r -> r.getRequestHeaders().add(HttpHeaders.CONTENT_LENGTH, String.valueOf(PREFIX.length())))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        Assertions.assertEquals(PREFIX.length(), result.get());
+
+        result.set(null);
+        new TestClient(HttpUrlConnectorProvider.config()
+                .connectionFactory(connectionFactory)
+                .useFixedLengthStreaming(false)
+                .prefix(PREFIX))
+                .client(c -> c.property(ClientProperties.CONNECTOR_CONFIGURATION,
+                        HttpUrlConnectorProvider.config().useFixedLengthStreaming(true).prefix(PREFIX)))
+                .request(r -> r.getRequestHeaders().add(HttpHeaders.CONTENT_LENGTH, String.valueOf(PREFIX.length())))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        Assertions.assertEquals(PREFIX.length(), result.get());
+
+        result.set(null);
+        new TestClient(HttpUrlConnectorProvider.config()
+                .connectionFactory(connectionFactory)
+                .useFixedLengthStreaming(false)
+                .prefix(PREFIX))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        HttpUrlConnectorProvider.config().useFixedLengthStreaming(true).prefix(PREFIX)))
+                .request(r -> r.getRequestHeaders().add(HttpHeaders.CONTENT_LENGTH, String.valueOf(PREFIX.length())))
+                .request(r -> r.setEntity(PREFIX)).apply();
+        Assertions.assertEquals(PREFIX.length(), result.get());
+    }
+
+    @Test
+    public void testUseMethodWorkaround() {
+        JdkVersion version = JdkVersion.getJdkVersion();
+        if (!version.isReflectiveAccessToJdkSupported()) {
+            return;
+        }
+        final String method = "XYZ";
+        final AtomicReference<HttpURLConnection> result = new AtomicReference<>();
+        final HttpUrlConnectorProvider.ConnectionFactory connectionFactory = new HttpUrlConnectorProvider.ConnectionFactory() {
+            @Override
+            public HttpURLConnection getConnection(URL url) throws IOException {
+                return new HttpURLConnection(url) {
+                    @Override
+                    public void disconnect() {
+
+                    }
+
+                    @Override
+                    public boolean usingProxy() {
+                        return false;
+                    }
+
+                    @Override
+                    public void connect() throws IOException {
+
+                    }
+
+                    @Override
+                    public void setRequestMethod(String method) throws ProtocolException {
+                        result.set(this);
+                        super.setRequestMethod(method);
+                        throw new IllegalStateException();
+                    }
+                };
+            }
+        };
+
+        new TestClient(HttpUrlConnectorProvider.config()
+                .connectionFactory(connectionFactory))
+                .request(r -> r.setMethod(method))
+                .apply();
+        Assertions.assertNotEquals(method, result.get().getRequestMethod());
+
+        new TestClient(HttpUrlConnectorProvider.config()
+                .connectionFactory(connectionFactory)
+                .useSetMethodWorkaround(true))
+                .request(r -> r.setMethod(method))
+                .apply();
+        Assertions.assertEquals(method, result.get().getRequestMethod());
+
+        HttpUrlConnectorProvider.Config config = HttpUrlConnectorProvider.config();
+        config.build().useSetMethodWorkaround();
+        new TestClient(config
+                .connectionFactory(connectionFactory))
+                .request(r -> r.setMethod(method))
+                .apply();
+        Assertions.assertEquals(method, result.get().getRequestMethod());
+
+        new TestClient(HttpUrlConnectorProvider.config().prefix(PREFIX)
+                .connectionFactory(connectionFactory))
+                .request(r -> r.setMethod(method))
+                .request(r -> r.setProperty(ClientProperties.CONNECTOR_CONFIGURATION,
+                        HttpUrlConnectorProvider.config().useSetMethodWorkaround(true).prefix(PREFIX)))
+                .apply();
+        Assertions.assertEquals(method, result.get().getRequestMethod());
+
+        new TestClient(HttpUrlConnectorProvider.config().prefix(PREFIX)
+                .connectionFactory(connectionFactory))
+                .client(c -> c.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true))
+                .request(r -> r.setMethod(method))
+                .apply();
+        Assertions.assertNotEquals(method, result.get().getRequestMethod());
+
+        new TestClient(HttpUrlConnectorProvider.config().prefix(PREFIX)
+                .connectionFactory(connectionFactory))
+                .client(c -> c.property(PREFIX + HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true))
+                .request(r -> r.setMethod(method))
+                .apply();
+        Assertions.assertEquals(method, result.get().getRequestMethod());
+    }
+
+    private static class Conf extends HttpUrlConnectorConfiguration<Conf> {
+        private static class RW extends ReadWrite {
+            @Override
+            public <X extends ConnectorConfiguration<?>> void setNonEmpty(X otherC) {
+                super.setNonEmpty(otherC);
+            }
+
+            @Override
+            public RW prefix(String prefix) {
+                super.prefix(prefix);
+                return this;
+            }
+        }
+    }
+
+    private static class TestClient {
+        final Conf.RW rw;
+        final Client client;
+
+        private TestClient(ConnectorConfiguration<?> config) {
+            this.rw = config instanceof Conf.RW ? (Conf.RW) config : new Conf.RW() {
+                {
+                    setNonEmpty(config);
+                }
+            };
+            this.client = ClientBuilder.newClient(new ClientConfig());
+        }
+
+        public TestClient client(Consumer<Client> consumer) {
+            consumer.accept(client);
+            return this;
+        }
+
+        public TestClient rw(Consumer<HttpUrlConnectorConfiguration<?>> consumer) {
+            consumer.accept(rw);
+            return this;
+        }
+
+        public Request request() {
+            return new Request(client, rw);
+        }
+
+        public Request request(Consumer<ClientRequest> consumer) {
+            return request().request(consumer);
+        }
+    }
+
+    private static class Request {
+        final ClientRequest request;
+        final Client client;
+        final Conf.RW rw;
+
+        Request(Client client, Conf.RW rw) {
+            this.client = client;
+            this.rw = rw;
+            request = createRequest(client);
+        }
+
+        private static ClientRequest createRequest(Client client) {
+            ClientRequest request =  new ClientRequest(URI.create("http://localhost:8080"),
+                    (ClientConfig) client.getConfiguration(), new MapPropertiesDelegate()) {
+                @Override
+                public void writeEntity() throws IOException {
+                    throw new IllegalStateException();
+                }
+
+                @Override
+                public MultivaluedMap<String, String> getStringHeaders() {
+                    throw new IllegalStateException();
+                }
+            };
+            request.setMethod("POST");
+            return request;
+        }
+
+        public Request request(Consumer<ClientRequest> consumer) {
+            consumer.accept(request);
+            return this;
+        }
+
+        public Request apply() {
+            try {
+                HttpUrlConnector connector = new HttpUrlConnector(client, client.getConfiguration(), rw);
+                connector.apply(request);
+            } catch (ProcessingException | IllegalStateException expected) {
+            }
+            return this;
+        }
+    }
+}
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java b/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java
index 7b83560..8390bcf 100644
--- a/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java
+++ b/core-common/src/main/java/org/glassfish/jersey/internal/inject/ParamConverters.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
  * Copyright (c) 2018 Payara Foundation and/or its affiliates.
  *
  * This program and the accompanying materials are made available under the
@@ -326,7 +326,8 @@
                         final List<ClassTypePair> ctps = ReflectionHelper.getTypeArgumentAndClass(genericType);
                         final ClassTypePair ctp = (ctps.size() == 1) ? ctps.get(0) : null;
                         final boolean empty = value.isEmpty();
-                        for (ParamConverterProvider provider : Providers.getProviders(manager, ParamConverterProvider.class)) {
+                        for (final ParamConverterProvider provider
+                                : Providers.getAllProviders(manager, ParamConverterProvider.class)) {
                             final ParamConverter<?> converter = provider.getConverter(ctp.rawClass(), ctp.type(), annotations);
                             if (converter != null) {
                                 if (empty) {
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/util/JdkVersion.java b/core-common/src/main/java/org/glassfish/jersey/internal/util/JdkVersion.java
index 9b8ec40..d7c478c 100644
--- a/core-common/src/main/java/org/glassfish/jersey/internal/util/JdkVersion.java
+++ b/core-common/src/main/java/org/glassfish/jersey/internal/util/JdkVersion.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 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
@@ -119,6 +119,19 @@
     }
 
     /**
+     * <p>
+     *     Returns <tt>true</tt> if the reflective access to JDK classes is supported. It was disabled by JDK 16+.
+     * </p>
+     * <p>
+     *     This can be used to verify the property <tt>HttpUrlConnectorProvider#SET_METHOD_WORKAROUND</tt> still works,
+     *     for instance.
+     * </p>
+     */
+    public boolean isReflectiveAccessToJdkSupported() {
+        return getMajor() < 16;
+    }
+
+    /**
      * Returns <tt>true</tt> if {@code sun.misc.Unsafe} is present in the
      * current JDK version, or <tt>false</tt> otherwise.
      *
diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java
index 75b1987..d1480d2 100644
--- a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java
+++ b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/Views.java
@@ -230,6 +230,76 @@
             final List<Object> old = originalMap.put(key, (List<Object>) (List<?>) value);
             return valuesTransformer.apply(old);
         }
+
+        @Override
+        public boolean containsKey(Object key) {
+            Iterator<Entry<String, List<String>>> i = entrySet().iterator();
+            if (key == null) {
+                while (i.hasNext()) {
+                    Entry<String, List<String>> e = i.next();
+                    if (e.getKey() == null) {
+                        return true;
+                    }
+                }
+            } else {
+                while (i.hasNext()) {
+                    Entry<String, List<String>> e = i.next();
+                    if (((String) key).equalsIgnoreCase(e.getKey())) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public List<String> get(Object key) {
+            Iterator<Entry<String, List<String>>> i = entrySet().iterator();
+            if (key == null) {
+                while (i.hasNext()) {
+                    Entry<String, List<String>> e = i.next();
+                    if (e.getKey() == null) {
+                        return e.getValue();
+                    }
+                }
+            } else {
+                while (i.hasNext()) {
+                    Entry<String, List<String>> e = i.next();
+                    if (((String) key).equalsIgnoreCase(e.getKey())) {
+                        return e.getValue();
+                    }
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public List<String> remove(Object key) {
+            Iterator<Entry<String, List<String>>> i = entrySet().iterator();
+            Entry<String, List<String>> correctEntry = null;
+            if (key == null) {
+                while (correctEntry == null && i.hasNext()) {
+                    Entry<String, List<String>> e = i.next();
+                    if (e.getKey() == null) {
+                        correctEntry = e;
+                    }
+                }
+            } else {
+                while (correctEntry == null && i.hasNext()) {
+                    Entry<String, List<String>> e = i.next();
+                    if (((String) key).equalsIgnoreCase(e.getKey())) {
+                        correctEntry = e;
+                    }
+                }
+            }
+
+            List<String> oldValue = null;
+            if (correctEntry != null) {
+                oldValue = correctEntry.getValue();
+                i.remove();
+            }
+            return oldValue;
+        }
     }
 
     /**
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/ServerRuntime.java b/core-server/src/main/java/org/glassfish/jersey/server/ServerRuntime.java
index 04d820d..4e94366 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/ServerRuntime.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/ServerRuntime.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 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
@@ -720,7 +720,7 @@
                             // output streams writes out bytes only on close (for example GZipOutputStream).
                             response.close();
                         } catch (final Exception e) {
-                            LOGGER.log(Level.SEVERE, LocalizationMessages.ERROR_CLOSING_COMMIT_OUTPUT_STREAM(), e);
+                            LOGGER.log(Level.FINER, LocalizationMessages.ERROR_CLOSING_COMMIT_OUTPUT_STREAM(), e);
                         }
                     }
                 }
diff --git a/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java b/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java
index 7cf46ee..1a8f1b4 100644
--- a/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java
+++ b/core-server/src/main/java/org/glassfish/jersey/server/model/ResourceMethodInvoker.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2011, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 2025 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,7 +21,6 @@
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedList;
@@ -280,13 +279,8 @@
                                 model.getPriority(ContainerResponseFilter.class)));
             }
         }
-
-        _readerInterceptors.addAll(
-                StreamSupport.stream(processingProviders.getGlobalReaderInterceptors().spliterator(), false)
-                             .collect(Collectors.toList()));
-        _writerInterceptors.addAll(
-                StreamSupport.stream(processingProviders.getGlobalWriterInterceptors().spliterator(), false)
-                             .collect(Collectors.toList()));
+        processingProviders.getGlobalReaderInterceptors().forEach(_readerInterceptors::add);
+        processingProviders.getGlobalWriterInterceptors().forEach(_writerInterceptors::add);
 
         if (resourceMethod != null) {
             addNameBoundFiltersAndInterceptors(
@@ -458,9 +452,7 @@
                 if (entityAnn.length == 0) {
                     response.setEntityAnnotations(methodAnnotations);
                 } else {
-                    final Annotation[] mergedAnn = Arrays.copyOf(methodAnnotations,
-                            methodAnnotations.length + entityAnn.length);
-                    System.arraycopy(entityAnn, 0, mergedAnn, methodAnnotations.length, entityAnn.length);
+                    final Annotation[] mergedAnn = mergeDistinctAnnotations(methodAnnotations, entityAnn);
                     response.setEntityAnnotations(mergedAnn);
                 }
             }
@@ -487,6 +479,22 @@
         return jaxrsResponse;
     }
 
+    private static Annotation[] mergeDistinctAnnotations(Annotation[] existing, Annotation[] newOnes) {
+        List<Annotation> merged = new ArrayList<>(existing.length + newOnes.length);
+        Collections.addAll(merged, existing);
+
+        newOnesLoop:
+        for (Annotation n : newOnes) {
+            for (Annotation ex : existing) {
+                if (ex == n) {
+                    continue newOnesLoop;
+                }
+            }
+            merged.add(n);
+        }
+        return merged.toArray(new Annotation[0]);
+    }
+
     private Type unwrapInvocableResponseType(ContainerRequest request, Type entityType) {
         if (isCompletionStageResponseType
                 && request.resolveProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)) {
diff --git a/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java b/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java
index 8413a07..4ee091b 100644
--- a/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java
+++ b/core-server/src/test/java/org/glassfish/jersey/server/internal/inject/ParamConverterInternalTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2023 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
  * Copyright (c) 2018 Payara Foundation and/or its affiliates.
  *
  * This program and the accompanying materials are made available under the
@@ -27,8 +27,11 @@
 import java.util.Date;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
+import jakarta.annotation.Priority;
+import jakarta.inject.Singleton;
 import jakarta.ws.rs.DefaultValue;
 import jakarta.ws.rs.GET;
 import jakarta.ws.rs.HeaderParam;
@@ -104,6 +107,15 @@
         assertEquals(404, responseContext.getStatus());
     }
 
+    @Test
+    public void testCustomEnumResource() throws ExecutionException, InterruptedException {
+        initiateWebApplication(BadEnumResource.class, EnumParamConverterProvider.class);
+        final ContainerResponse responseContext = getResponseContext(UriBuilder.fromPath("/")
+                .queryParam("d", "A").build().toString());
+        assertEquals(1, counter.get());
+        assertEquals(200, responseContext.getStatus());
+    }
+
     public static class URIStringReaderProvider implements ParamConverterProvider {
 
         @Override
@@ -188,6 +200,40 @@
         }
     }
 
+    static final AtomicInteger counter = new AtomicInteger(0);
+    @Singleton
+    @Priority(1)
+    public static class EnumParamConverterProvider implements ParamConverterProvider {
+
+        @Override
+        public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations) {
+            if (Enum.class.isAssignableFrom(rawType)) {
+                return new ParamConverter<T>() {
+                    @Override
+                    public T fromString(final String value) {
+                        counter.addAndGet(1);
+                        if (value == null) {
+                            return null;
+                        }
+                        Class<? extends Enum> enumClass = null;
+                        try {
+                            enumClass = (Class<Enum>) Class.forName(genericType.getTypeName());
+                        } catch (ClassNotFoundException e) {
+                            throw new RuntimeException(e);
+                        }
+                        return (T) Enum.valueOf(enumClass, value.toUpperCase());
+                    }
+
+                    @Override
+                    public String toString(final T value) {
+                        return String.valueOf(value);
+                    }
+                };
+            }
+            return null;
+        }
+    }
+
     @Path("/")
     public static class ListOfStringResource {
 
diff --git a/examples/NOTICE.md b/examples/NOTICE.md
index 70c3d2b..0abdafc 100644
--- a/examples/NOTICE.md
+++ b/examples/NOTICE.md
@@ -71,7 +71,7 @@
 * Project: http://www.javassist.org/
 * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.
 
-Jackson JAX-RS Providers Version 2.18.0
+Jackson JAX-RS Providers Version 2.19.1
 * License: Apache License, 2.0
 * Project: https://github.com/FasterXML/jackson-jaxrs-providers
 * Copyright: (c) 2009-2023 FasterXML, LLC. All rights reserved unless otherwise indicated.
diff --git a/examples/osgi-helloworld-webapp/functional-test/src/test/resources/felix.policy b/examples/osgi-helloworld-webapp/functional-test/src/test/resources/felix.policy
index d8348f9..51a40bb 100644
--- a/examples/osgi-helloworld-webapp/functional-test/src/test/resources/felix.policy
+++ b/examples/osgi-helloworld-webapp/functional-test/src/test/resources/felix.policy
@@ -1,5 +1,5 @@
 //
-// Copyright (c) 2014, 2020 Oracle and/or its affiliates. All rights reserved.
+// Copyright (c) 2014, 2025 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
@@ -33,6 +33,7 @@
 allow { [org.osgi.service.condpermadmin.BundleLocationCondition "*jersey-common*"] (org.osgi.framework.PackagePermission) } "packagePermissionToJerseyCommon";
 allow { [org.osgi.service.condpermadmin.BundleLocationCondition "*jersey-common*"] (java.lang.reflect.ReflectPermission "suppressAccessChecks") } "suppressAccessChecksToJerseyCommon";
 allow { [org.osgi.service.condpermadmin.BundleLocationCondition "*jersey-common*"] (java.net.SocketPermission "*" "connect,resolve") } "socketPermissionToJerseyCommon";
+allow { [org.osgi.service.condpermadmin.BundleLocationCondition "*jersey-common*"] (java.net.NetPermission "*" "getProxySelector") } "proxySelectorToClientProxy";
 allow { [org.osgi.service.condpermadmin.BundleLocationCondition "*jersey-common*"] (java.lang.RuntimePermission "accessDeclaredMembers") } "accessDeclaredMembersToJerseyCommon";
 allow { [org.osgi.service.condpermadmin.BundleLocationCondition "*jersey-common*"] (java.lang.RuntimePermission "accessClassInPackage.sun.misc") } "accessClassInPackageSunMisc";
 allow { [org.osgi.service.condpermadmin.BundleLocationCondition "*jersey-common*"] (java.lang.RuntimePermission "getClassLoader") } "getCLToJerseyCommon";
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 350be25..d86fa8b 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
@@ -6,7 +6,10 @@
 import java.io.Reader;
 import java.io.Writer;
 import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
 import java.lang.reflect.Type;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -18,9 +21,12 @@
 import jakarta.ws.rs.core.NoContentException;
 import jakarta.ws.rs.core.Response;
 import jakarta.ws.rs.core.StreamingOutput;
+import jakarta.ws.rs.ext.ContextResolver;
 import jakarta.ws.rs.ext.MessageBodyReader;
 import jakarta.ws.rs.ext.MessageBodyWriter;
+import jakarta.ws.rs.ext.Providers;
 
+import com.fasterxml.jackson.core.PrettyPrinter;
 import org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.AnnotationBundleKey;
 import org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.Annotations;
 import org.glassfish.jersey.jackson.internal.jackson.jaxrs.cfg.EndpointConfigBase;
@@ -216,12 +222,13 @@
     protected ProviderBase() {
         this(null);
     }
+
     /**
      * @since 2.17
      */
     protected ProviderBase(MAPPER_CONFIG mconfig,
-            LookupCache<AnnotationBundleKey, EP_CONFIG> readerCache,
-            LookupCache<AnnotationBundleKey, EP_CONFIG> writerCache)
+                           LookupCache<AnnotationBundleKey, EP_CONFIG> readerCache,
+                           LookupCache<AnnotationBundleKey, EP_CONFIG> writerCache)
     {
         _mapperConfig = mconfig;
         _jaxRSFeatures = JAXRS_FEATURE_DEFAULTS;
@@ -612,7 +619,12 @@
         try {
             // Want indentation?
             if (writer.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
-                g.useDefaultPrettyPrinter();
+                PrettyPrinter defaultPrettyPrinter = writer.getConfig().getDefaultPrettyPrinter();
+                if (defaultPrettyPrinter != null) {
+                    g.setPrettyPrinter(defaultPrettyPrinter);
+                } else {
+                    g.useDefaultPrettyPrinter();
+                }
             }
             JavaType rootType = null;
 
@@ -999,6 +1011,24 @@
     /**********************************************************
      */
 
+    // @since 2.19
+    protected <OM extends ObjectMapper> OM _locateMapperViaProvider(Class<?> type, MediaType mediaType,
+                                                                    Class<OM> mapperType, Providers providers) {
+        if (providers != null) {
+            ContextResolver<OM> resolver = providers.getContextResolver(mapperType, mediaType);
+            // Above should work as is, but due to this bug
+            //   [https://jersey.dev.java.net/issues/show_bug.cgi?id=288]
+            // in Jersey, it doesn't. But this works until resolution of the issue:
+            if (resolver == null) {
+                resolver = providers.getContextResolver(mapperType, null);
+            }
+            if (resolver != null) {
+                return resolver.getContext(type);
+            }
+        }
+        return null;
+    }
+
     protected static boolean _containedIn(Class<?> mainType, HashSet<ClassKey> set)
     {
         if (set != null) {
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JacksonJsonProvider.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JacksonJsonProvider.java
index 13f9275..93a5192 100644
--- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JacksonJsonProvider.java
+++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JacksonJsonProvider.java
@@ -19,6 +19,7 @@
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectReader;
 import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.databind.json.JsonMapper;
 
 /**
  * Basic implementation of JAX-RS abstractions ({@link MessageBodyReader},
@@ -207,21 +208,12 @@
     @Override
     protected ObjectMapper _locateMapperViaProvider(Class<?> type, MediaType mediaType)
     {
-        if (_providers != null) {
-            ContextResolver<ObjectMapper> resolver = _providers.getContextResolver(ObjectMapper.class, mediaType);
-            /* Above should work as is, but due to this bug
-             *   [https://jersey.dev.java.net/issues/show_bug.cgi?id=288]
-             * in Jersey, it doesn't. But this works until resolution of
-             * the issue:
-             */
-            if (resolver == null) {
-                resolver = _providers.getContextResolver(ObjectMapper.class, null);
-            }
-            if (resolver != null) {
-                return resolver.getContext(type);
-            }
+        // 26-Nov-2024, tatu: [jakarta-rs#36] Look for JsonMapper primarily
+        ObjectMapper m =_locateMapperViaProvider(type, mediaType, JsonMapper.class, _providers);
+        if (m == null) {
+            m = _locateMapperViaProvider(type, mediaType, ObjectMapper.class, _providers);
         }
-        return null;
+        return m;
     }
 
     @Override
diff --git a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JsonMapperConfigurator.java b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JsonMapperConfigurator.java
index fd5d9aa..3325c5e 100644
--- a/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JsonMapperConfigurator.java
+++ b/media/json-jackson/src/main/java/org/glassfish/jersey/jackson/internal/jackson/jaxrs/json/JsonMapperConfigurator.java
@@ -10,6 +10,7 @@
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
 import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector;
+import com.fasterxml.jackson.databind.json.JsonMapper;
 
 /**
  * Helper class used to encapsulate details of configuring an
@@ -49,7 +50,7 @@
             _lock.lock();
             try {
                 if (_defaultMapper == null) {
-                    _defaultMapper = new ObjectMapper();
+                    _defaultMapper = new JsonMapper();
                     _setAnnotations(_defaultMapper, _defaultAnnotationsToUse);
                 }
             } finally {
@@ -77,7 +78,7 @@
             _lock.lock();
             try {
                 if (_mapper == null) {
-                    _mapper = new ObjectMapper();
+                    _mapper = new JsonMapper();
                     _setAnnotations(_mapper, _defaultAnnotationsToUse);
                 }
             } finally {
@@ -120,9 +121,8 @@
             case JACKSON:
                 return new JacksonAnnotationIntrospector();
             case JAXB:
-                /* For this, need to use indirection just so that error occurs
-                 * when we get here, and not when this class is being loaded
-                 */
+                // For this, need to use indirection just so that error occurs
+                // when we get here, and not when this class is being loaded
                 try {
                     if (_jaxbIntrospectorClass == null) {
                         _jaxbIntrospectorClass = JakartaXmlBindAnnotationIntrospector.class;
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 44c9073..a7c123c 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.18.0", "com.fasterxml.jackson.jaxrs", "jackson-jaxrs-json-provider");
+        "2.19.1", "com.fasterxml.jackson.jaxrs", "jackson-jaxrs-json-provider");
 
     @Override
     public Version version() {
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 d893c65..d9b20ee 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.18.0

+Jackson JAX-RS Providers version 2.19.1

 * License: Apache License, 2.0

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

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

diff --git a/pom.xml b/pom.xml
index f1fa3ef..ee38b9c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2244,7 +2244,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.3.1</httpclient5.version>
-        <jackson.version>2.18.0</jackson.version>
+        <jackson.version>2.19.1</jackson.version>
         <javassist.version>3.30.2-GA</javassist.version>
         <jboss.logging.8.version>3.4.3.Final</jboss.logging.8.version>
         <jersey1.version>1.19.3</jersey1.version>
diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/Jersey5939Test.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/Jersey5939Test.java
new file mode 100644
index 0000000..78f1ef4
--- /dev/null
+++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/Jersey5939Test.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2025 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.api;
+
+import org.glassfish.jersey.server.ContainerResponse;
+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.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerResponseContext;
+import jakarta.ws.rs.container.ContainerResponseFilter;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Response;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Check the reused response does not leak by additional Annotations (ContainerResponse#setEntityAnnottaions.
+ */
+public class Jersey5939Test extends JerseyTest {
+
+    private final List<ContainerResponse> capturedResponses = new ArrayList<>();
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(Restlet.class).register(new ContainerResponseFilter() {
+            @Override
+            public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
+                    throws IOException {
+                if (responseContext instanceof ContainerResponse) {
+                    capturedResponses.add((ContainerResponse) responseContext);
+                }
+            }
+        });
+    }
+
+    @Test
+    public void testIssue5939() {
+        for (int i = 0; i < 10; i++) {
+            Response response = target("foo/bar").request().get();
+            assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
+            ContainerResponse containerResponse = capturedResponses.get(i);
+            Annotation[] annotations = containerResponse.getEntityAnnotations();
+            // [@javax.ws.rs.GET(), @javax.ws.rs.Path("/bar")]
+            assertEquals(2, annotations.length, "Found " + annotations.length + " annotations, in iteration " + i);
+        }
+    }
+
+    @Path("/foo")
+    public static class Restlet {
+
+        private static final Response RESPONSE_204 = Response.noContent().build();
+
+        @GET
+        @Path("/bar")
+        public Response fooBar() {
+            return RESPONSE_204;
+        }
+    }
+}
\ No newline at end of file