Support Helidon3 and Helidon4 (precedence) by helidon connector. (#5958)

Signed-off-by: jansupol <jan.supol@oracle.com>
diff --git a/connectors/helidon-connector/pom.xml b/connectors/helidon-connector/pom.xml
index d96442a..734bba0 100644
--- a/connectors/helidon-connector/pom.xml
+++ b/connectors/helidon-connector/pom.xml
@@ -91,7 +91,7 @@
         <profile>
             <id>HelidonExclude</id>
             <activation>
-                <jdk>[1.8,17)</jdk>
+                <jdk>[1.8,21)</jdk>
             </activation>
             <build>
                 <directory>${java8.build.outputDirectory}</directory>
@@ -129,9 +129,11 @@
         <profile>
             <id>HelidonInclude</id>
             <activation>
-                <jdk>[17,)</jdk>
+                <jdk>[21,)</jdk>
             </activation>
             <build>
+                <!-- 17 is correct, the module works with Helidon 3 supporting JDK 17, too -->
+                <!-- The build is against Helidon 4, which requires JDK 21 for the build, though -->
                 <directory>${java17.build.outputDirectory}</directory>
                 <plugins>
                     <plugin>
diff --git a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/Helidon3ConnectorProvider.java b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/Helidon3ConnectorProvider.java
new file mode 100644
index 0000000..2508425
--- /dev/null
+++ b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/Helidon3ConnectorProvider.java
@@ -0,0 +1,30 @@
+/*
+ * 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.helidon.connector;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Configuration;
+import org.glassfish.jersey.Beta;
+import org.glassfish.jersey.client.spi.Connector;
+
+@Beta
+class Helidon3ConnectorProvider extends io.helidon.jersey.connector.HelidonConnectorProvider {
+    @Override
+    public Connector getConnector(Client client, Configuration runtimeConfig) {
+        return super.getConnector(client, runtimeConfig);
+    }
+}
diff --git a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonClientProperties.java b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonClientProperties.java
index 93baa42..aaf7126 100644
--- a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonClientProperties.java
+++ b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonClientProperties.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 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
@@ -13,13 +13,16 @@
  *
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
  */
+
 package org.glassfish.jersey.helidon.connector;
 
-import io.helidon.jersey.connector.HelidonProperties;
+import io.helidon.common.tls.Tls;
+import io.helidon.config.Config;
+import io.helidon.webclient.api.WebClient;
 import org.glassfish.jersey.internal.util.PropertiesClass;
 
-import io.helidon.config.Config;
-import io.helidon.webclient.WebClient;
+import java.util.List;
+import java.util.Map;
 
 /**
  * Configuration options specific to the Client API that utilizes {@code HelidonConnectorProvider}.
@@ -28,8 +31,60 @@
 @PropertiesClass
 public final class HelidonClientProperties {
 
+    private HelidonClientProperties() {
+    }
+
     /**
-     * A Helidon {@link Config} instance that is passed to {@link WebClient.Builder#config(Config)} if available
+     * Property name to set a {@link Config} instance to by used by underlying {@link WebClient}.
+     * This property is settable on {@link jakarta.ws.rs.core.Configurable#property(String, Object)}.
+     *
+     * @see io.helidon.webclient.api.WebClientConfig.Builder#config(io.helidon.common.config.Config)
      */
-    public static final String CONFIG = HelidonProperties.CONFIG;
+    public static final String CONFIG = "jersey.connector.helidon.config";
+
+    /**
+     * Property name to set a {@link Tls} instance to be used by underlying {@link WebClient}.
+     * This property is settable on {@link jakarta.ws.rs.core.Configurable#property(String, Object)}.
+     *
+     * @see io.helidon.webclient.api.WebClientConfig.Builder#tls(Tls)
+     */
+    public static final String TLS = "jersey.connector.helidon.tls";
+
+    /**
+     * Property name to set a {@code List<? extends  ProtocolConfig>} instance with a list of
+     * protocol configs to be used by underlying {@link WebClient}.
+     * This property is settable on {@link jakarta.ws.rs.core.Configurable#property(String, Object)}.
+     *
+     * @see io.helidon.webclient.api.WebClientConfig.Builder#protocolConfigs(List)
+     */
+    public static final String PROTOCOL_CONFIGS = "jersey.connector.helidon.protocolConfigs";
+
+    /**
+     * Property name to set a {@code Map<String, String>} instance with a list of
+     * default headers to be used by underlying {@link WebClient}.
+     * This property is settable on {@link jakarta.ws.rs.core.Configurable#property(String, Object)}.
+     *
+     * @see io.helidon.webclient.api.WebClientConfig.Builder#defaultHeadersMap(Map)
+     */
+    public static final String DEFAULT_HEADERS = "jersey.connector.helidon.defaultHeaders";
+
+    /**
+     * Property name to set a protocol ID for each request. You can use this property
+     * to request an HTTP/2 upgrade from HTTP/1.1 by setting its value to {@code "h2"}.
+     * When using TLS, Helidon uses negotiation via the ALPN extension instead of this
+     * property.
+     *
+     * @see io.helidon.webclient.api.HttpClientRequest#protocolId(String)
+     */
+    public static final String PROTOCOL_ID = "jersey.connector.helidon.protocolId";
+
+    /**
+     * Property name to enable or disable connection caching in the underlying {@link WebClient}.
+     * The default for the Helidon connector is {@code false}, or no sharing (which is the
+     * opposite of {@link WebClient}). Set this property to {@code true} to enable connection
+     * caching.
+     *
+     * @see io.helidon.webclient.api.WebClientConfig.Builder#shareConnectionCache(boolean)
+     */
+    public static final String SHARE_CONNECTION_CACHE = "jersey.connector.helidon.shareConnectionCache";
 }
