Helidon container example

Signed-off-by: Maxim Nesen <maxim.nesen@oracle.com>
diff --git a/bom/pom.xml b/bom/pom.xml
index 1460faa..ffdfa03 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -125,6 +125,11 @@
             </dependency>
             <dependency>
                 <groupId>org.glassfish.jersey.containers</groupId>
+                <artifactId>jersey-container-helidon-http</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.glassfish.jersey.containers</groupId>
                 <artifactId>jersey-container-grizzly2-http</artifactId>
                 <version>${project.version}</version>
             </dependency>
@@ -391,6 +396,11 @@
             </dependency>
             <dependency>
                 <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+                <artifactId>jersey-test-framework-provider-helidon</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.glassfish.jersey.test-framework.providers</groupId>
                 <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
                 <version>${project.version}</version>
             </dependency>
diff --git a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBuilder.java b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBuilder.java
index 4cccbd2..98d790f 100644
--- a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBuilder.java
+++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpContainerBuilder.java
@@ -199,7 +199,7 @@
                     .host(baseUri.getHost())
                     .port(baseUri.getPort());
         } else {
-            if (webServerBuilder.port() < 0) {
+            if (webServerBuilder.port() <= 0) {
                 webServerBuilder.port(Container.DEFAULT_HTTP_PORT);
             }
 
diff --git a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpServer.java b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpServer.java
index dbea20d..145bba0 100644
--- a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpServer.java
+++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonHttpServer.java
@@ -18,6 +18,7 @@
 
 import jakarta.ws.rs.core.Application;
 import org.glassfish.jersey.server.JerseySeBootstrapConfiguration;
+import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.server.spi.Container;
 import org.glassfish.jersey.server.spi.WebServer;
 
@@ -26,10 +27,14 @@
 import java.util.concurrent.CompletionException;
 import java.util.concurrent.CompletionStage;
 
-final class HelidonHttpServer implements WebServer {
+public final class HelidonHttpServer implements WebServer {
 
     private final HelidonHttpContainer helidonHttpContainer;
 
+    HelidonHttpServer(Application application) {
+        this(HelidonHttpContainerBuilder.builder().application(application), JerseySeBootstrapConfiguration.builder().build());
+    }
+
     HelidonHttpServer(Application application, JerseySeBootstrapConfiguration seBootstrapConfig) {
         this(HelidonHttpContainerBuilder.builder().application(application), seBootstrapConfig);
     }
diff --git a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyRoutingService.java b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyRoutingService.java
index 8076956..6bbbe55 100644
--- a/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyRoutingService.java
+++ b/containers/helidon/src/main/java/org/glassfish/jersey/helidon/HelidonJerseyRoutingService.java
@@ -25,6 +25,7 @@
 import io.helidon.http.HeaderNames;
 import io.helidon.http.HeaderValues;
 import io.helidon.http.InternalServerException;
+import io.helidon.http.ServerResponseHeaders;
 import io.helidon.http.Status;
 import io.helidon.webserver.KeyPerformanceIndicatorSupport;
 import io.helidon.webserver.http.HttpRules;
@@ -94,7 +95,7 @@
     }
 
     private ApplicationHandler appHandler() {
-        return bridge.getContainer().getApplicationHandler();
+        return container().getApplicationHandler();
     }
 
     private Container container() {
@@ -114,9 +115,7 @@
     @Override
     public void afterStop() {
         try {
-            final InjectionManager ij = appHandler().getInjectionManager();
-            ij.shutdown();
-            appHandler().onShutdown(bridge.getContainer());
+            appHandler().onShutdown(container());
         } catch (Exception e) {
             if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
                 LOGGER.log(System.Logger.Level.DEBUG, "Exception during shutdown of Jersey", e);
@@ -138,6 +137,11 @@
     }
 
     private void doHandle(final Context ctx, final ServerRequest req, final ServerResponse res) {
+        ServerResponseHeaders savedResponseHeaders = null;
+        if (req.listenerContext().config().restoreResponseHeaders()) {
+            savedResponseHeaders = ServerResponseHeaders.create(res.headers());
+        }
+
         final BaseUriRequestUri uris = BaseUriRequestUri.resolve(req);
         final ContainerRequest requestContext = new ContainerRequest(uris.baseUri,
                 uris.requestUri,
@@ -187,6 +191,9 @@
                     final RoutingResponse routing = (RoutingResponse) res;
                     if (routing.reset()) {
                         res.status(Status.OK_200);
+                        if (savedResponseHeaders != null) {
+                            savedResponseHeaders.forEach(res::header);
+                        }
                         routing.next();
                     }
                 }
diff --git a/examples/helloworld-helidon/README.MD b/examples/helloworld-helidon/README.MD
new file mode 100644
index 0000000..61a5dff
--- /dev/null
+++ b/examples/helloworld-helidon/README.MD
@@ -0,0 +1,33 @@
+[//]: # " 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 Distribution License v. 1.0, which is available at "
+[//]: # " http://www.eclipse.org/org/documents/edl-v10.php. "
+[//]: # " "
+[//]: # " SPDX-License-Identifier: BSD-3-Clause "
+
+Hello World Example
+===================
+
+This example demonstrates Hello World example running on top of Helidon container and using Helidon connector as a client
+implementation for testing. JAX-RS resource returns the usual text `Hello World!`.
+
+Contents
+--------
+
+The mapping of the URI path space is presented in the following table:
+
+URI path             | Resource class      | HTTP methods | Notes
+-------------------- | ------------------- | ------------ | --------------------------------------------------------
+**_/helloworld_**    | HelloWorldResource  |  GET         |  Returns `Hello World!`
+
+Running the Example
+-------------------
+
+Run the example as follows:
+
+>     mvn clean compile exec:java
+
+This deploys the example using [Helidon](http://helidon.io/) container.
+
+-   <http://localhost:8080/helloworld>
diff --git a/examples/helloworld-helidon/pom.xml b/examples/helloworld-helidon/pom.xml
new file mode 100644
index 0000000..0b66ba7
--- /dev/null
+++ b/examples/helloworld-helidon/pom.xml
@@ -0,0 +1,92 @@
+<?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 Distribution License v. 1.0, which is available at
+    http://www.eclipse.org/org/documents/edl-v10.php.
+
+    SPDX-License-Identifier: BSD-3-Clause
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.glassfish.jersey.examples</groupId>
+        <artifactId>project</artifactId>
+        <version>3.1.99-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>helloworld-helidon</artifactId>
+    <packaging>jar</packaging>
+    <name>jersey-examples-helloworld-helidon</name>
+
+    <description>Jersey "Hello world" example on Helidon container.</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.glassfish.jersey.containers</groupId>
+            <artifactId>jersey-container-helidon-http</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.inject</groupId>
+            <artifactId>jersey-hk2</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework</groupId>
+            <artifactId>jersey-test-framework-util</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+            <artifactId>jersey-test-framework-provider-helidon</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.connectors</groupId>
+            <artifactId>jersey-helidon-connector</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.helidon.jersey</groupId>
+            <artifactId>helidon-jersey-connector</artifactId>
+            <version>${helidon.connector.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>exec-maven-plugin</artifactId>
+                <configuration>
+                    <mainClass>org.glassfish.jersey.examples.helloworld.helidon.App</mainClass>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-surefire-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+        <profile>
+            <id>pre-release</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-assembly-plugin</artifactId>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
+</project>
diff --git a/examples/helloworld-helidon/src/main/java/org/glassfish/jersey/examples/helloworld/helidon/App.java b/examples/helloworld-helidon/src/main/java/org/glassfish/jersey/examples/helloworld/helidon/App.java
new file mode 100644
index 0000000..e735863
--- /dev/null
+++ b/examples/helloworld-helidon/src/main/java/org/glassfish/jersey/examples/helloworld/helidon/App.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 Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.glassfish.jersey.examples.helloworld.helidon;
+
+import java.net.URI;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.SeBootstrap;
+import org.glassfish.jersey.helidon.HelidonHttpContainerProvider;
+import org.glassfish.jersey.helidon.HelidonHttpServer;
+import org.glassfish.jersey.server.JerseySeBootstrapConfiguration;
+import org.glassfish.jersey.server.ResourceConfig;
+
+/**
+ * Hello world!
+ */
+public class App {
+
+    static final String ROOT_PATH = "helloworld";
+
+    private static final URI BASE_URI = URI.create("http://localhost:8080/");
+
+    public static void main(String[] args) {
+        try {
+            System.out.println("\"Hello World\" Jersey Example App on Helidon container.");
+
+            ResourceConfig resourceConfig = new ResourceConfig(HelloWorldResource.class);
+            final SeBootstrap.Configuration config =
+                    JerseySeBootstrapConfiguration.builder().host("localhost").port(8080).build();
+            /*
+            final WebServer server = HelidonHttpContainerBuilder.builder()
+                    .host("localhost").port(8080).application(resourceConfig).build();
+            */
+            final HelidonHttpServer server = new HelidonHttpContainerProvider()
+                    .createServer(HelidonHttpServer.class, resourceConfig, config);
+            server.start();
+
+            Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    server.stop();
+                }
+            }));
+
+            System.out.println(String.format("Application started. \nTry out %s%s\nStop the application using "
+                                                     + "CTRL+C.", BASE_URI, ROOT_PATH));
+            Thread.currentThread().join();
+        } catch (InterruptedException ex) {
+            Logger.getLogger(App.class.getName()).log(Level.SEVERE, null, ex);
+        }
+
+    }
+}
diff --git a/examples/helloworld-helidon/src/main/java/org/glassfish/jersey/examples/helloworld/helidon/HelloWorldResource.java b/examples/helloworld-helidon/src/main/java/org/glassfish/jersey/examples/helloworld/helidon/HelloWorldResource.java
new file mode 100644
index 0000000..394e18f
--- /dev/null
+++ b/examples/helloworld-helidon/src/main/java/org/glassfish/jersey/examples/helloworld/helidon/HelloWorldResource.java
@@ -0,0 +1,48 @@
+/*
+ * 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 Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.glassfish.jersey.examples.helloworld.helidon;
+
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+
+@Path("helloworld")
+public class HelloWorldResource {
+    public static final String CLICHED_MESSAGE = "Hello World!";
+
+    @GET
+    @Produces("text/plain")
+    public String getHello() {
+        return CLICHED_MESSAGE;
+    }
+
+    @GET
+    @Path("query1")
+    @Produces("text/plain")
+    public String getQueryParameter(@DefaultValue("error1") @QueryParam(value = "test1") String test1,
+            @DefaultValue("error2") @QueryParam(value = "test2") String test2) {
+        return test1 + test2;
+    }
+
+    @POST
+    @Path("query2")
+    @Consumes("text/plain")
+    @Produces("text/plain")
+    public String postQueryParameter(@DefaultValue("error1") @QueryParam(value = "test1") String test1,
+            @DefaultValue("error2") @QueryParam(value = "test2") String test2, String entity) {
+        return entity + test1 + test2;
+    }
+
+}
diff --git a/examples/helloworld-helidon/src/test/java/org/glassfish/jersey/examples/helloworld/helidon/CustomLoggingFilter.java b/examples/helloworld-helidon/src/test/java/org/glassfish/jersey/examples/helloworld/helidon/CustomLoggingFilter.java
new file mode 100644
index 0000000..caf4575
--- /dev/null
+++ b/examples/helloworld-helidon/src/test/java/org/glassfish/jersey/examples/helloworld/helidon/CustomLoggingFilter.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 Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.glassfish.jersey.examples.helloworld.helidon;
+
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.client.ClientResponseFilter;
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.ContainerResponseContext;
+import jakarta.ws.rs.container.ContainerResponseFilter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Custom logging filter.
+ *
+ */
+public class CustomLoggingFilter implements ContainerRequestFilter, ContainerResponseFilter,
+        ClientRequestFilter, ClientResponseFilter {
+
+    private static final Logger LOGGER = Logger.getLogger(CustomLoggingFilter.class.getName());
+
+    static int preFilterCalled = 0;
+    static int postFilterCalled = 0;
+
+    @Override
+    public void filter(ClientRequestContext context) throws IOException {
+        LOGGER.info("CustomLoggingFilter.preFilter called");
+        assertEquals("bar", context.getConfiguration().getProperty("foo"));
+        preFilterCalled++;
+    }
+
+    @Override
+    public void filter(ClientRequestContext context, ClientResponseContext clientResponseContext) throws IOException {
+        LOGGER.info("CustomLoggingFilter.postFilter called");
+        assertEquals("bar", context.getConfiguration().getProperty("foo"));
+        postFilterCalled++;
+    }
+
+    @Override
+    public void filter(ContainerRequestContext context) throws IOException {
+        LOGGER.info("CustomLoggingFilter.preFilter called");
+        assertEquals("bar", context.getProperty("foo"));
+        preFilterCalled++;
+    }
+
+    @Override
+    public void filter(ContainerRequestContext context, ContainerResponseContext containerResponseContext) throws IOException {
+        LOGGER.info("CustomLoggingFilter.postFilter called");
+        assertEquals("bar", context.getProperty("foo"));
+        postFilterCalled++;
+    }
+}
+
diff --git a/examples/helloworld-helidon/src/test/java/org/glassfish/jersey/examples/helloworld/helidon/HelloWorldTest.java b/examples/helloworld-helidon/src/test/java/org/glassfish/jersey/examples/helloworld/helidon/HelloWorldTest.java
new file mode 100644
index 0000000..ae6d0f5
--- /dev/null
+++ b/examples/helloworld-helidon/src/test/java/org/glassfish/jersey/examples/helloworld/helidon/HelloWorldTest.java
@@ -0,0 +1,263 @@
+/*
+ * 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 Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.glassfish.jersey.examples.helloworld.helidon;
+
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.InvocationCallback;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.UriBuilder;
+
+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.glassfish.jersey.test.TestProperties;
+import org.glassfish.jersey.test.helidon.HelidonTestContainerFactory;
+import org.glassfish.jersey.test.spi.TestContainerException;
+import org.glassfish.jersey.test.spi.TestContainerFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
+import org.junit.jupiter.api.parallel.ResourceAccessMode;
+import org.junit.jupiter.api.parallel.ResourceLock;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class HelloWorldTest extends JerseyTest {
+
+    @Override
+    protected ResourceConfig configure() {
+        enable(TestProperties.LOG_TRAFFIC);
+        // enable(TestProperties.DUMP_ENTITY);
+        return new ResourceConfig(HelloWorldResource.class);
+    }
+
+    @Override
+    protected void configureClient(ClientConfig clientConfig) {
+        clientConfig.connectorProvider(new HelidonConnectorProvider());
+    }
+
+    @Override
+    protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
+        return new HelidonTestContainerFactory();
+    }
+
+    @Test
+    @Execution(ExecutionMode.CONCURRENT)
+    @ResourceLock(value = "dummy", mode = ResourceAccessMode.READ)
+    public void testHelloWorld() throws Exception {
+        URL getUrl = UriBuilder.fromUri(getBaseUri()).path(App.ROOT_PATH).build().toURL();
+        HttpURLConnection connection = (HttpURLConnection) getUrl.openConnection();
+        try {
+            connection.setDoOutput(true);
+            connection.setInstanceFollowRedirects(false);
+            connection.setRequestMethod("GET");
+            connection.setRequestProperty("Content-Type", "text/plain");
+            assertEquals(HttpURLConnection.HTTP_OK, connection.getResponseCode());
+        } finally {
+            connection.disconnect();
+        }
+    }
+
+    @Test
+    @Execution(ExecutionMode.CONCURRENT)
+    @ResourceLock(value = "dummy", mode = ResourceAccessMode.READ)
+    public void testConnection() {
+        Response response = target().path(App.ROOT_PATH).request("text/plain").get();
+        assertEquals(200, response.getStatus());
+    }
+
+    @Test
+    @Execution(ExecutionMode.CONCURRENT)
+    @ResourceLock(value = "dummy", mode = ResourceAccessMode.READ)
+    public void testClientStringResponse() {
+        String s = target().path(App.ROOT_PATH).request().get(String.class);
+        assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+    }
+
+    @Test
+    @Execution(ExecutionMode.CONCURRENT)
+    @ResourceLock(value = "dummy", mode = ResourceAccessMode.READ)
+    public void testAsyncClientRequests() throws InterruptedException {
+        final int REQUESTS = 10;
+        final CountDownLatch latch = new CountDownLatch(REQUESTS);
+        final long tic = System.currentTimeMillis();
+        for (int i = 0; i < REQUESTS; i++) {
+            final int id = i;
+            target().path(App.ROOT_PATH).request().async().get(new InvocationCallback<Response>() {
+                @Override
+                public void completed(Response response) {
+                    try {
+                        final String result = response.readEntity(String.class);
+                        assertEquals(HelloWorldResource.CLICHED_MESSAGE, result);
+                    } finally {
+                        latch.countDown();
+                    }
+                }
+
+                @Override
+                public void failed(Throwable error) {
+                    error.printStackTrace();
+                    latch.countDown();
+                }
+            });
+        }
+        latch.await(10 * getAsyncTimeoutMultiplier(), TimeUnit.SECONDS);
+        final long toc = System.currentTimeMillis();
+        Logger.getLogger(HelloWorldTest.class.getName()).info("Executed in: " + (toc - tic));
+    }
+
+    @Test
+    @Execution(ExecutionMode.CONCURRENT)
+    @ResourceLock(value = "dummy", mode = ResourceAccessMode.READ)
+    public void testHead() {
+        Response response = target().path(App.ROOT_PATH).request().head();
+        assertEquals(200, response.getStatus());
+        assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType());
+    }
+
+    @Test
+    @Execution(ExecutionMode.CONCURRENT)
+    @ResourceLock(value = "dummy", mode = ResourceAccessMode.READ)
+    public void testFooBarOptions() {
+        Response response = target().path(App.ROOT_PATH).request().header("Accept", "foo/bar").options();
+        assertEquals(200, response.getStatus());
+        final String allowHeader = response.getHeaderString("Allow");
+        _checkAllowContent(allowHeader);
+        assertEquals("foo/bar", response.getMediaType().toString());
+        assertEquals(0, response.getLength());
+    }
+
+    @Test
+    @Execution(ExecutionMode.CONCURRENT)
+    @ResourceLock(value = "dummy", mode = ResourceAccessMode.READ)
+    public void testTextPlainOptions() {
+        Response response = target().path(App.ROOT_PATH).request().header("Accept", MediaType.TEXT_PLAIN).options();
+        assertEquals(200, response.getStatus());
+        final String allowHeader = response.getHeaderString("Allow");
+        _checkAllowContent(allowHeader);
+        assertEquals(MediaType.TEXT_PLAIN_TYPE, response.getMediaType());
+        final String responseBody = response.readEntity(String.class);
+        _checkAllowContent(responseBody);
+    }
+
+    private void _checkAllowContent(final String content) {
+        assertTrue(content.contains("GET"));
+        assertTrue(content.contains("HEAD"));
+        assertTrue(content.contains("OPTIONS"));
+    }
+
+    @Test
+    @Execution(ExecutionMode.CONCURRENT)
+    @ResourceLock(value = "dummy", mode = ResourceAccessMode.READ)
+    public void testMissingResourceNotFound() {
+        Response response;
+
+        response = target().path(App.ROOT_PATH + "arbitrary").request().get();
+        assertEquals(404, response.getStatus());
+
+        response = target().path(App.ROOT_PATH).path("arbitrary").request().get();
+        assertEquals(404, response.getStatus());
+    }
+
+    @Test
+    @ResourceLock(value = "dummy", mode = ResourceAccessMode.READ_WRITE)
+    public void testLoggingFilterClientClass() {
+        Client client = client();
+        client.register(CustomLoggingFilter.class).property("foo", "bar");
+        CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+        String s = target().path(App.ROOT_PATH).request().get(String.class);
+        assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+        assertEquals(1, CustomLoggingFilter.preFilterCalled);
+        assertEquals(1, CustomLoggingFilter.postFilterCalled);
+    }
+
+    @Test
+    @ResourceLock(value = "dummy", mode = ResourceAccessMode.READ_WRITE)
+    public void testLoggingFilterClientInstance() {
+        Client client = client();
+        client.register(new CustomLoggingFilter()).property("foo", "bar");
+        CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+        String s = target().path(App.ROOT_PATH).request().get(String.class);
+        assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+        assertEquals(1, CustomLoggingFilter.preFilterCalled);
+        assertEquals(1, CustomLoggingFilter.postFilterCalled);
+    }
+
+    @Test
+    @ResourceLock(value = "dummy", mode = ResourceAccessMode.READ_WRITE)
+    public void testLoggingFilterTargetClass() {
+        WebTarget target = target().path(App.ROOT_PATH);
+        target.register(CustomLoggingFilter.class).property("foo", "bar");
+        CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+        String s = target.request().get(String.class);
+        assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+        assertEquals(1, CustomLoggingFilter.preFilterCalled);
+        assertEquals(1, CustomLoggingFilter.postFilterCalled);
+    }
+
+    @Test
+    @ResourceLock(value = "dummy", mode = ResourceAccessMode.READ_WRITE)
+    public void testLoggingFilterTargetInstance() {
+        WebTarget target = target().path(App.ROOT_PATH);
+        target.register(new CustomLoggingFilter()).property("foo", "bar");
+        CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+        String s = target.request().get(String.class);
+        assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+        assertEquals(1, CustomLoggingFilter.preFilterCalled);
+        assertEquals(1, CustomLoggingFilter.postFilterCalled);
+    }
+
+    @Test
+    @ResourceLock(value = "dummy", mode = ResourceAccessMode.READ_WRITE)
+    public void testConfigurationUpdate() {
+        Client client1 = client();
+        client1.register(CustomLoggingFilter.class).property("foo", "bar");
+
+        Client client = ClientBuilder.newClient(client1.getConfiguration());
+        CustomLoggingFilter.preFilterCalled = CustomLoggingFilter.postFilterCalled = 0;
+        String s = target().path(App.ROOT_PATH).request().get(String.class);
+        assertEquals(HelloWorldResource.CLICHED_MESSAGE, s);
+        assertEquals(1, CustomLoggingFilter.preFilterCalled);
+        assertEquals(1, CustomLoggingFilter.postFilterCalled);
+    }
+
+    @Test
+    @ResourceLock(value = "dummy", mode = ResourceAccessMode.READ_WRITE)
+    public void testQueryParameterGet() {
+        String result = target().path(App.ROOT_PATH + "/query1").queryParam("test1", "expected1")
+                .queryParam("test2", "expected2").request().get(String.class);
+        assertEquals("expected1expected2", result);
+    }
+
+    @Test
+    @ResourceLock(value = "dummy", mode = ResourceAccessMode.READ_WRITE)
+    public void testQueryParameterPost() {
+        String result = target().path(App.ROOT_PATH + "/query2").queryParam("test1", "expected1")
+                .queryParam("test2", "expected2").request("text/plain").post(Entity.entity("entity", "text/plain"))
+                .readEntity(String.class);
+        assertEquals("entityexpected1expected2", result);
+    }
+
+}
diff --git a/examples/pom.xml b/examples/pom.xml
index 72695a3..909db82 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -292,6 +292,15 @@
                 <module>helloworld-spring-annotations</module>
             </modules>
         </profile>
+        <profile>
+            <id>jdk21</id>
+            <activation>
+                <jdk>[21,)</jdk>
+            </activation>
+            <modules>
+                <module>helloworld-helidon</module>
+            </modules>
+        </profile>
     </profiles>
 
 </project>
diff --git a/pom.xml b/pom.xml
index 645d657..1f6bb54 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2191,9 +2191,9 @@
         <microprofile.rest.client3.version>3.0.1</microprofile.rest.client3.version>
         <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.container.version>4.3.1</helidon.container.version>
         <helidon3.connector.version>3.2.12</helidon3.connector.version>
-        <helidon.connector.version>4.2.4</helidon.connector.version>
+        <helidon.connector.version>4.3.1</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/test-framework/providers/bundle/pom.xml b/test-framework/providers/bundle/pom.xml
index 567e835..ac11835 100644
--- a/test-framework/providers/bundle/pom.xml
+++ b/test-framework/providers/bundle/pom.xml
@@ -75,4 +75,19 @@
             <version>${project.version}</version>
         </dependency>
     </dependencies>
+    <profiles>
+        <profile>
+            <id>helidon-container</id>
+            <activation>
+                <jdk>[21,)</jdk>
+            </activation>
+            <dependencies>
+                <dependency>
+                    <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+                    <artifactId>jersey-test-framework-provider-helidon</artifactId>
+                    <version>${project.version}</version>
+                </dependency>
+            </dependencies>
+        </profile>
+    </profiles>
 </project>
diff --git a/test-framework/providers/helidon-http/pom.xml b/test-framework/providers/helidon-http/pom.xml
index 34a3eea..2fad9c1 100644
--- a/test-framework/providers/helidon-http/pom.xml
+++ b/test-framework/providers/helidon-http/pom.xml
@@ -43,6 +43,17 @@
             <artifactId>jersey-container-helidon-http</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>io.helidon.webserver</groupId>
+            <artifactId>helidon-webserver</artifactId>
+            <version>${helidon.container.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.helidon.service</groupId>
+            <artifactId>helidon-service-registry</artifactId>
+            <version>${helidon.container.version}</version>
+            <scope>runtime</scope>
+        </dependency>
     </dependencies>
 
 </project>