+ * This sampler uses the + * BlazeMeter HTTP plugin and + * requires Java 17+ at runtime. It supports the same HTTP request features as + * {@link us.abstracta.jmeter.javadsl.http.DslHttpSampler} (headers, body, cookies, cache, etc.) + * plus multi-protocol client configuration. + * + * @since 2.3 + */ +public class DslHttp2Sampler extends DslHttpSampler { + + private static final String DEFAULT_NAME = "bzm - HTTP Sampler"; + private static final String PROFILE_PROPERTY = "HTTP2Sampler.profile"; + + protected ClientProfile clientProfile; + protected Boolean enableHttp3; + protected Boolean enableHttp2; + protected Boolean enableHttp1; + protected Boolean alpnEnabled; + protected Boolean automaticFallback; + protected Boolean protocolErrorFallback; + protected Boolean h2cUpgrade; + protected Boolean http2PriorKnowledge; + + public DslHttp2Sampler(String name, String url) { + super(name != null ? name : DEFAULT_NAME, url); + } + + /** + * Specifies the client profile which defines default protocol negotiation behavior. + * + * @param profile the client profile to use. When none is specified, the plugin default + * ({@link ClientProfile#BROWSER_LIKE}) is used. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler clientProfile(ClientProfile profile) { + this.clientProfile = profile; + return this; + } + + /** + * Enables or disables HTTP/3 (QUIC) support for this sampler. + * + * @param enable when {@code true}, allows HTTP/3 when the origin advertises it via Alt-Svc. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler enableHttp3(boolean enable) { + this.enableHttp3 = enable; + return this; + } + + /** + * Enables or disables HTTP/2 support for this sampler. + * + * @param enable when {@code true}, allows HTTP/2 negotiation via ALPN (HTTPS) or h2c (HTTP). + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler enableHttp2(boolean enable) { + this.enableHttp2 = enable; + return this; + } + + /** + * Enables or disables HTTP/1.1 support for this sampler. + * + * @param enable when {@code true}, allows HTTP/1.1 requests and fallback. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler enableHttp1(boolean enable) { + this.enableHttp1 = enable; + return this; + } + + /** + * Enables or disables TLS ALPN for HTTPS connections. + * + * @param enable when {@code true}, enables ALPN for HTTP/1.1 and HTTP/2 protocol negotiation. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler alpnEnabled(boolean enable) { + this.alpnEnabled = enable; + return this; + } + + /** + * Enables or disables automatic fallback between protocols (e.g. HTTP/3 to HTTP/2 to HTTP/1.1). + * + * @param enable when {@code true}, enables automatic protocol fallback. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler automaticFallback(boolean enable) { + this.automaticFallback = enable; + return this; + } + + /** + * Enables or disables fallback to HTTP/1.1 when an HTTP/2 {@code protocol_error} occurs. + * + * @param enable when {@code true}, retries with HTTP/1.1 on protocol errors. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler protocolErrorFallback(boolean enable) { + this.protocolErrorFallback = enable; + return this; + } + + /** + * Enables HTTP/1.1 to HTTP/2 cleartext (h2c) upgrade for {@code http://} origins. + * + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler h2cUpgrade() { + return h2cUpgrade(true); + } + + /** + * Same as {@link #h2cUpgrade()} but allowing to enable or disable the setting. + * + * @param enable when {@code true}, enables h2c upgrade negotiation. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler h2cUpgrade(boolean enable) { + this.h2cUpgrade = enable; + return this; + } + + /** + * Forces HTTP/2 prior knowledge (h2c) for cleartext origins, skipping the upgrade negotiation. + *
+ * Use only when the server is known to support h2c prior knowledge. + * + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler http2PriorKnowledge() { + return http2PriorKnowledge(true); + } + + /** + * Same as {@link #http2PriorKnowledge()} but allowing to enable or disable the setting. + * + * @param enable when {@code true}, forces h2c prior knowledge. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler http2PriorKnowledge(boolean enable) { + this.http2PriorKnowledge = enable; + return this; + } + + /** + * Configures the sampler to use HTTP/2 only, disabling HTTP/1.1 and HTTP/3. + *
+ * For {@code https://} origins, ALPN is enabled to negotiate HTTP/2 over TLS.
+ *
+ * @return the sampler for further configuration or usage.
+ */
+ public DslHttp2Sampler http2Only() {
+ return clientProfile(ClientProfile.BROWSER_LIKE_CUSTOM)
+ .enableHttp1(false)
+ .enableHttp2(true)
+ .enableHttp3(false)
+ .alpnEnabled(true)
+ .automaticFallback(false);
+ }
+
+ /**
+ * Configures the sampler to use HTTP/1.1 only, disabling HTTP/2 and HTTP/3.
+ *
+ * @return the sampler for further configuration or usage.
+ */
+ public DslHttp2Sampler http1Only() {
+ return clientProfile(ClientProfile.BROWSER_LIKE_CUSTOM)
+ .enableHttp1(true)
+ .enableHttp2(false)
+ .enableHttp3(false);
+ }
+
+ @Override
+ protected TestElement buildConfiguredTestElement() {
+ TestElement ret = buildTestElement();
+ return BaseTestElement.configureTestElement(ret, name, HTTP2SamplerGui.class);
+ }
+
+ @Override
+ protected TestElement buildTestElement() {
+ if (JMeterUtils.getProperty(DslBaseHttpSampler.RESET_CONNECTIONS_BETWEEN_ITERATIONS_PROP)
+ == null) {
+ JMeterUtils.setProperty(DslBaseHttpSampler.RESET_CONNECTIONS_BETWEEN_ITERATIONS_PROP,
+ String.valueOf(false));
+ }
+ HTTP2Sampler ret = new HTTP2Sampler();
+ HttpElementHelper.modifyTestElementUrl(ret, protocol, host, port, path);
+ HttpElementHelper.modifyTestElementTimeouts(ret, connectionTimeout, responseTimeout);
+ HttpElementHelper.modifyTestElementProxy(ret, proxyUrl, proxyUser, proxyPassword);
+ configureHttp2Sampler(ret);
+ return ret;
+ }
+
+ private void configureHttp2Sampler(HTTP2Sampler elem) {
+ HTTPSamplerProxy proxy = configureHttpTestElement(new HTTPSamplerProxy());
+ copyHttpSamplerProperties(proxy, elem);
+ if (clientProfile != null) {
+ elem.setProfile(clientProfile.propertyValue);
+ }
+ if (enableHttp3 != null) {
+ elem.setEnableHttp3(enableHttp3);
+ }
+ if (enableHttp2 != null) {
+ elem.setEnableHttp2(enableHttp2);
+ }
+ if (enableHttp1 != null) {
+ elem.setEnableHttp1(enableHttp1);
+ }
+ if (alpnEnabled != null) {
+ elem.setAlpnEnabled(alpnEnabled);
+ }
+ if (automaticFallback != null) {
+ elem.setFallbackEnabled(automaticFallback);
+ }
+ if (protocolErrorFallback != null) {
+ elem.setProtocolErrorFallbackEnabled(protocolErrorFallback);
+ }
+ if (h2cUpgrade != null) {
+ elem.setHttp1UpgradeEnabled(h2cUpgrade);
+ }
+ if (http2PriorKnowledge != null) {
+ elem.setHttp2PriorKnowledgeEnabled(http2PriorKnowledge);
+ }
+ }
+
+ private void copyHttpSamplerProperties(HTTPSamplerBase source, HTTP2Sampler target) {
+ target.setMethod(source.getMethod());
+ target.setArguments(cloneArguments(source.getArguments()));
+ target.setDoMultipart(source.getUseMultipart());
+ target.setHTTPFiles(source.getHTTPFiles());
+ if (source.getContentEncoding() != null) {
+ target.setContentEncoding(source.getContentEncoding());
+ }
+ target.setFollowRedirects(source.getFollowRedirects());
+ target.setUseKeepAlive(source.getUseKeepAlive());
+ if (source.getPropertyAsBoolean(HTTPSamplerBase.IMAGE_PARSER, false)) {
+ HttpElementHelper.modifyTestElementEmbeddedResources(target, true,
+ source.getPropertyAsString(HTTPSamplerBase.EMBEDDED_URL_RE),
+ source.getPropertyAsString(HTTPSamplerBase.EMBEDDED_URL_EXCLUDE_RE));
+ }
+ }
+
+ private Arguments cloneArguments(Arguments args) {
+ Arguments ret = new Arguments();
+ for (int i = 0; i < args.getArgumentCount(); i++) {
+ ret.addArgument((HTTPArgument) args.getArgument(i).clone());
+ }
+ return ret;
+ }
+
+ @Override
+ public HashTree buildTreeUnder(HashTree parent, BuildTreeContext context) {
+ files.forEach(f -> f.setPath(context.processAssetFile(f.getPath())));
+ if (path == null && urlBuilder != null) {
+ path = urlBuilder.build();
+ }
+ HashTree ret = parent.add(buildConfiguredTestElement());
+ if (!headers.isEmpty()) {
+ context.buildChild(headers, ret);
+ }
+ new DslCookieManager().registerDependency(context);
+ new DslCacheManager().registerDependency(context);
+ return ret;
+ }
+
+ /**
+ * Defines client profiles that bundle default protocol negotiation settings.
+ */
+ public enum ClientProfile implements EnumPropertyValue {
+ /**
+ * Browser-like profile with HTTP/3, HTTP/2, HTTP/1.1 and automatic fallback enabled.
+ */
+ BROWSER_LIKE("browser-like"),
+ /**
+ * Browser-compatible profile for less common browsers.
+ */
+ BROWSER_COMPATIBLE("browser-compatible"),
+ /**
+ * Legacy profile oriented to older systems.
+ */
+ LEGACY("legacy"),
+ /**
+ * Custom profile where per-sampler protocol toggles are used.
+ */
+ BROWSER_LIKE_CUSTOM("browser-like-custom");
+
+ public final String propertyValue;
+
+ ClientProfile(String propertyValue) {
+ this.propertyValue = propertyValue;
+ }
+
+ @Override
+ public String propertyValue() {
+ return propertyValue;
+ }
+ }
+
+ public static class CodeBuilder extends SingleTestElementCallBuilder
+ * This element uses the
+ * BlazeMeter HTTP plugin
+ * {@code bzm - HTTP Async Controller}. Check the plugin documentation for more details.
+ *
+ * By default, this element does not generate a parent sample result. Check provided methods to
+ * change this behavior.
+ *
+ * @since 2.3
+ */
+public class DslHttpAsyncController extends BaseController
+ * This module uses the
+ * BlazeMeter HTTP plugin.
+ * It requires Java 17+ at runtime.
+ *
+ * Example usage:
+ *
+ *
+ * By default, the sampler uses the {@link DslHttp2Sampler.ClientProfile#BROWSER_LIKE}
+ * profile which enables HTTP/3, HTTP/2, and HTTP/1.1 with automatic fallback.
+ *
+ * @param url specifies URL to send HTTP requests to (e.g. {@code https://example.com/path}).
+ * @return the sampler for further configuration or usage.
+ * @see DslHttp2Sampler
+ */
+ public static DslHttp2Sampler http2Sampler(String url) {
+ return http2Sampler(null, url);
+ }
+
+ /**
+ * Same as {@link #http2Sampler(String)} but allowing to set a name on the sampler.
+ *
+ * @param name is the label assigned to the sampler in collected metrics.
+ * @param url specifies URL to send HTTP requests to.
+ * @return the sampler for further configuration or usage.
+ * @see #http2Sampler(String)
+ */
+ public static DslHttp2Sampler http2Sampler(String name, String url) {
+ return new DslHttp2Sampler(name, url);
+ }
+
+ /**
+ * Builds an HTTP Async Controller to run HTTP/2 samplers concurrently within a thread
+ * iteration.
+ *
+ * @param children test elements to execute concurrently.
+ * @return the controller for further configuration or usage.
+ * @see DslHttpAsyncController
+ */
+ public static DslHttpAsyncController httpAsyncController(ThreadGroupChild... children) {
+ return DslHttpAsyncController.httpAsyncController(children);
+ }
+
+ /**
+ * Same as {@link #httpAsyncController(ThreadGroupChild...)} but allowing to set a name on the
+ * controller.
+ *
+ * @param name is the label assigned to the controller in collected metrics.
+ * @param children test elements to execute concurrently.
+ * @return the controller for further configuration or usage.
+ */
+ public static DslHttpAsyncController httpAsyncController(String name,
+ ThreadGroupChild... children) {
+ return DslHttpAsyncController.httpAsyncController(name, children);
+ }
+
+}
diff --git a/jmeter-java-dsl-http2/src/test/java/DslHttp2SamplerTest.java b/jmeter-java-dsl-http2/src/test/java/DslHttp2SamplerTest.java
new file mode 100644
index 00000000..b469df09
--- /dev/null
+++ b/jmeter-java-dsl-http2/src/test/java/DslHttp2SamplerTest.java
@@ -0,0 +1,48 @@
+import static org.assertj.core.api.Assertions.assertThat;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.testPlan;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.threadGroup;
+import static us.abstracta.jmeter.javadsl.http2.Http2JmeterDsl.http2Sampler;
+
+import org.apache.http.entity.ContentType;
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.JmeterDslTest;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class DslHttp2SamplerTest extends JmeterDslTest {
+
+ @Test
+ public void shouldSendHttpRequestWhenHttp2Sampler() throws Exception {
+ TestPlanStats stats = testPlan(
+ threadGroup(1, 1,
+ http2Sampler(wiremockUri)
+ )
+ ).run();
+ assertThat(stats.overall().samplesCount()).isEqualTo(1);
+ assertThat(stats.overall().errorsCount()).isZero();
+ }
+
+ @Test
+ public void shouldSendPostWhenHttp2SamplerWithPost() throws Exception {
+ TestPlanStats stats = testPlan(
+ threadGroup(1, 1,
+ http2Sampler(wiremockUri)
+ .post(JSON_BODY, ContentType.APPLICATION_JSON)
+ )
+ ).run();
+ assertThat(stats.overall().samplesCount()).isEqualTo(1);
+ assertThat(stats.overall().errorsCount()).isZero();
+ }
+
+ @Test
+ public void shouldUseHttp2OnlyProfileWhenHttp2Only() throws Exception {
+ TestPlanStats stats = testPlan(
+ threadGroup(1, 1,
+ http2Sampler(wiremockUri)
+ .http2Only()
+ )
+ ).run();
+ assertThat(stats.overall().samplesCount()).isEqualTo(1);
+ assertThat(stats.overall().errorsCount()).isZero();
+ }
+
+}
diff --git a/jmeter-java-dsl-http2/src/test/resources/jtls/custom-sample-jtl.xml b/jmeter-java-dsl-http2/src/test/resources/jtls/custom-sample-jtl.xml
new file mode 100644
index 00000000..28007e9a
--- /dev/null
+++ b/jmeter-java-dsl-http2/src/test/resources/jtls/custom-sample-jtl.xml
@@ -0,0 +1,7 @@
+
+{@code
+ * import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+ * import static us.abstracta.jmeter.javadsl.http2.Http2JmeterDsl.*;
+ * import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+ *
+ * public class Test {
+ *
+ * public static void main(String[] args) throws Exception {
+ * TestPlanStats stats = testPlan(
+ * threadGroup(1, 1,
+ * http2Sampler("https://example.com")
+ * .http2Only()
+ * )
+ * ).run();
+ * }
+ * }
+ * }
+ *
+ * @since 2.3
+ */
+public class Http2JmeterDsl {
+
+ private Http2JmeterDsl() {
+ }
+
+ /**
+ * Builds an HTTP/2 sampler pointing to the given URL.
+ *