diff --git a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonConnector.java b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonConnector.java
new file mode 100644
index 0000000..c8c5f5b
--- /dev/null
+++ b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonConnector.java
@@ -0,0 +1,343 @@
+/*
+ * 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.helidon.connector;
+
+import io.helidon.common.LazyValue;
+import io.helidon.common.Version;
+import io.helidon.common.tls.Tls;
+import io.helidon.config.Config;
+import io.helidon.http.Header;
+import io.helidon.http.HeaderNames;
+import io.helidon.http.Method;
+import io.helidon.http.media.ReadableEntity;
+import io.helidon.service.registry.ServiceRegistryException;
+import io.helidon.service.registry.Services;
+import io.helidon.webclient.api.HttpClientRequest;
+import io.helidon.webclient.api.HttpClientResponse;
+import io.helidon.webclient.api.Proxy;
+import io.helidon.webclient.api.WebClient;
+import io.helidon.webclient.api.WebClientConfig;
+import io.helidon.webclient.spi.ProtocolConfig;
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.core.Configuration;
+import jakarta.ws.rs.core.Response;
+
+
+import java.net.URI;
+import java.security.AccessController;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+import org.glassfish.jersey.client.ClientResponse;
+import org.glassfish.jersey.client.JerseyClient;
+import org.glassfish.jersey.client.spi.AsyncConnectorCallback;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.innate.VirtualThreadUtil;
+import org.glassfish.jersey.internal.util.PropertiesHelper;
+
+class HelidonConnector implements Connector {
+    static final System.Logger LOGGER = System.getLogger(HelidonConnector.class.getName());
+    private static final String CONNECTOR_CONFIG_ROOT = "jersey.connector.webclient";
+
+    private static final String HELIDON_VERSION = "Helidon/" + Version.VERSION + " (java "
+            + AccessController.doPrivileged(PropertiesHelper.getSystemProperty("java.runtime.version")) + ")";
+
+    private final LazyValue<ExecutorService> executorService;
+    private final WebClient webClient;
+    private final Proxy proxy;
+
+    @SuppressWarnings("unchecked")
+    HelidonConnector(Client client, Configuration configuration) {
+        executorService = LazyValue.create(() -> VirtualThreadUtil.withConfig(configuration,
+                        VirtualThreadUtil.threadFactoryBuilder("helidon-connector-", 0L) ,true).newCachedThreadPool());
+
+        // create underlying HTTP client
+        Map<String, Object> properties = configuration.getProperties();
+        WebClientConfig.Builder builder = WebClientConfig.builder();
+
+        // use config from property first then registry
+        var helidonConfig = configuration.getProperty(HelidonClientProperties.CONFIG);
+        if (helidonConfig != null) {
+            if (helidonConfig instanceof Config) {
+                builder.config((Config) helidonConfig);
+            } else {
+                LOGGER.log(System.Logger.Level.WARNING,
+                        LocalizationMessages.HELIDON_CONFIG_IGNORED(HelidonClientProperties.CONFIG));
+                builder.config(configFromRegistry());
+            }
+        } else {
+            builder.config(configFromRegistry());
+        }
+
+        // proxy support
+        proxy = ProxyBuilder.createProxy(configuration).orElse(Proxy.create());
+
+        // possibly override config with properties defined in Jersey client
+        // property values are ignored if cannot be converted to correct type
+        if (properties.containsKey(ClientProperties.CONNECT_TIMEOUT)) {
+            Integer connectTimeout = ClientProperties.getValue(properties, ClientProperties.CONNECT_TIMEOUT, Integer.class);
+            if (connectTimeout != null) {
+                builder.connectTimeout(Duration.ofMillis(connectTimeout));
+            }
+        }
+        if (properties.containsKey(ClientProperties.READ_TIMEOUT)) {
+            Integer readTimeout = ClientProperties.getValue(properties, ClientProperties.READ_TIMEOUT, Integer.class);
+            if (readTimeout != null) {
+                builder.readTimeout(Duration.ofMillis(readTimeout));
+            }
+        }
+        if (properties.containsKey(ClientProperties.FOLLOW_REDIRECTS)) {
+            Boolean followRedirects = ClientProperties.getValue(properties, ClientProperties.FOLLOW_REDIRECTS, Boolean.class);
+            if (followRedirects != null) {
+                builder.followRedirects(followRedirects);
+            }
+        }
+        if (properties.containsKey(ClientProperties.EXPECT_100_CONTINUE)) {
+            Boolean expect100Continue = ClientProperties
+                    .getValue(properties, ClientProperties.EXPECT_100_CONTINUE, Boolean.class);
+            if (expect100Continue != null) {
+                builder.sendExpectContinue(expect100Continue);
+            }
+        }
+
+        // first check property and then the Jersey client SSL config
+        boolean isTlsSet = false;
+        if (properties.containsKey(HelidonClientProperties.TLS)) {
+            Tls tls = ClientProperties.getValue(properties, HelidonClientProperties.TLS, Tls.class);
+            if (tls != null) {
+                builder.tls(tls);
+                isTlsSet = true;
+            }
+        }
+        if (!isTlsSet && client.getSslContext() != null) {
+            boolean isJerseyClient = client instanceof JerseyClient;
+            boolean jerseyHasDefaultSsl = isJerseyClient && ((JerseyClient) client).isDefaultSslContext();
+            if (!isJerseyClient || !jerseyHasDefaultSsl) {
+                builder.tls(Tls.builder().sslContext(client.getSslContext()).build());
+            }
+        }
+
+        // protocol configs
+        if (properties.containsKey(HelidonClientProperties.PROTOCOL_CONFIGS)) {
+            List<? extends ProtocolConfig> protocolConfigs =
+                    (List<? extends ProtocolConfig>) properties.get(HelidonClientProperties.PROTOCOL_CONFIGS);
+            if (protocolConfigs != null) {
+                builder.addProtocolConfigs(protocolConfigs);
+            }
+        }
+
+        // default headers
+        if (properties.containsKey(HelidonClientProperties.DEFAULT_HEADERS)) {
+            Map<String, String> defaultHeaders = ClientProperties
+                    .getValue(properties, HelidonClientProperties.DEFAULT_HEADERS, Map.class);
+            if (defaultHeaders != null) {
+                builder.defaultHeadersMap(defaultHeaders);
+            }
+        }
+
+        // connection sharing defaults to false in this connector
+        if (properties.containsKey(HelidonClientProperties.SHARE_CONNECTION_CACHE)) {
+            Boolean shareConnectionCache = ClientProperties
+                    .getValue(properties, HelidonClientProperties.SHARE_CONNECTION_CACHE, Boolean.class);
+            if (shareConnectionCache != null) {
+                builder.shareConnectionCache(shareConnectionCache);
+            }
+        }
+
+        webClient = builder.build();
+    }
+
+    /**
+     * Map a Jersey request to a Helidon HTTP request.
+     *
+     * @param request the request to map
+     * @return the mapped request
+     */
+    private HttpClientRequest mapRequest(ClientRequest request) {
+        // possibly override proxy in request
+        Proxy requestProxy = ProxyBuilder.createProxy(request).orElse(proxy);
+
+        // create WebClient request
+        URI uri = request.getUri();
+        HttpClientRequest httpRequest = webClient
+                .method(Method.create(request.getMethod()))
+                .proxy(requestProxy)
+                .uri(uri);
+
+        // map request headers
+        request.getRequestHeaders().forEach((key, value) -> {
+            String[] values = value.toArray(new String[0]);
+            httpRequest.header(HeaderNames.create(key), values);
+        });
+
+        // request config
+        Boolean followRedirects = request.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, Boolean.class);
+        if (followRedirects != null) {
+            httpRequest.followRedirects(followRedirects);
+        }
+        Integer readTimeout = request.resolveProperty(ClientProperties.READ_TIMEOUT, Integer.class);
+        if (readTimeout != null) {
+            httpRequest.readTimeout(Duration.ofMillis(readTimeout));
+        }
+        String protocolId = request.resolveProperty(HelidonClientProperties.PROTOCOL_ID, String.class);
+        if (protocolId != null) {
+            httpRequest.protocolId(protocolId);
+        }
+
+        // copy properties
+        for (String name : request.getConfiguration().getPropertyNames()) {
+            Object value = request.getConfiguration().getProperty(name);
+            if (!name.startsWith("jersey") && value instanceof String) {
+                httpRequest.property(name, (String) value);
+            }
+        }
+        for (String propertyName : request.getPropertyNames()) {
+            Object value = request.resolveProperty(propertyName, Object.class);
+            if (!propertyName.startsWith("jersey") && value instanceof String) {
+                httpRequest.property(propertyName, (String) value);
+            }
+        }
+
+        return httpRequest;
+    }
+
+    /**
+     * Map a Helidon HTTP/1.1 response to a Jersey response.
+     *
+     * @param httpResponse the response to map
+     * @param request the request
+     * @return the mapped response
+     */
+    private ClientResponse mapResponse(HttpClientResponse httpResponse, ClientRequest request) {
+        Response.StatusType statusType = new Response.StatusType() {
+            @Override
+            public int getStatusCode() {
+                return httpResponse.status().code();
+            }
+
+            @Override
+            public Response.Status.Family getFamily() {
+                return Response.Status.Family.familyOf(getStatusCode());
+            }
+
+            @Override
+            public String getReasonPhrase() {
+                return httpResponse.status().reasonPhrase();
+            }
+        };
+        ClientResponse response = new ClientResponse(statusType, request) {
+            @Override
+            public void close() {
+                super.close();
+                httpResponse.close();       // closes WebClient's response
+            }
+        };
+
+        // copy headers
+        for (Header header : httpResponse.headers()) {
+            for (String v : header.allValues()) {
+                response.getHeaders().add(header.name(), v);
+            }
+        }
+
+        // last URI, possibly after redirections
+        response.setResolvedRequestUri(httpResponse.lastEndpointUri().toUri());
+
+        // handle entity
+        ReadableEntity entity = httpResponse.entity();
+        if (entity.hasEntity()) {
+            response.setEntityStream(entity.inputStream());
+        }
+        return response;
+    }
+
+    /**
+     * Execute Jersey request using WebClient.
+     *
+     * @param request the Jersey request
+     * @return a Jersey response
+     */
+    @Override
+    public ClientResponse apply(ClientRequest request) {
+        HttpClientResponse httpResponse;
+        HttpClientRequest httpRequest = mapRequest(request);
+
+        if (request.hasEntity()) {
+            httpResponse = httpRequest.outputStream(os -> {
+                request.setStreamProvider(length -> os);
+                request.writeEntity();
+            });
+        } else {
+            httpResponse = httpRequest.request();
+        }
+
+        return mapResponse(httpResponse, request);
+    }
+
+    /**
+     * Asynchronously execute Jersey request using WebClient.
+     *
+     * @param request the Jersey request
+     * @return a Jersey response
+     */
+    @Override
+    public Future<?> apply(ClientRequest request, AsyncConnectorCallback callback) {
+        return executorService.get().submit(() -> {
+            try {
+                ClientResponse response = apply(request);
+                callback.response(response);
+            } catch (Throwable t) {
+                callback.failure(t);
+            }
+        });
+    }
+
+    @Override
+    public String getName() {
+        return HELIDON_VERSION;
+    }
+
+    @Override
+    public void close() {
+    }
+
+    WebClient client() {
+        return webClient;
+    }
+
+    Proxy proxy() {
+        return proxy;
+    }
+
+    Config configFromRegistry() {
+        try {
+            io.helidon.common.config.Config cfg = Services.get(io.helidon.common.config.Config.class);
+            if (cfg instanceof Config) {
+                return ((Config) cfg).get(CONNECTOR_CONFIG_ROOT);
+            }
+        } catch (ServiceRegistryException e) {
+            // falls through
+        }
+        LOGGER.log(System.Logger.Level.TRACE, LocalizationMessages.NO_CONFIG_IN_REGISTERY());
+        return Config.empty();
+    }
+}
\ No newline at end of file
diff --git a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.java b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.java
index aa1540a..dea881e 100644
--- a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.java
+++ b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020, 2022 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 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,15 +16,20 @@
 
 package org.glassfish.jersey.helidon.connector;
 
