Repository: helidon-io/helidon (java)

Issue:
4.x: Add docs about ability to use HelidonTest in a meta-annotation
## Environment Details
* Helidon Version: 4.2.0
* Helidon MP
* JDK version: not applicable
* OS: not applicable

----------

## Problem Description
There is [a question about meta-annotation](https://github.com/helidon-io/helidon/issues/4918) and [PR was merged](https://github.com/helidon-io/helidon/pull/8759). 

`HelidonTest` can be used in a a meta-annotation, but there were no corresponding changes in docs: [junit](https://helidon.io/docs/v4/mp/testing/testing) or [testng](https://helidon.io/docs/v4/mp/testing/testing-ng).

Maybe the answer from the issue  should be added in docs?

Pull request description:
4.x: Add docs about ability to use HelidonTest in a meta-annotation (#9944)
### Description
Fixes #9944 

Added necessary docs. After [improving MP Testing](https://github.com/helidon-io/helidon/pull/9695#improved-meta-annotation-support) (see the section `Improved Meta Annotation Support`) the solution is even simpler than the explanation from [the original issue](https://github.com/helidon-io/helidon/issues/4918#issuecomment-2109870213).

Did I add docs in the right place? I'm not sure. Correct me if I'm wrong.

It looks like that:
<img width="812" alt="Screenshot 2025-04-07 at 19 53 22" src="https://github.com/user-attachments/assets/800f150d-d866-44ad-8e6a-90e7dee3ec0c" />

P.S. Sorry for typos. I did my best ))).


Patch:
diff --git a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java
index 3ab5280599..08af6e429c 100644
--- a/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java
+++ b/codegen/class-model/src/main/java/io/helidon/codegen/classmodel/Type.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023, 2024 Oracle and/or its affiliates.
+ * Copyright (c) 2023, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -67,22 +67,6 @@ abstract class Type extends ModelComponent {
 
     abstract TypeName typeName();
 
-    private static String extractBoundTypeName(TypeName instance) {
-        return instance.resolvedName();
-    }
-
-    private static String calcName(TypeName instance) {
-        String className;
-        if (instance.enclosingNames().isEmpty()) {
-            className = instance.className();
-        } else {
-            className = String.join(".", instance.enclosingNames()) + "." + instance.className();
-        }
-
-        return (instance.primitive() || instance.packageName().isEmpty())
-                ? className : instance.packageName() + "." + className;
-    }
-
     abstract String fqTypeName();
     abstract String resolvedTypeName();
 
diff --git a/cors/src/main/java/io/helidon/cors/Aggregator.java b/cors/src/main/java/io/helidon/cors/Aggregator.java
index f3d440fb4b..bf24194ed2 100644
--- a/cors/src/main/java/io/helidon/cors/Aggregator.java
+++ b/cors/src/main/java/io/helidon/cors/Aggregator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020, 2024 Oracle and/or its affiliates.
+ * Copyright (c) 2020, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import java.util.function.Supplier;
 
 import io.helidon.common.config.Config;
 import io.helidon.common.config.ConfigValue;
+import io.helidon.common.uri.UriEncoding;
 import io.helidon.common.uri.UriPath;
 import io.helidon.cors.LogHelper.MatcherChecks;
 import io.helidon.http.PathMatcher;
@@ -327,7 +328,8 @@ public class Aggregator {
         }
 
         boolean matches(String path, String method) {
-            return matcher.match(UriPath.create(path)).accepted() && get().matches(method);
+            return matcher.match(UriPath.create(UriEncoding.encode(path, UriEncoding.Type.PATH))).accepted()
+                    && get().matches(method);
         }
 
         PathMatcher matcher() {
diff --git a/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc b/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc
index af3239ac18..c8c222e421 100644
--- a/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc
+++ b/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc
@@ -1,6 +1,6 @@
 ///////////////////////////////////////////////////////////////////////////////
 
-    Copyright (c) 2020, 2024 Oracle and/or its affiliates.
+    Copyright (c) 2020, 2025 Oracle and/or its affiliates.
 
     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
@@ -87,7 +87,7 @@ include::{sourcedir}/mp/config/AdvancedConfigurationSnippets.java[tag=snippet_2,
 Custom Config Sources are loaded using the Java Service Loader pattern, by implementing
 either `org.eclipse.microprofile.config.spi.ConfigSource`,
 or `org.eclipse.microprofile.config.spi.ConfigSourceProvider` SPI and registering
-it as a service (Using `pass:[META-INF/services/${class-name}]` file when using classpath, or
+it as a service (Using pass:normal[`META-INF/services/${class-name}`] file when using classpath, or
 using the `provides` statement in `module-info.java` when using module path).
 
 The interface `org.eclipse.microprofile.config.spi.ConfigSource` requires implementation of the following methods:
@@ -172,7 +172,7 @@ sources:
 
 == Extending Meta-Config to Create a Custom Config Source Type
 
-Helidon meta-config by default supports the following types: environment-variables, system-properties, properties, yaml, hocon and json. Users can also extend meta-config to create a custom config source type by loading it using the Java Service Loader pattern. This is achieved  by implementing `io.helidon.config.mp.spi.MpMetaConfigProvider` SPI and registering it as a service (Using `pass:[META-INF/services/${class-name}]` file when using classpath, or using the `provides` statement in `module-info.java` when using module path).
+Helidon meta-config by default supports the following types: environment-variables, system-properties, properties, yaml, hocon and json. Users can also extend meta-config to create a custom config source type by loading it using the Java Service Loader pattern. This is achieved  by implementing `io.helidon.config.mp.spi.MpMetaConfigProvider` SPI and registering it as a service (Using pass:normal[`META-INF/services/${class-name}`] file when using classpath, or using the `provides` statement in `module-info.java` when using module path).
 
 The interface `io.helidon.config.mp.spi.MpMetaConfigProvider` requires implementation of the following methods:
 
diff --git a/docs/src/main/asciidoc/se/config/advanced-configuration.adoc b/docs/src/main/asciidoc/se/config/advanced-configuration.adoc
index 5fc5843a2e..dc3828beb7 100644
--- a/docs/src/main/asciidoc/se/config/advanced-configuration.adoc
+++ b/docs/src/main/asciidoc/se/config/advanced-configuration.adoc
@@ -1,6 +1,6 @@
 ///////////////////////////////////////////////////////////////////////////////
 
-    Copyright (c) 2018, 2024 Oracle and/or its affiliates.
+    Copyright (c) 2018, 2025 Oracle and/or its affiliates.
 
     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
@@ -486,7 +486,7 @@ for more detail.
 
 === Tokens
 A token reference is a key token starting with `$`, optionally enclosed between
- `{` and `}`, i.e. `$ref` or `${ref}`. Even a key composed of more than one token
+ `{` and `}`, i.e. `$ref` or pass:normal[`${ref}`]. Even a key composed of more than one token
  can be referenced in another key, i.e. `${env.ref}`.
 
 As an example use case, you can use token references to declare the default values (see
diff --git a/docs/src/main/asciidoc/se/guides/webclient.adoc b/docs/src/main/asciidoc/se/guides/webclient.adoc
index e8c2a312b2..3e17bc555c 100644
--- a/docs/src/main/asciidoc/se/guides/webclient.adoc
+++ b/docs/src/main/asciidoc/se/guides/webclient.adoc
@@ -193,7 +193,7 @@ Hello David!
 ----
 
 It is also possible to change the greeting word by using a PUT request to `/greet/greeting` path. The request also
-needs to include a body with JSON type and using a structure like `pass:[{"greeting" : "value"}]`.
+needs to include a body with JSON type and using a structure like pass:normal[`{"greeting" : "value"}`].
 
 [source,java]
 .Modify the application greeting:
diff --git a/docs/src/main/asciidoc/se/openapi/openapi.adoc b/docs/src/main/asciidoc/se/openapi/openapi.adoc
index 8a4ff53a83..5e799b666d 100644
--- a/docs/src/main/asciidoc/se/openapi/openapi.adoc
+++ b/docs/src/main/asciidoc/se/openapi/openapi.adoc
@@ -1,6 +1,6 @@
 ///////////////////////////////////////////////////////////////////////////////
 
-    Copyright (c) 2019, 2024 Oracle and/or its affiliates.
+    Copyright (c) 2019, 2025 Oracle and/or its affiliates.
 
     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
@@ -83,7 +83,7 @@ Then add that instance or builder to your application's routing. The <<#register
 [[config]]
 == Configuration
 
-Helidon SE OpenAPI configuration supports the following settings:
+Helidon SE OpenAPI configuration supports the settings described below in the `server.features.openapi` section.
 
 include::{rootdir}/config/io_helidon_openapi_OpenApiFeature.adoc[leveloffset=+1,tag=config]
 
@@ -94,6 +94,24 @@ include::{rootdir}/config/io_helidon_openapi_OpenApiFeature.adoc[leveloffset=+1,
 Helidon SE provides a link:{helidon-github-examples-url}/openapi[complete OpenAPI example]
 based on the SE QuickStart sample app.
 
+=== Configure OpenAPI behavior
+The following example shows how to use configuration to customize how OpenAPI works, in this case changing the
+endpoint where Helidon provides the OpenAPI document.
+
+.Configure OpenAPI behavior
+[source,yaml]
+----
+server:
+  port: 8080                  <1>
+  host: 0.0.0.0
+  features:
+    openapi:                  <2>
+      web-context: /myopenapi <3>
+----
+<1> The `port` and `host` settings are for the server as a whole, not specifically for OpenAPI.
+<2> The `openapi` subsection within `features` contains OpenAPI settings.
+<3> Changes the endpoint for returning the OpenAPI document from the default `/openapi` to `/myopenapi`.
+
 Most Helidon {flavor-uc} applications need only add the dependency as explained above; Helidon discovers and registers OpenAPI
 automatically. The example below shows how to create and register `OpenApiFeature` explicitly instead.
 
diff --git a/http/http/src/main/java/io/helidon/http/PathMatchers.java b/http/http/src/main/java/io/helidon/http/PathMatchers.java
index bbaaff18a8..9d8664b8c5 100644
--- a/http/http/src/main/java/io/helidon/http/PathMatchers.java
+++ b/http/http/src/main/java/io/helidon/http/PathMatchers.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2022, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -236,10 +236,14 @@ public final class PathMatchers {
             switch (ch) {
             case '{':
                 subSeqCounter++;
+                // nested curly braces should be added
+                builder.append('{');
                 break;
             case '}':
                 if (subSeqCounter > 0) {
                     subSeqCounter--;
+                    // nested curly braces should be added (except for the last one, because that is end of parameter)
+                    builder.append(ch);
                 } else {
                     return builder.toString();
                 }
@@ -452,6 +456,10 @@ public final class PathMatchers {
             return "pattern: " + patternString;
         }
 
+        Pattern pattern() {
+            return pattern;
+        }
+
         private Parameters extractParams(Matcher matcher) {
             // Old school, but slightly faster than stream.
             Map<String, String> params = new HashMap<>(paramToGroupName.size());
diff --git a/http/http/src/main/java/io/helidon/http/ServerResponseTrailers.java b/http/http/src/main/java/io/helidon/http/ServerResponseTrailers.java
index ec2005dfb4..c97a9aace3 100644
--- a/http/http/src/main/java/io/helidon/http/ServerResponseTrailers.java
+++ b/http/http/src/main/java/io/helidon/http/ServerResponseTrailers.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2023, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -39,4 +39,16 @@ public interface ServerResponseTrailers extends WritableHeaders<ServerResponseTr
     static ServerResponseTrailers create(Headers existing) {
         return new ServerResponseTrailersImpl(WritableHeaders.create(existing));
     }
+
+    /**
+     * Create a new instance of mutable server response trailers by wrapping
+     * existing writable trailers. Any updates to this class will also impact
+     * the underlying trailers.
+     *
+     * @param existing trailers to wrap
+     * @return new server response trailers
+     */
+    static ServerResponseTrailers wrap(WritableHeaders<?> existing) {
+        return new ServerResponseTrailersImpl(existing);
+    }
 }
diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerResponse.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerResponse.java
index 23a597d9b2..b8a3d0fef2 100644
--- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerResponse.java
+++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2ServerResponse.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022, 2024 Oracle and/or its affiliates.
+ * Copyright (c) 2022, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.io.UncheckedIOException;
 import java.util.Objects;
+import java.util.function.Consumer;
 import java.util.function.UnaryOperator;
 
 import io.helidon.common.buffers.BufferData;
@@ -135,6 +136,10 @@ class Http2ServerResponse extends ServerResponseBase<Http2ServerResponse> {
                                                         !sendTrailers);
 
             if (sendTrailers) {
+                Consumer<ServerResponseTrailers> beforeTrailers = beforeTrailers();
+                if (beforeTrailers != null) {
+                    beforeTrailers.accept(trailers);
+                }
                 bytesWritten += stream.writeTrailers(Http2Headers.create(trailers));
             }
 
@@ -168,7 +173,7 @@ class Http2ServerResponse extends ServerResponseBase<Http2ServerResponse> {
         outputStream = new BlockingOutputStream(request, this, () -> {
             this.isSent = true;
             afterSend();
-        });
+        }, beforeTrailers());
         if (outputStreamFilter == null) {
             return contentEncode(outputStream);
         } else {
@@ -255,10 +260,12 @@ class Http2ServerResponse extends ServerResponseBase<Http2ServerResponse> {
         private boolean closed;
         private boolean firstByte = true;
         private long bytesWritten;
+        private final Consumer<ServerResponseTrailers> beforeTrailers;
 
         private BlockingOutputStream(Http2ServerRequest request,
                                      Http2ServerResponse response,
-                                     Runnable responseCloseRunnable) {
+                                     Runnable responseCloseRunnable,
+                                     Consumer<ServerResponseTrailers> beforeTrailers) {
             this.request = request;
             this.response = response;
             this.headers = response.headers;
@@ -266,6 +273,7 @@ class Http2ServerResponse extends ServerResponseBase<Http2ServerResponse> {
             this.stream = response.stream;
             this.status = response.status();
             this.responseCloseRunnable = responseCloseRunnable;
+            this.beforeTrailers = beforeTrailers;
         }
 
         @Override
@@ -376,6 +384,9 @@ class Http2ServerResponse extends ServerResponseBase<Http2ServerResponse> {
             if (response.streamResult != null) {
                 trailers.set(STREAM_RESULT_NAME, response.streamResult);
             }
+            if (beforeTrailers != null) {
+                beforeTrailers.accept(ServerResponseTrailers.wrap(trailers));
+            }
 
             Http2Headers http2Headers = Http2Headers.create(trailers);
             bytesWritten += stream.writeTrailers(http2Headers);
diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerResponse.java b/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerResponse.java
index 8f2820e2cb..ef9675afd5 100644
--- a/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerResponse.java
+++ b/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerResponse.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022, 2024 Oracle and/or its affiliates.
+ * Copyright (c) 2022, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package io.helidon.webserver.http;
 import java.io.OutputStream;
 import java.util.Arrays;
 import java.util.Optional;
+import java.util.function.Consumer;
 import java.util.function.UnaryOperator;
 
 import io.helidon.common.GenericType;
@@ -214,6 +215,17 @@ public interface ServerResponse {
      */
     ServerResponseTrailers trailers();
 
+    /**
+     * Callback to update any last minute trailers before they are written to the
+     * output stream.
+     *
+     * @param beforeTrailers consumer of mutable trailers
+     * @return this instance
+     */
+    default ServerResponse beforeTrailers(Consumer<ServerResponseTrailers> beforeTrailers) {
+        return this;
+    }
+
     /**
      * Description of the result of output stream processing.
      * In case an output stream was used, calling this method will immediately close the stream and return this
diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerResponseBase.java b/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerResponseBase.java
index 3939c1125b..25376cc503 100644
--- a/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerResponseBase.java
+++ b/webserver/webserver/src/main/java/io/helidon/webserver/http/ServerResponseBase.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022, 2024 Oracle and/or its affiliates.
+ * Copyright (c) 2022, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Consumer;
 
 import io.helidon.common.GenericType;
 import io.helidon.common.buffers.BufferData;
@@ -33,6 +34,7 @@ import io.helidon.http.HeaderValues;
 import io.helidon.http.HttpException;
 import io.helidon.http.HttpPrologue;
 import io.helidon.http.ServerRequestHeaders;
+import io.helidon.http.ServerResponseTrailers;
 import io.helidon.http.Status;
 import io.helidon.http.encoding.ContentEncoder;
 import io.helidon.http.encoding.ContentEncodingContext;
@@ -71,6 +73,7 @@ public abstract class ServerResponseBase<T extends ServerResponseBase<T>> implem
     private boolean reroute;
     private UriQuery rerouteQuery;
     private String reroutePath;
+    private Consumer<ServerResponseTrailers> beforeTrailers;
 
     /**
      * Create server response.
@@ -187,6 +190,21 @@ public abstract class ServerResponseBase<T extends ServerResponseBase<T>> implem
         return nexted;
     }
 
+    @Override
+    public ServerResponse beforeTrailers(Consumer<ServerResponseTrailers> beforeTrailers) {
+        this.beforeTrailers = beforeTrailers;
+        return this;
+    }
+
+    /**
+     * Gets consumer for server response trailers if registered on this response.
+     *
+     * @return consumer if registered or {@code null} otherwise
+     */
+    protected Consumer<ServerResponseTrailers> beforeTrailers() {
+        return beforeTrailers;
+    }
+
     /**
      * Gets media context for this response.
      *
diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java
index 7019fcf49d..24b212f58d 100644
--- a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java
+++ b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java
@@ -538,8 +538,7 @@ public class Http1Connection implements ServerConnection, InterruptableTask<Void
                                                                    sendListener,
                                                                    writer,
                                                                    request,
-                                                                   !request.headers()
-                                                                           .contains(HeaderValues.CONNECTION_CLOSE),
+                                                                   !headers.contains(HeaderValues.CONNECTION_CLOSE),
                                                                    http1Config.validateResponseHeaders());
 
             routing.route(ctx, request, response);
diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerResponse.java b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerResponse.java
index 8336ebf059..a4452427cf 100644
--- a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerResponse.java
+++ b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1ServerResponse.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022, 2024 Oracle and/or its affiliates.
+ * Copyright (c) 2022, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Objects;
 import java.util.ServiceLoader;
+import java.util.function.Consumer;
 import java.util.function.Supplier;
 import java.util.function.UnaryOperator;
 
@@ -437,6 +438,7 @@ class Http1ServerResponse extends ServerResponseBase<Http1ServerResponse> {
 
         BlockingOutputStream bos = new BlockingOutputStream(headers,
                                                             trailers,
+                                                            beforeTrailers(),
                                                             this::status,
                                                             () -> streamResult,
                                                             dataWriter,
@@ -500,9 +502,11 @@ class Http1ServerResponse extends ServerResponseBase<Http1ServerResponse> {
         private boolean closing = false;
         private final boolean validateHeaders;
         private final boolean isNoEntityStatus;
+        private final Consumer<ServerResponseTrailers> beforeTrailers;
 
         private BlockingOutputStream(ServerResponseHeaders headers,
                                      WritableHeaders<?> trailers,
+                                     Consumer<ServerResponseTrailers> beforeTrailers,
                                      Supplier<Status> status,
                                      Supplier<String> streamResult,
                                      DataWriter dataWriter,
@@ -515,6 +519,7 @@ class Http1ServerResponse extends ServerResponseBase<Http1ServerResponse> {
                                      boolean isNoEntityStatus) {
             this.headers = headers;
             this.trailers = trailers;
+            this.beforeTrailers = beforeTrailers;
             this.status = status;
             this.streamResult = streamResult;
             this.dataWriter = dataWriter;
@@ -613,6 +618,9 @@ class Http1ServerResponse extends ServerResponseBase<Http1ServerResponse> {
             if (sendTrailers) {
                 // not optimized, trailers enabled: we need to write trailers
                 trailers.set(STREAM_RESULT_NAME, streamResult.get());
+                if (beforeTrailers != null) {
+                    beforeTrailers.accept(ServerResponseTrailers.wrap(trailers));
+                }
                 BufferData buffer = BufferData.growing(128);
                 writeHeaders(trailers, buffer, this.validateHeaders);
                 buffer.write('\r');        // "\r\n" - empty line after headers


Test patch:
diff --git a/cors/src/test/java/io/helidon/cors/AggregatorTest.java b/cors/src/test/java/io/helidon/cors/AggregatorTest.java
index 7dff918517..fa27c426a7 100644
--- a/cors/src/test/java/io/helidon/cors/AggregatorTest.java
+++ b/cors/src/test/java/io/helidon/cors/AggregatorTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020, 2022 Oracle and/or its affiliates.
+ * Copyright (c) 2020, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -42,6 +42,19 @@ class AggregatorTest {
         builder.addCrossOrigin("/greet", addToMap(2, CrossOriginConfig.builder()
                 .allowMethods("GET")
                 .build()));
+        builder.addCrossOrigin("/openintegration/v10/ui/space in path",
+                               addToMap(6, CrossOriginConfig.builder()
+                                       .allowMethods("GET", "PUT")
+                                       .build()));
+        builder.addCrossOrigin("/openintegration/v1.0/ui/dotInPath",
+                               addToMap(7, CrossOriginConfig.builder()
+                                       .allowMethods("GET", "PUT")
+                                       .build()));
+        builder.addCrossOrigin("/openintegration/{+}",
+                               addToMap(8, CrossOriginConfig.builder()
+                                       .allowMethods("GET", "PUT")
+                                       .build()));
+
         builder.addPathlessCrossOrigin(addToMap(3, CrossOriginConfig.builder()
                 .allowMethods("GET", "HEAD", "OPTIONS")
                 .build()));
@@ -52,6 +65,7 @@ class AggregatorTest {
                 .allowMethods("POST")
                 .build()));
 
+
         aggregator = builder.build();
     }
 
@@ -84,6 +98,18 @@ class AggregatorTest {
         checkMatch("/greet/sub", "POST", 5);
     }
 
+    @Test
+    void testSpecialCharacters() {
+        /*
+        The first two checks should match the entries added for those two specific paths (IDs 6 and 7 respectively).
+        The second two checks make sure that wildcarded matches work with paths containing dots or spaces.
+         */
+        checkMatch("/openintegration/v10/ui/space in path", "GET", 6);
+        checkMatch("/openintegration/v1.0/ui/dotInPath", "GET", 7);
+        checkMatch("/openintegration/v20/ui/space in path", "GET", 8);
+        checkMatch("/openintegration/v2.0/ui/dot and space in path", "GET", 8);
+    }
+
     private static CrossOriginConfig addToMap(int ID, CrossOriginConfig coc) {
         configs.put(coc, ID);
         return coc;
diff --git a/docs/src/main/asciidoc/mp/testing/testing-ng.adoc b/docs/src/main/asciidoc/mp/testing/testing-ng.adoc
index dcd88cb8f8..0ef15e2af1 100644
--- a/docs/src/main/asciidoc/mp/testing/testing-ng.adoc
+++ b/docs/src/main/asciidoc/mp/testing/testing-ng.adoc
@@ -69,6 +69,22 @@ I.e. By default, the test instance is re-used between test methods.
 
 NOTE: The test instance is not re-used between CDI container, using a dedicated CDI container implies a new test instance
 
+=== Using meta-annotations
+Meta-annotations are supported on both test classes and test methods and can be used as a composition mechanism.
+
+[source,java]
+.Class-level meta-annotation
+----
+include::{sourcedir}/mp/testing/TestingNgSnippets.java[tag=snippet_2, indent=0]
+----
+
+[source,java]
+.Method-level meta-annotation
+----
+include::{sourcedir}/mp/testing/TestingNgSnippets.java[tag=snippet_3, indent=0]
+----
+1. `org.testng.annotations.Test` is not inheritable and should be placed on methods
+
 == API
 
 include::{rootdir}/mp/testing/testing-common.adoc[tag=api]
diff --git a/docs/src/main/asciidoc/mp/testing/testing.adoc b/docs/src/main/asciidoc/mp/testing/testing.adoc
index 7c475cab74..a6923a32f4 100644
--- a/docs/src/main/asciidoc/mp/testing/testing.adoc
+++ b/docs/src/main/asciidoc/mp/testing/testing.adoc
@@ -89,6 +89,21 @@ NOTE: The test instance is not re-used between CDI container, using a dedicated
 include::{sourcedir}/mp/testing/TestingJunit5Snippets.java[tag=snippet_2, indent=0]
 ----
 
+=== Using meta-annotations
+Meta-annotations are supported on both test classes and test methods and can be used as a composition mechanism.
+
+[source,java]
+.Class-level meta-annotation example
+----
+include::{sourcedir}/mp/testing/TestingJunit5Snippets.java[tag=snippet_4, indent=0]
+----
+
+[source,java]
+.Method-level meta-annotation example
+----
+include::{sourcedir}/mp/testing/TestingJunit5Snippets.java[tag=snippet_5, indent=0]
+----
+
 == API
 
 include::{rootdir}/mp/testing/testing-common.adoc[tag=api]
diff --git a/docs/src/main/java/io/helidon/docs/mp/testing/TestingJunit5Snippets.java b/docs/src/main/java/io/helidon/docs/mp/testing/TestingJunit5Snippets.java
index 96be17a0a2..c636730232 100644
--- a/docs/src/main/java/io/helidon/docs/mp/testing/TestingJunit5Snippets.java
+++ b/docs/src/main/java/io/helidon/docs/mp/testing/TestingJunit5Snippets.java
@@ -16,6 +16,14 @@
 package io.helidon.docs.mp.testing;
 
 import io.helidon.microprofile.testing.junit5.HelidonTest;
+import io.helidon.microprofile.testing.AddBean;
+import io.helidon.microprofile.testing.DisableDiscovery;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 
 import jakarta.annotation.Priority;
 import jakarta.enterprise.context.ApplicationScoped;
@@ -113,4 +121,49 @@ class TestingJunit5Snippets {
         }
         // end::snippet_3[]
     }
+
+    class Snippet4 {
+
+        class FirstBean {}
+
+        class SecondBean {}
+
+        // tag::snippet_4[]
+        @HelidonTest
+        @AddBean(FirstBean.class)
+        @AddBean(SecondBean.class)
+        @DisableDiscovery
+        @Target(ElementType.TYPE)
+        @Retention(RetentionPolicy.RUNTIME)
+        public @interface CustomMetaAnnotation {
+        }
+
+        @CustomMetaAnnotation
+        class AnnotationOnClass {
+        }
+        // end::snippet_4[]
+
+        // tag::snippet_5[]
+        @Test
+        @AddBean(FirstBean.class)
+        @AddBean(SecondBean.class)
+        @DisableDiscovery
+        @Target(ElementType.METHOD)
+        @Retention(RetentionPolicy.RUNTIME)
+        public @interface MyTestMethod {
+        }
+
+        @HelidonTest
+        class AnnotationOnMethod {
+
+            @MyTestMethod
+            void testOne() {
+            }
+
+            @MyTestMethod
+            void testTwo() {
+            }
+        }
+        // end::snippet_5[]
+    }
 }
diff --git a/docs/src/main/java/io/helidon/docs/mp/testing/TestingNgSnippets.java b/docs/src/main/java/io/helidon/docs/mp/testing/TestingNgSnippets.java
index 40cd0a2288..98b5a60174 100644
--- a/docs/src/main/java/io/helidon/docs/mp/testing/TestingNgSnippets.java
+++ b/docs/src/main/java/io/helidon/docs/mp/testing/TestingNgSnippets.java
@@ -16,6 +16,14 @@
 package io.helidon.docs.mp.testing;
 
 import io.helidon.microprofile.testing.testng.HelidonTest;
+import io.helidon.microprofile.testing.AddBean;
+import io.helidon.microprofile.testing.DisableDiscovery;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 
 import jakarta.annotation.Priority;
 import jakarta.enterprise.context.ApplicationScoped;
@@ -89,4 +97,50 @@ class TestingNgSnippets {
         }
         // end::snippet_1[]
     }
+
+    class Snippet2 {
+
+        class FirstBean {}
+
+        class SecondBean {}
+
+        // tag::snippet_2[]
+        @HelidonTest
+        @AddBean(FirstBean.class)
+        @AddBean(SecondBean.class)
+        @DisableDiscovery
+        @Target(ElementType.TYPE)
+        @Retention(RetentionPolicy.RUNTIME)
+        public @interface CustomMetaAnnotation {
+        }
+
+        @CustomMetaAnnotation
+        class AnnotationOnClass {
+        }
+        // end::snippet_2[]
+
+        // tag::snippet_3[]
+        @AddBean(FirstBean.class)
+        @AddBean(SecondBean.class)
+        @DisableDiscovery
+        @Target(ElementType.METHOD)
+        @Retention(RetentionPolicy.RUNTIME)
+        public @interface MyTestMethod {
+        }
+
+        @HelidonTest
+        class AnnotationOnMethod {
+
+            @Test // <1>
+            @MyTestMethod
+            void testOne() {
+            }
+
+            @Test // <1>
+            @MyTestMethod
+            void testTwo() {
+            }
+        }
+        // end::snippet_3[]
+    }
 }
diff --git a/webserver/webserver/src/test/java/io/helidon/webserver/http/PathMatchersTest.java b/http/http/src/test/java/io/helidon/http/PathMatchersTest.java
similarity index 92%
rename from webserver/webserver/src/test/java/io/helidon/webserver/http/PathMatchersTest.java
rename to http/http/src/test/java/io/helidon/http/PathMatchersTest.java
index b37c7d4251..652ec8ffd3 100644
--- a/webserver/webserver/src/test/java/io/helidon/webserver/http/PathMatchersTest.java
+++ b/http/http/src/test/java/io/helidon/http/PathMatchersTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2022, 2025 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,14 +14,12 @@
  * limitations under the License.
  */
 
-package io.helidon.webserver.http;
+package io.helidon.http;
 
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import io.helidon.http.PathMatcher;
-import io.helidon.http.PathMatchers;
 import io.helidon.common.uri.UriPath;
 
 import org.hamcrest.Description;
@@ -35,7 +33,17 @@ import static org.hamcrest.MatcherAssert.assertThat;
 
 class PathMatchersTest {
     @Test
-    public void testNormalization() {
+    void testRegexQuantifier() {
+        var matcher = PathMatchers.pattern("/{id:\\w{2}}/name");
+        var patternMatcher = (PathMatchers.PatternPathMatcher) matcher;
+
+        var actualPattern = patternMatcher.pattern();
+
+        assertThat(actualPattern.pattern(), is("/(?<gfXdbHQlk0>\\w{2})/name"));
+    }
+
+    @Test
+    void testNormalization() {
         assertThat("/a/./b", pathMatcherMatches("/a/b"));
         assertThat("/a/b/../c", pathMatcherMatches("/a/c"));
     }
@@ -89,7 +97,7 @@ class PathMatchersTest {
     }
 
     @Test
-    public void testOptionals() {
+    void testOptionals() {
         assertThat("/foo/bar", pathMatcherMatches("/foo[/bar]"));
         assertThat("/foo", pathMatcherMatches("/foo[/bar]"));
         assertThat("/foo/ba", not(pathMatcherMatches("/foo[/bar]")));
diff --git a/integrations/langchain4j/providers/oracle/src/test/java/io/helidon/integrations/langchain4j/providers/oracle/OracleEmbeddingStorePropertiesTest.java b/integrations/langchain4j/providers/oracle/src/test/java/io/helidon/integrations/langchain4j/providers/oracle/OracleEmbeddingStorePropertiesTest.java
index 31d7127008..d1066f5383 100644
--- a/integrations/langchain4j/providers/oracle/src/test/java/io/helidon/integrations/langchain4j/providers/oracle/OracleEmbeddingStorePropertiesTest.java
+++ b/integrations/langchain4j/providers/oracle/src/test/java/io/helidon/integrations/langchain4j/providers/oracle/OracleEmbeddingStorePropertiesTest.java
@@ -35,6 +35,7 @@ class OracleEmbeddingStorePropertiesTest {
         var config = OracleEmbeddingStoreConfig.create(Services.get(Config.class)
                                                                .get(OracleEmbeddingStoreConfig.CONFIG_ROOT));
         assertThat(config, is(notNullValue()));
+        assertThat(config.enabled(), is(true));
         assertThat(config.dataSource().isPresent(), is(true));
         assertThat(config.dataSource().get(), is("datasource"));
         assertThat(config.exactSearch().isPresent(), is(true));
diff --git a/integrations/langchain4j/providers/oracle/src/test/resources/application.yaml b/integrations/langchain4j/providers/oracle/src/test/resources/application.yaml
index 627c56980f..cd33a90d32 100644
--- a/integrations/langchain4j/providers/oracle/src/test/resources/application.yaml
+++ b/integrations/langchain4j/providers/oracle/src/test/resources/application.yaml
@@ -17,6 +17,7 @@
 langchain4j:
   oracle:
     embedding-store:
+      enabled: true
       data-source: "datasource"
       exact-search: false
       vector-index-create-option: "CREATE_IF_NOT_EXISTS"
diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestMetaAnnotation.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestClassLevelMetaAnnotation.java
similarity index 96%
rename from microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestMetaAnnotation.java
rename to microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestClassLevelMetaAnnotation.java
index e441778690..2fc152a538 100644
--- a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestMetaAnnotation.java
+++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestClassLevelMetaAnnotation.java
@@ -33,8 +33,8 @@ import org.junit.jupiter.api.Test;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 
-@TestMetaAnnotation.MetaAnnotation
-class TestMetaAnnotation {
+@TestClassLevelMetaAnnotation.MetaAnnotation
+class TestClassLevelMetaAnnotation {
 
     @Inject
     MyBean bean;
diff --git a/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestMethodLevelMetaAnnotation.java b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestMethodLevelMetaAnnotation.java
new file mode 100644
index 0000000000..6051e281c0
--- /dev/null
+++ b/microprofile/tests/testing/junit5/src/test/java/io/helidon/microprofile/tests/testing/junit5/TestMethodLevelMetaAnnotation.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.tests.testing.junit5;
+
+import io.helidon.microprofile.testing.AddBean;
+import io.helidon.microprofile.testing.AddConfig;
+import io.helidon.microprofile.testing.AddConfigBlock;
+import io.helidon.microprofile.testing.Configuration;
+import jakarta.inject.Inject;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+import io.helidon.microprofile.testing.junit5.HelidonTest;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+@HelidonTest
+class TestMethodLevelMetaAnnotation {
+
+    @Inject
+    private MyBean bean;
+
+    @Inject
+    @ConfigProperty(name = "some.key1")
+    private String value1;
+
+    @Inject
+    @ConfigProperty(name = "some.key2")
+    private String value2;
+
+    @Inject
+    @ConfigProperty(name = "some.key")
+    private String someKey;
+
+    @Inject
+    @ConfigProperty(name = "another.key")
+    private String anotherKey;
+
+    @Inject
+    @ConfigProperty(name = "second-key")
+    private String anotherValue;
+
+    @MyTestMethod
+    void testAnnotationComposition() {
+        assertThat(bean.hello(), is("hello"));
+        assertThat(value1, is("some.value1"));
+        assertThat(value2, is("some.value2"));
+        assertThat(someKey, is("some.value"));
+        assertThat(anotherKey, is("another.value"));
+        assertThat(anotherValue, is("test-custom-config-second-value"));
+    }
+
+
+    @Test
+    @Target(ElementType.METHOD)
+    @Retention(RetentionPolicy.RUNTIME)
+    @AddBean(MyBean.class)
+    @AddConfigBlock("""
+            some.key1=some.value1
+            some.key2=some.value2
+        """)
+    @AddConfig(key = "second-key", value = "test-custom-config-second-value")
+    @Configuration(configSources = {"testConfigSources.properties", "testConfigSources.yaml"})
+    public @interface MyTestMethod { }
+
+
+    static class MyBean {
+        public String hello() {
+            return "hello";
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMetaAnnotation.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestClassLevelMetaAnnotation.java
similarity index 96%
rename from microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMetaAnnotation.java
rename to microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestClassLevelMetaAnnotation.java
index a13f5500eb..6ec2e36b5c 100644
--- a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMetaAnnotation.java
+++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestClassLevelMetaAnnotation.java
@@ -33,8 +33,8 @@ import io.helidon.microprofile.testing.testng.HelidonTest;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.testng.annotations.Test;
 
-@TestMetaAnnotation.MetaAnnotation
-public class TestMetaAnnotation {
+@TestClassLevelMetaAnnotation.MetaAnnotation
+public class TestClassLevelMetaAnnotation {
 
     @Inject
     MyBean bean;
diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMethodLevelMetaAnnotation.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMethodLevelMetaAnnotation.java
new file mode 100644
index 0000000000..a9bf2162ef
--- /dev/null
+++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMethodLevelMetaAnnotation.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.microprofile.tests.testing.testng;
+
+import io.helidon.microprofile.testing.AddBean;
+import io.helidon.microprofile.testing.AddConfig;
+import io.helidon.microprofile.testing.AddConfigBlock;
+import io.helidon.microprofile.testing.Configuration;
+import io.helidon.microprofile.testing.testng.HelidonTest;
+import jakarta.inject.Inject;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.testng.annotations.Test;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+@HelidonTest
+public class TestMethodLevelMetaAnnotation {
+
+    @Inject
+    private MyBean bean;
+
+    @Inject
+    @ConfigProperty(name = "some.key1")
+    private String value1;
+
+    @Inject
+    @ConfigProperty(name = "some.key2")
+    private String value2;
+
+    @Inject
+    @ConfigProperty(name = "some.key")
+    private String someKey;
+
+    @Inject
+    @ConfigProperty(name = "another.key")
+    private String anotherKey;
+
+    @Inject
+    @ConfigProperty(name = "second-key")
+    private String anotherValue;
+
+    @Test // org.testng.annotations.Test is not inheritable and can't be used in meta-annotation
+    @MyTestMethod
+    void testAnnotationComposition() {
+        assertThat(bean.hello(), is("hello"));
+        assertThat(value1, is("some.value1"));
+        assertThat(value2, is("some.value2"));
+        assertThat(someKey, is("some.value"));
+        assertThat(anotherKey, is("another.value"));
+        assertThat(anotherValue, is("test-custom-config-second-value"));
+    }
+
+
+    @Target(ElementType.METHOD)
+    @Retention(RetentionPolicy.RUNTIME)
+    @AddBean(MyBean.class)
+    @AddConfigBlock("""
+            some.key1=some.value1
+            some.key2=some.value2
+        """)
+    @AddConfig(key = "second-key", value = "test-custom-config-second-value")
+    @Configuration(configSources = {"testConfigSources.properties", "testConfigSources.yaml"})
+    public @interface MyTestMethod { }
+
+
+    static class MyBean {
+        public String hello() {
+            return "hello";
+        }
+    }
+}
diff --git a/webserver/tests/http2/src/test/java/io/helidon/webserver/tests/http2/BeforeTrailersTest.java b/webserver/tests/http2/src/test/java/io/helidon/webserver/tests/http2/BeforeTrailersTest.java
new file mode 100644
index 0000000000..c43631aca3
--- /dev/null
+++ b/webserver/tests/http2/src/test/java/io/helidon/webserver/tests/http2/BeforeTrailersTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.webserver.tests.http2;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import io.helidon.http.Header;
+import io.helidon.http.HeaderNames;
+import io.helidon.webclient.api.HttpClientResponse;
+import io.helidon.webclient.api.WebClient;
+import io.helidon.webclient.http2.Http2Client;
+import io.helidon.webserver.http.HttpRouting;
+import io.helidon.webserver.testing.junit5.ServerTest;
+import io.helidon.webserver.testing.junit5.SetUpRoute;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+@ServerTest
+class BeforeTrailersTest {
+    static private final AtomicBoolean CALLED = new AtomicBoolean();
+
+    private final WebClient webClient;
+
+    BeforeTrailersTest(WebClient webClient) {
+        this.webClient = webClient;
+    }
+
+    @SetUpRoute
+    static void routing(HttpRouting.Builder builder) {
+        builder.addFilter((chain, req, res) -> {
+                    if (req.path().path().equals("/trailers")) {
+                        res.header(HeaderNames.TRAILER, "helidon");
+                        res.beforeTrailers(trailers -> {
+                            trailers.add(HeaderNames.create("helidon"), "rocks");
+                            CALLED.set(true);
+                        });
+                    }
+                    chain.proceed();
+                })
+                .any((req, res) -> res.send("hello"));
+    }
+
+    @BeforeEach
+    void reset() {
+        CALLED.set(false);
+    }
+
+    @Test
+    void testNoBeforeTrailers() {
+        Http2Client http2Client = webClient.client(Http2Client.PROTOCOL);
+        try (HttpClientResponse res = http2Client.get("/noTrailers").request()) {
+            assertThat(res.status().code(), is(200));
+            assertThat(CALLED.get(), is(false));
+        }
+    }
+
+    @Test
+    void testBeforeTrailers() {
+        Http2Client http2Client = webClient.client(Http2Client.PROTOCOL);
+        try (HttpClientResponse res = http2Client.get("/trailers").request()) {
+            assertThat(res.status().code(), is(200));
+            assertThat(CALLED.get(), is(true));
+            assertThat(res.entity().as(String.class), is("hello"));     // need to read entity
+            Header trailer = res.trailers().get(HeaderNames.create("helidon"));
+            assertThat(trailer.get(), is("rocks"));
+        }
+    }
+}
diff --git a/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BeforeTrailersTest.java b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BeforeTrailersTest.java
new file mode 100644
index 0000000000..aea3ec4abe
--- /dev/null
+++ b/webserver/tests/webserver/src/test/java/io/helidon/webserver/tests/BeforeTrailersTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2025 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.webserver.tests;
+
+import io.helidon.http.Header;
+import io.helidon.http.HeaderName;
+import io.helidon.http.HeaderNames;
+import io.helidon.webclient.api.HttpClientResponse;
+import io.helidon.webclient.api.WebClient;
+import io.helidon.webserver.http.HttpRouting;
+import io.helidon.webserver.testing.junit5.ServerTest;
+import io.helidon.webserver.testing.junit5.SetUpRoute;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+@ServerTest
+class BeforeTrailersTest {
+    private static final HeaderName HELIDON = HeaderNames.create("helidon");
+
+    private final WebClient client;
+
+    BeforeTrailersTest(WebClient client) {
+        this.client = client;
+    }
+
+    @SetUpRoute
+    static void routing(HttpRouting.Builder builder) {
+        builder.addFilter((chain, req, res) -> {
+                    if (req.path().path().equals("/trailers")) {
+                        res.header(HeaderNames.TRAILER, "helidon");
+                        res.beforeTrailers(trailers -> {
+                            trailers.add(HELIDON, "rocks");
+                        });
+                    }
+                    chain.proceed();
+                })
+                .any((req, res) -> res.send("hello"));
+    }
+
+    @Test
+    void testNoBeforeTrailers() {
+        try (HttpClientResponse res = client.get("/noTrailers").request()) {
+            assertThat(res.status().code(), is(200));
+            assertThat(res.trailers().contains(HELIDON), is(false));
+        }
+    }
+
+    @Test
+    void testBeforeTrailers() {
+        try (HttpClientResponse res = client.get("/trailers").request()) {
+            assertThat(res.status().code(), is(200));
+            assertThat(res.entity().as(String.class), is("hello"));     // need to read entity
+            Header trailer = res.trailers().get(HELIDON);
+            assertThat(trailer.get(), is("rocks"));
+        }
+    }
+}