-import org.glassfish.jersey.Beta;
-import org.glassfish.jersey.client.spi.Connector;
-import org.glassfish.jersey.internal.util.JdkVersion;
-
 import jakarta.ws.rs.ProcessingException;
 import jakarta.ws.rs.client.Client;
 import jakarta.ws.rs.core.Configuration;
+
 import java.io.OutputStream;
 
+import org.glassfish.jersey.Beta;
+import org.glassfish.jersey.client.spi.Connector;
+import org.glassfish.jersey.client.spi.ConnectorProvider;
+import org.glassfish.jersey.internal.util.JdkVersion;
+import org.glassfish.jersey.internal.util.collection.LazyValue;
+import org.glassfish.jersey.internal.util.collection.Value;
+import org.glassfish.jersey.internal.util.collection.Values;
+
 /**
  * Provider for Helidon WebClient {@link Connector} that utilizes the Helidon HTTP Client to send and receive
  * HTTP request and responses. JDK 8 is not supported by the Helidon Connector.
@@ -65,12 +70,20 @@
  * @since 2.31
  */
 @Beta
-public class HelidonConnectorProvider extends io.helidon.jersey.connector.HelidonConnectorProvider {
+public class HelidonConnectorProvider implements ConnectorProvider {
+    private static final LazyValue<Helidon3ConnectorProvider> helidon3ConnectorProvider =
+            Values.lazy((Value<Helidon3ConnectorProvider>) Helidon3ConnectorProvider::new);
+
+    public HelidonConnectorProvider() {
+    }
+
     @Override
     public Connector getConnector(Client client, Configuration runtimeConfig) {
-        if (JdkVersion.getJdkVersion().getMajor() < 17) {
-            throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+        if (HelidonVersionChecker.VERSION.get() == HelidonVersionChecker.Version.VERSION_3) {
+            return helidon3ConnectorProvider.get().getConnector(client, runtimeConfig);
+        } else if (JdkVersion.getJdkVersion().getMajor() < 21) {
+            throw new ProcessingException(LocalizationMessages.HELIDON_4_NOT_SUPPORTED());
         }
-        return super.getConnector(client, runtimeConfig);
+        return new HelidonConnector(client, runtimeConfig);
     }
 }
diff --git a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonVersionChecker.java b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonVersionChecker.java
new file mode 100644
index 0000000..179c2fa
--- /dev/null
+++ b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/HelidonVersionChecker.java
@@ -0,0 +1,43 @@
+/*
+ * 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.helidon.connector;
+
+import org.glassfish.jersey.internal.util.collection.LazyValue;
+import org.glassfish.jersey.internal.util.collection.Value;
+import org.glassfish.jersey.internal.util.collection.Values;
+
+import java.lang.reflect.Field;
+
+/* package */ class HelidonVersionChecker {
+    static enum Version {
+        VERSION_3,
+        VERSION_4;
+    }
+
+    /* package */ static final LazyValue<Version> VERSION = Values.lazy((Value<Version>) () -> {
+       try {
+           // Cannot use io.helidon.common.Version.VERSION as that constant is linked into the code, resulting in 4.2.4 constant
+           // Must do it in the Runtime
+           Field field = Class.forName("io.helidon.common.Version").getDeclaredField("VERSION");
+           String version = (String) field.get(null);
+           return version.startsWith("4") ? Version.VERSION_4 : Version.VERSION_3;
+       } catch (Throwable t) {
+           // not helidon 4
+       }
+       return Version.VERSION_3;
+    });
+}
diff --git a/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/ProxyBuilder.java b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/ProxyBuilder.java
new file mode 100644
index 0000000..819d283
--- /dev/null
+++ b/connectors/helidon-connector/src/main/java17/org/glassfish/jersey/helidon/connector/ProxyBuilder.java
@@ -0,0 +1,86 @@
+/*
+ * 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.helidon.connector;
+
+import java.net.URI;
+import java.util.Locale;
+import java.util.Optional;
+
+import io.helidon.webclient.api.Proxy;
+
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.core.Configuration;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.ClientRequest;
+
+class ProxyBuilder {
+
+    static Optional<Proxy> createProxy(Configuration config) {
+        Object proxyUri = config.getProperty(ClientProperties.PROXY_URI);
+        String userName = ClientProperties.getValue(config.getProperties(), ClientProperties.PROXY_USERNAME, String.class);
+        String password = ClientProperties.getValue(config.getProperties(), ClientProperties.PROXY_PASSWORD, String.class);
+        return createProxy(proxyUri, userName, password);
+    }
+
+    static Optional<Proxy> createProxy(ClientRequest clientRequest) {
+        Object proxyUri = clientRequest.resolveProperty(ClientProperties.PROXY_URI, Object.class);
+        String userName = clientRequest.resolveProperty(ClientProperties.PROXY_USERNAME, String.class);
+        String password = clientRequest.resolveProperty(ClientProperties.PROXY_PASSWORD, String.class);
+        return createProxy(proxyUri, userName, password);
+    }
+
+    private ProxyBuilder() {
+    }
+
+    private static Optional<Proxy> createProxy(Object proxyUri, String userName, String password) {
+        if (proxyUri != null) {
+            URI u = getProxyUri(proxyUri);
+            Proxy.Builder builder = Proxy.builder();
+            if (u.getScheme().toUpperCase(Locale.ROOT).equals("DIRECT")) {
+                builder.type(Proxy.ProxyType.NONE);
+            } else {
+                builder.host(u.getHost()).port(u.getPort());
+                if ("HTTP".equals(u.getScheme().toUpperCase(Locale.ROOT))) {
+                    builder.type(Proxy.ProxyType.HTTP);
+                } else {
+                    HelidonConnector.LOGGER.log(System.Logger.Level.WARNING,
+                            LocalizationMessages.PROXY_SCHEMA_NOT_SUPPORTED(u.getScheme()));
+                    return Optional.empty();
+                }
+            }
+            if (userName != null) {
+                builder.username(userName);
+                if (password != null) {
+                    builder.password(password.toCharArray());
+                }
+            }
+            return Optional.of(builder.build());
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    private static URI getProxyUri(Object proxy) {
+        if (proxy instanceof URI) {
+            return (URI) proxy;
+        } else if (proxy instanceof String) {
+            return URI.create((String) proxy);
+        } else {
+            throw new ProcessingException(LocalizationMessages.WRONG_PROXY_URI_TYPE(proxy));
+        }
+    }
+}
diff --git a/connectors/helidon-connector/src/main/java8/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.java b/connectors/helidon-connector/src/main/java8/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.java
index 932155a..f63ef05 100644
--- a/connectors/helidon-connector/src/main/java8/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.java
+++ b/connectors/helidon-connector/src/main/java8/org/glassfish/jersey/helidon/connector/HelidonConnectorProvider.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
@@ -23,7 +23,6 @@
 import jakarta.ws.rs.ProcessingException;
 import jakarta.ws.rs.client.Client;
 import jakarta.ws.rs.core.Configuration;
-import java.io.OutputStream;
 
 /**
  * Helidon Connector stub which only throws exception when running on JDK prior to 17.
@@ -35,7 +34,7 @@
     @Override
     public Connector getConnector(Client client, Configuration runtimeConfig) {
         if (JdkVersion.getJdkVersion().getMajor() < 17) {
-            throw new ProcessingException(LocalizationMessages.NOT_SUPPORTED());
+            throw new ProcessingException(LocalizationMessages.HELIDON_3_NOT_SUPPORTED());
         }
         return null;
     }
diff --git a/connectors/helidon-connector/src/main/resources/org/glassfish/jersey/helidon/connector/localization.properties b/connectors/helidon-connector/src/main/resources/org/glassfish/jersey/helidon/connector/localization.properties
index fb56d5c..f4e68d2 100644
--- a/connectors/helidon-connector/src/main/resources/org/glassfish/jersey/helidon/connector/localization.properties
+++ b/connectors/helidon-connector/src/main/resources/org/glassfish/jersey/helidon/connector/localization.properties
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2020, 2022 Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2020, 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
@@ -14,4 +14,9 @@
 # SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 #
 
-not.supported=Helidon connector is not supported on JDK version less than 17.
\ No newline at end of file
+helidon.config.ignored=Ignoring Helidon Connector config at "{0}".
+helidon3.not.supported=Helidon connector is not supported on JDK version less than 17.
+helidon4.not.supported=Helidon 4 connector is not supported on JDK version less than 21.
+no.config.in.registery=Unable to find Config in service registry.
+proxy.schema.not.supported=Proxy schema {0} not supported.
+wrong.proxy.uri.type=The proxy URI "{0}" MUST be String or URI.
\ No newline at end of file
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/VirtualThreadUtil.java b/core-common/src/main/java/org/glassfish/jersey/innate/VirtualThreadUtil.java
index 3511d8e..5c7dbdb 100644
--- a/core-common/src/main/java/org/glassfish/jersey/innate/VirtualThreadUtil.java
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/VirtualThreadUtil.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
@@ -18,6 +18,7 @@
 
 import org.glassfish.jersey.CommonProperties;
 import org.glassfish.jersey.innate.virtual.LoomishExecutors;
+import org.glassfish.jersey.innate.virtual.ThreadFactoryBuilder;
 
 import jakarta.ws.rs.core.Configuration;
 import java.util.concurrent.ThreadFactory;
@@ -26,6 +27,9 @@
  * Factory class to provide JDK specific implementation of bits related to the virtual thread support.
  */
 public final class VirtualThreadUtil {
+    public static ThreadFactoryBuilder threadFactoryBuilder(String prefix, long start) {
+        return new ThreadFactoryBuilder().prefix(prefix).start(start);
+    }
 
     private static final boolean USE_VIRTUAL_THREADS_BY_DEFAULT = false;
 
@@ -48,10 +52,23 @@
     /**
      * Return an instance of {@link LoomishExecutors} based on a configuration property.
      * @param config the {@link Configuration}
-     * @param useVirtualByDefault the default use if not said otherwise by property
+     * @param useVirtualByDefault the default use if not said otherwise by property.
      * @return the {@link LoomishExecutors} instance.
      */
     public static LoomishExecutors withConfig(Configuration config, boolean useVirtualByDefault) {
+        return withConfig(config, null, useVirtualByDefault);
+    }
+
+    /**
+     * Return an instance of {@link LoomishExecutors} based on a configuration property.
+     * @param config the {@link Configuration}
+     * @param threadFactoryBuilder the information for the thread factory.
+     * @param useVirtualByDefault the default use if not said otherwise by property.
+     * @return the {@link LoomishExecutors} instance.
+     */
+    public static LoomishExecutors withConfig(Configuration config,
+                                              ThreadFactoryBuilder threadFactoryBuilder,
+                                              boolean useVirtualByDefault) {
         ThreadFactory tfThreadFactory = null;
         boolean useVirtualThreads = useVirtualThreads(config, useVirtualByDefault);
 
@@ -59,6 +76,8 @@
             Object threadFactory = config.getProperty(CommonProperties.THREAD_FACTORY);
             if (threadFactory != null && ThreadFactory.class.isInstance(threadFactory)) {
                 tfThreadFactory = (ThreadFactory) threadFactory;
+            } else if (threadFactoryBuilder != null) {
+                return VirtualThreadSupport.allowVirtual(useVirtualThreads, threadFactoryBuilder);
             }
         }
 
diff --git a/core-common/src/main/java/org/glassfish/jersey/innate/virtual/ThreadFactoryBuilder.java b/core-common/src/main/java/org/glassfish/jersey/innate/virtual/ThreadFactoryBuilder.java
new file mode 100644
index 0000000..383015c
--- /dev/null
+++ b/core-common/src/main/java/org/glassfish/jersey/innate/virtual/ThreadFactoryBuilder.java
@@ -0,0 +1,62 @@
+/*
+ * 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.innate.virtual;
+
+/**
+ * Bearer of the information used for building {@code ThreadFactory} either by using virtual threads or platform threads.
+ */
+public class ThreadFactoryBuilder {
+    private long start = 0L;
+    private String prefix = null;
+
+    /**
+     * Get the thread name prefix.
+     * @return the thread name prefix.
+     */
+    public String prefix() {
+        return prefix;
+    }
+
+    /**
+     * The prefix of the name of the thread. For instance "my-factory-thread-".
+     * @param prefix the thread name prefix.
+     * @return
+     */
+    public ThreadFactoryBuilder prefix(String prefix) {
+        this.prefix = prefix;
+        return this;
+    }
+
+    /**
+     * Get the initial id for the first thread created by the thread factory.
+     * @return the initial thread id.
+     */
+    public long start() {
+        return start;
+    }
+
+    /**
+     * Set the initial id for the first thread created by the thread factory. The prefix and the id make the name of the thread.
+     * For instance "my-factory-thread-0".
+     * @param start the initial id for the first thread created by the thread factory.
+     * @return the updated builder.
+     */
+    public ThreadFactoryBuilder start(long start) {
+        this.start = start;
+        return this;
+    }
+}
diff --git a/core-common/src/main/java20-/org/glassfish/jersey/innate/VirtualThreadSupport.java b/core-common/src/main/java20-/org/glassfish/jersey/innate/VirtualThreadSupport.java
index 867a65b..12e06d3 100644
--- a/core-common/src/main/java20-/org/glassfish/jersey/innate/VirtualThreadSupport.java
+++ b/core-common/src/main/java20-/org/glassfish/jersey/innate/VirtualThreadSupport.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
@@ -17,6 +17,7 @@
 package org.glassfish.jersey.innate;
 
 import org.glassfish.jersey.innate.virtual.LoomishExecutors;
+import org.glassfish.jersey.innate.virtual.ThreadFactoryBuilder;
 
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -56,13 +57,30 @@
     /**
      * Return an instance of {@link LoomishExecutors} based on a permission to use virtual threads.
      * @param allow whether to allow virtual threads.
-     * @param threadFactory the thread factory to be used by a the {@link ExecutorService}.
+     * @param threadFactory the thread factory to be used by the {@link ExecutorService}.
      * @return the {@link LoomishExecutors} instance.
      */
     public static LoomishExecutors allowVirtual(boolean allow, ThreadFactory threadFactory) {
         return new NonLoomishExecutors(threadFactory);
     }
 
+    /**
+     * Return an instance of {@link LoomishExecutors} based on a permission to use virtual threads.
+     * @param allow whether to allow virtual threads.
+     * @param threadFactoryBuilder the builder used to build thread factory to be used by the {@link ExecutorService}.
+     * @return the {@link LoomishExecutors} instance.
+     */
+    public static LoomishExecutors allowVirtual(boolean allow, ThreadFactoryBuilder threadFactoryBuilder) {
+        ThreadFactory threadFactory = new ThreadFactory() {
+            private long index = threadFactoryBuilder.start();
+            @Override
+            public Thread newThread(Runnable r) {
+                return new Thread(r, threadFactoryBuilder.prefix() + index++);
+            }
+        };
+        return new NonLoomishExecutors(threadFactory);
+    }
+
     private static final class NonLoomishExecutors implements LoomishExecutors {
         private final ThreadFactory threadFactory;
 
diff --git a/core-common/src/main/java21/org/glassfish/jersey/innate/VirtualThreadSupport.java b/core-common/src/main/java21/org/glassfish/jersey/innate/VirtualThreadSupport.java
index 0e7d695..14bc024 100644
--- a/core-common/src/main/java21/org/glassfish/jersey/innate/VirtualThreadSupport.java
+++ b/core-common/src/main/java21/org/glassfish/jersey/innate/VirtualThreadSupport.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
@@ -17,6 +17,7 @@
 package org.glassfish.jersey.innate;
 
 import org.glassfish.jersey.innate.virtual.LoomishExecutors;
+import org.glassfish.jersey.innate.virtual.ThreadFactoryBuilder;
 
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -57,13 +58,33 @@
     /**
      * Return an instance of {@link LoomishExecutors} based on a permission to use virtual threads.
      * @param allow whether to allow virtual threads.
-     * @param threadFactory the thread factory to be used by a the {@link ExecutorService}.
+     * @param threadFactory the thread factory to be used by the {@link ExecutorService}.
      * @return the {@link LoomishExecutors} instance.
      */
     public static LoomishExecutors allowVirtual(boolean allow, ThreadFactory threadFactory) {
         return allow ? new Java21LoomishExecutors(threadFactory) : new NonLoomishExecutors(threadFactory);
     }
 
+    /**
+     * Return an instance of {@link LoomishExecutors} based on a permission to use virtual threads.
+     * @param allow whether to allow virtual threads.
+     * @param threadFactoryBuilder the builder used to build thread factory to be used by the {@link ExecutorService}.
+     * @return the {@link LoomishExecutors} instance.
+     */
+    public static LoomishExecutors allowVirtual(boolean allow, ThreadFactoryBuilder threadFactoryBuilder) {
+        return allow ? loomish(threadFactoryBuilder) : nonLoomish(threadFactoryBuilder);
+    }
+
+    private static LoomishExecutors nonLoomish(ThreadFactoryBuilder threadFactoryBuilder) {
+        return new NonLoomishExecutors(
+                Thread.ofPlatform().name(threadFactoryBuilder.prefix(), threadFactoryBuilder.start()).factory());
+    }
+
+    private static LoomishExecutors loomish(ThreadFactoryBuilder threadFactoryBuilder) {
+        return new Java21LoomishExecutors(
+                Thread.ofVirtual().name(threadFactoryBuilder.prefix(), threadFactoryBuilder.start()).factory());
+    }
+
     private static class NonLoomishExecutors implements LoomishExecutors {
         private final ThreadFactory threadFactory;
 
diff --git a/pom.xml b/pom.xml
index 772798d..305114e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2192,7 +2192,8 @@
         <microprofile.rest.client.version>4.0</microprofile.rest.client.version>
         <helidon.config.version>3.2.12</helidon.config.version>
         <helidon.container.version>4.2.4</helidon.container.version>
-        <helidon.connector.version>3.2.12</helidon.connector.version>
+        <helidon3.connector.version>3.2.12</helidon3.connector.version>
+        <helidon.connector.version>4.2.4</helidon.connector.version>
         <helidon.config.11.version>1.4.15</helidon.config.11.version> <!-- JDK 11- support -->
         <smallrye.config.version>3.7.1</smallrye.config.version>
 
diff --git a/tests/integration/helidon3-client/pom.xml b/tests/integration/helidon3-client/pom.xml
new file mode 100644
index 0000000..04fba08
--- /dev/null
+++ b/tests/integration/helidon3-client/pom.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>project</artifactId>
+        <groupId>org.glassfish.jersey.tests.integration</groupId>
+        <version>3.1.99-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>helidon3-client</artifactId>
+    <name>jersey-helidon-client3-integration</name>
+    <description>
+        Checks if Helidon 3 works with Helidon connector.
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.glassfish.jersey.connectors</groupId>
+            <artifactId>jersey-helidon-connector</artifactId>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>io.helidon.jersey</groupId>
+                    <artifactId>helidon-jersey-connector</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>io.helidon.jersey</groupId>
+            <artifactId>helidon-jersey-connector</artifactId>
+            <version>${helidon3.connector.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+            <artifactId>jersey-test-framework-provider-external</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+            <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework</groupId>
+            <artifactId>jersey-test-framework-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.surefire</groupId>
+                <artifactId>surefire</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/tests/integration/helidon3-client/src/test/java/org/glassfish/jersey/integration/helidon/Helidon3Test.java b/tests/integration/helidon3-client/src/test/java/org/glassfish/jersey/integration/helidon/Helidon3Test.java
new file mode 100644
index 0000000..78a2405
--- /dev/null
+++ b/tests/integration/helidon3-client/src/test/java/org/glassfish/jersey/integration/helidon/Helidon3Test.java
@@ -0,0 +1,67 @@
+/*
+ * 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.integration.helidon;
+
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.helidon.connector.HelidonConnectorProvider;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class Helidon3Test extends JerseyTest {
+
+    @Path("/")
+    public static class Helidon3TestResource {
+        @POST
+        @Path("version")
+        public String header(@Context HttpHeaders headers, String content) {
+            return headers.getHeaderString(HttpHeaders.USER_AGENT);
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(Helidon3TestResource.class);
+    }
+
+    @Override
+    protected void configureClient(ClientConfig config) {
+        config.connectorProvider(new HelidonConnectorProvider());
+        super.configureClient(config);
+    }
+
+    @Test
+    public void testPostWithHelidon3() {
+        System.out.println("Helidon Version " + io.helidon.common.Version.VERSION);
+        Assertions.assertEquals('3', io.helidon.common.Version.VERSION.charAt(0));
+        try (Response response = target("version").request().post(Entity.entity("ANYTHING", MediaType.TEXT_PLAIN_TYPE))) {
+            Assertions.assertEquals(200, response.getStatus());
+            Assertions.assertTrue(response.readEntity(String.class).contains("Helidon/3"));
+        }
+    }
+}
diff --git a/tests/integration/pom.xml b/tests/integration/pom.xml
index b69188a..49fc438 100644
--- a/tests/integration/pom.xml
+++ b/tests/integration/pom.xml
@@ -128,6 +128,7 @@
             <modules>
                 <module>async-jersey-filter</module>
                 <module>externalproperties</module>
+                <module>helidon3-client</module>
                 <module>jaxrs-component-inject</module>
                 <module>jersey-780</module>
                 <module>jersey-1107</module>