diff --git a/README.md b/README.md
index f833e90..d7c79e2 100644
--- a/README.md
+++ b/README.md
@@ -237,6 +237,39 @@ The available profiles are:
- **Legacy / Older Systems** (`legacy`)
- **Browser-like (Custom)** (`browser-like-custom`) (persists per-sampler toggles below)
+You can also define additional profiles in `user.properties` / `jmeter.properties`.
+Custom profiles are loaded by both the GUI and non-GUI runtime, so the same profile
+works in local, CLI, and distributed test runs.
+
+Example:
+
+```properties
+blazemeter.http.profiles.mobile-h3.label=Mobile H3 Conservative
+blazemeter.http.profiles.mobile-h3.extends=browser-like
+blazemeter.http.profiles.mobile-h3.enableHttp3=true
+blazemeter.http.profiles.mobile-h3.enableHttp2=true
+blazemeter.http.profiles.mobile-h3.enableHttp1=true
+blazemeter.http.profiles.mobile-h3.happyEyeballsDelayMs=500
+blazemeter.http.profiles.mobile-h3.http3BrokenCooldownMs=600000
+```
+
+Profile IDs may contain lowercase letters, digits, `_`, and `-`. If `extends` is omitted,
+the custom profile inherits from `browser-like`. Supported fields are:
+
+`label`, `extends`, `enableHttp3`, `enableHttp2`, `enableHttp1`, `alpnEnabled`,
+`fallbackEnabled`, `protocolErrorFallbackEnabled`, `altSvcCacheEnabled`,
+`http1OnlyCacheEnabled`, `h2cCacheEnabled`, `http2PriorKnowledge`,
+`h2cUpgradeEnabled`, `happyEyeballsDelayMs`, `http3BrokenCooldownMs`,
+`http1OnlyCooldownMs`, and `h2cCacheTtlMs`.
+
+For larger profile sets, put the same properties in a separate file and reference it:
+
+```properties
+blazemeter.http.profiles.file=/path/to/http-profiles.properties
+```
+
+Profile values defined directly in JMeter properties override values loaded from that file.
+
#### Client Behavior → Protocols / Fallback / Cache / Timing
@@ -391,6 +424,8 @@ Restart JMeter after changing JMeter properties that are applied when affected c
| **blazemeter.http.maxConcurrentAsyncInController** | Default concurrency cap inside **`bzm - HTTP Async Controller`** when parallel limiting is unchecked | 100 |
| **HTTPSampler.response_timeout** | Default response timeout (ms) when the sampler defines none | 0 |
| **http.post_add_content_type_if_missing** | Add Content-Type header if missing? | false |
+| **blazemeter.http.profile** | Default client behavior profile when a sampler does not define one | browser-like |
+| **blazemeter.http.profiles.file** | Optional `.properties` file containing custom profile definitions | |
| **blazemeter.http.enableHttp3** | Enable HTTP/3 support (Alt-Svc + QUIC) | profile |
| **blazemeter.http.enableHttp2** | Enable HTTP/2 support | profile |
| **blazemeter.http.enableHttp1** | Enable HTTP/1.1 support | profile |
@@ -403,7 +438,7 @@ Restart JMeter after changing JMeter properties that are applied when affected c
| **blazemeter.http.altSvcCacheEnabled** | Enable Alt-Svc cache for HTTP/3 discovery | profile |
| **blazemeter.http.http1OnlyCacheEnabled** | Enable HTTP/1.1-only cache for HTTPS origins | profile |
| **blazemeter.http.h2cCacheEnabled** | Enable H2C cache for cleartext origins | profile |
-| **blazemeter.http.h2cUpgradeEnabled** | Enable **H2C Upgrade (HTTP/1.1 Upgrade)** | false |
+| **blazemeter.http.h2cUpgradeEnabled** | Enable **H2C Upgrade (HTTP/1.1 Upgrade)** | profile |
| **blazemeter.http.h2cCacheTtlMs** | H2C cache TTL in milliseconds | profile |
| **blazemeter.http.http1OnlyCooldownMs** | HTTP/1.1-only cache TTL in milliseconds | profile |
| **blazemeter.http.http3BrokenCooldownMs** | Cooldown before retrying HTTP/3 after failures (ms) | profile |
@@ -432,4 +467,3 @@ Artifacts land under **`target/`**. Compilation uses the **`jmeter.version`** de
## License
Distributed under the **Apache License 2.0**. See **`LICENSE`** in this repository.
-
diff --git a/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfile.java b/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfile.java
new file mode 100644
index 0000000..b2709a2
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfile.java
@@ -0,0 +1,218 @@
+package com.blazemeter.jmeter.http2.core;
+
+public final class HTTP2ClientProfile {
+
+ private final String id;
+ private final String label;
+ private final boolean enableHttp3;
+ private final boolean enableHttp2;
+ private final boolean enableHttp1;
+ private final boolean alpnEnabled;
+ private final boolean fallbackEnabled;
+ private final boolean protocolErrorFallbackEnabled;
+ private final boolean altSvcCacheEnabled;
+ private final boolean http1OnlyCacheEnabled;
+ private final boolean h2cCacheEnabled;
+ private final boolean http2PriorKnowledgeEnabled;
+ private final boolean h2cUpgradeEnabled;
+ private final long happyEyeballsDelayMs;
+ private final long http3BrokenCooldownMs;
+ private final long http1OnlyCooldownMs;
+ private final long h2cCacheTtlMs;
+
+ private HTTP2ClientProfile(Builder builder) {
+ id = builder.id;
+ label = builder.label;
+ enableHttp3 = builder.enableHttp3;
+ enableHttp2 = builder.enableHttp2;
+ enableHttp1 = builder.enableHttp1;
+ alpnEnabled = builder.alpnEnabled;
+ fallbackEnabled = builder.fallbackEnabled;
+ protocolErrorFallbackEnabled = builder.protocolErrorFallbackEnabled;
+ altSvcCacheEnabled = builder.altSvcCacheEnabled;
+ http1OnlyCacheEnabled = builder.http1OnlyCacheEnabled;
+ h2cCacheEnabled = builder.h2cCacheEnabled;
+ http2PriorKnowledgeEnabled = builder.http2PriorKnowledgeEnabled;
+ h2cUpgradeEnabled = builder.h2cUpgradeEnabled;
+ happyEyeballsDelayMs = builder.happyEyeballsDelayMs;
+ http3BrokenCooldownMs = builder.http3BrokenCooldownMs;
+ http1OnlyCooldownMs = builder.http1OnlyCooldownMs;
+ h2cCacheTtlMs = builder.h2cCacheTtlMs;
+ }
+
+ public static Builder builder(String id, String label) {
+ return new Builder(id, label);
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public boolean isEnableHttp3() {
+ return enableHttp3;
+ }
+
+ public boolean isEnableHttp2() {
+ return enableHttp2;
+ }
+
+ public boolean isEnableHttp1() {
+ return enableHttp1;
+ }
+
+ public boolean isAlpnEnabled() {
+ return alpnEnabled;
+ }
+
+ public boolean isFallbackEnabled() {
+ return fallbackEnabled;
+ }
+
+ public boolean isProtocolErrorFallbackEnabled() {
+ return protocolErrorFallbackEnabled;
+ }
+
+ public boolean isAltSvcCacheEnabled() {
+ return altSvcCacheEnabled;
+ }
+
+ public boolean isHttp1OnlyCacheEnabled() {
+ return http1OnlyCacheEnabled;
+ }
+
+ public boolean isH2cCacheEnabled() {
+ return h2cCacheEnabled;
+ }
+
+ public boolean isHttp2PriorKnowledgeEnabled() {
+ return http2PriorKnowledgeEnabled;
+ }
+
+ public boolean isH2cUpgradeEnabled() {
+ return h2cUpgradeEnabled;
+ }
+
+ public long getHappyEyeballsDelayMs() {
+ return happyEyeballsDelayMs;
+ }
+
+ public long getHttp3BrokenCooldownMs() {
+ return http3BrokenCooldownMs;
+ }
+
+ public long getHttp1OnlyCooldownMs() {
+ return http1OnlyCooldownMs;
+ }
+
+ public long getH2cCacheTtlMs() {
+ return h2cCacheTtlMs;
+ }
+
+ public static final class Builder {
+ private final String id;
+ private final String label;
+ private boolean enableHttp3;
+ private boolean enableHttp2;
+ private boolean enableHttp1;
+ private boolean alpnEnabled;
+ private boolean fallbackEnabled;
+ private boolean protocolErrorFallbackEnabled;
+ private boolean altSvcCacheEnabled;
+ private boolean http1OnlyCacheEnabled;
+ private boolean h2cCacheEnabled;
+ private boolean http2PriorKnowledgeEnabled;
+ private boolean h2cUpgradeEnabled;
+ private long happyEyeballsDelayMs;
+ private long http3BrokenCooldownMs;
+ private long http1OnlyCooldownMs;
+ private long h2cCacheTtlMs;
+
+ private Builder(String id, String label) {
+ this.id = id;
+ this.label = label;
+ }
+
+ public Builder enableHttp3(boolean enableHttp3) {
+ this.enableHttp3 = enableHttp3;
+ return this;
+ }
+
+ public Builder enableHttp2(boolean enableHttp2) {
+ this.enableHttp2 = enableHttp2;
+ return this;
+ }
+
+ public Builder enableHttp1(boolean enableHttp1) {
+ this.enableHttp1 = enableHttp1;
+ return this;
+ }
+
+ public Builder alpnEnabled(boolean alpnEnabled) {
+ this.alpnEnabled = alpnEnabled;
+ return this;
+ }
+
+ public Builder fallbackEnabled(boolean fallbackEnabled) {
+ this.fallbackEnabled = fallbackEnabled;
+ return this;
+ }
+
+ public Builder protocolErrorFallbackEnabled(boolean protocolErrorFallbackEnabled) {
+ this.protocolErrorFallbackEnabled = protocolErrorFallbackEnabled;
+ return this;
+ }
+
+ public Builder altSvcCacheEnabled(boolean altSvcCacheEnabled) {
+ this.altSvcCacheEnabled = altSvcCacheEnabled;
+ return this;
+ }
+
+ public Builder http1OnlyCacheEnabled(boolean http1OnlyCacheEnabled) {
+ this.http1OnlyCacheEnabled = http1OnlyCacheEnabled;
+ return this;
+ }
+
+ public Builder h2cCacheEnabled(boolean h2cCacheEnabled) {
+ this.h2cCacheEnabled = h2cCacheEnabled;
+ return this;
+ }
+
+ public Builder http2PriorKnowledgeEnabled(boolean http2PriorKnowledgeEnabled) {
+ this.http2PriorKnowledgeEnabled = http2PriorKnowledgeEnabled;
+ return this;
+ }
+
+ public Builder h2cUpgradeEnabled(boolean h2cUpgradeEnabled) {
+ this.h2cUpgradeEnabled = h2cUpgradeEnabled;
+ return this;
+ }
+
+ public Builder happyEyeballsDelayMs(long happyEyeballsDelayMs) {
+ this.happyEyeballsDelayMs = happyEyeballsDelayMs;
+ return this;
+ }
+
+ public Builder http3BrokenCooldownMs(long http3BrokenCooldownMs) {
+ this.http3BrokenCooldownMs = http3BrokenCooldownMs;
+ return this;
+ }
+
+ public Builder http1OnlyCooldownMs(long http1OnlyCooldownMs) {
+ this.http1OnlyCooldownMs = http1OnlyCooldownMs;
+ return this;
+ }
+
+ public Builder h2cCacheTtlMs(long h2cCacheTtlMs) {
+ this.h2cCacheTtlMs = h2cCacheTtlMs;
+ return this;
+ }
+
+ public HTTP2ClientProfile build() {
+ return new HTTP2ClientProfile(this);
+ }
+ }
+}
diff --git a/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfileConfig.java b/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfileConfig.java
index c42b629..d899763 100644
--- a/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfileConfig.java
+++ b/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfileConfig.java
@@ -13,6 +13,7 @@ public class HTTP2ClientProfileConfig {
private final Boolean http1OnlyCacheEnabled;
private final Boolean h2cCacheEnabled;
private final Boolean http2PriorKnowledgeEnabled;
+ private final Boolean h2cUpgradeEnabled;
private final Long happyEyeballsDelayMs;
private final Long http3BrokenCooldownMs;
private final Long http1OnlyCooldownMs;
@@ -30,6 +31,7 @@ private HTTP2ClientProfileConfig(Builder builder) {
this.http1OnlyCacheEnabled = builder.http1OnlyCacheEnabled;
this.h2cCacheEnabled = builder.h2cCacheEnabled;
this.http2PriorKnowledgeEnabled = builder.http2PriorKnowledgeEnabled;
+ this.h2cUpgradeEnabled = builder.h2cUpgradeEnabled;
this.happyEyeballsDelayMs = builder.happyEyeballsDelayMs;
this.http3BrokenCooldownMs = builder.http3BrokenCooldownMs;
this.http1OnlyCooldownMs = builder.http1OnlyCooldownMs;
@@ -84,6 +86,10 @@ public Boolean getHttp2PriorKnowledgeEnabled() {
return http2PriorKnowledgeEnabled;
}
+ public Boolean getH2cUpgradeEnabled() {
+ return h2cUpgradeEnabled;
+ }
+
public Long getHappyEyeballsDelayMs() {
return happyEyeballsDelayMs;
}
@@ -112,6 +118,7 @@ public static final class Builder {
private Boolean http1OnlyCacheEnabled;
private Boolean h2cCacheEnabled;
private Boolean http2PriorKnowledgeEnabled;
+ private Boolean h2cUpgradeEnabled;
private Long happyEyeballsDelayMs;
private Long http3BrokenCooldownMs;
private Long http1OnlyCooldownMs;
@@ -172,6 +179,11 @@ public Builder http2PriorKnowledgeEnabled(Boolean http2PriorKnowledgeEnabled) {
return this;
}
+ public Builder h2cUpgradeEnabled(Boolean h2cUpgradeEnabled) {
+ this.h2cUpgradeEnabled = h2cUpgradeEnabled;
+ return this;
+ }
+
public Builder happyEyeballsDelayMs(Long happyEyeballsDelayMs) {
this.happyEyeballsDelayMs = happyEyeballsDelayMs;
return this;
diff --git a/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfiles.java b/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfiles.java
new file mode 100644
index 0000000..5ebbfe9
--- /dev/null
+++ b/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfiles.java
@@ -0,0 +1,581 @@
+package com.blazemeter.jmeter.http2.core;
+
+import com.blazemeter.jmeter.http2.util.BzmHttpPluginProperties;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Pattern;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jmeter.util.JMeterUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class HTTP2ClientProfiles {
+
+ public static final String PROFILE_BROWSER_LIKE = "browser-like";
+ public static final String PROFILE_BROWSER_LIKE_CUSTOM = "browser-like-custom";
+ public static final String PROFILE_BROWSER_COMPATIBLE = "browser-compatible";
+ public static final String PROFILE_LEGACY = "legacy";
+ public static final String DEFAULT_PROFILE_ID = PROFILE_BROWSER_LIKE;
+ public static final long DEFAULT_HTTP3_BROKEN_COOLDOWN_MS = 300000;
+ public static final long DEFAULT_HTTP1_ONLY_COOLDOWN_MS = 300000;
+ public static final long DEFAULT_H2C_CACHE_TTL_MS = 300000;
+ public static final long DEFAULT_HAPPY_EYEBALLS_DELAY_MS = 250;
+
+ private static final Logger LOG = LoggerFactory.getLogger(HTTP2ClientProfiles.class);
+ private static final String PROFILE_PROPERTY = "httpJettyClient.profile";
+ private static final String PROFILE_PREFIX = "blazemeter.http.profiles.";
+ private static final String PROFILE_FILE_PROPERTY = "blazemeter.http.profiles.file";
+ private static final Pattern PROFILE_ID_PATTERN = Pattern.compile("[a-z0-9_-]+");
+ private static final Set BUILT_IN_PROFILE_IDS = new HashSet<>();
+ private static final Map BUILT_IN_PROFILES = new LinkedHashMap<>();
+ private static final Set PROFILE_FIELDS = new HashSet<>();
+
+ static {
+ BUILT_IN_PROFILES.put(PROFILE_BROWSER_LIKE, ProfileDefinition.builder(PROFILE_BROWSER_LIKE)
+ .label("Browser-like (Most Browsers)")
+ .enableHttp3(true)
+ .enableHttp2(true)
+ .enableHttp1(true)
+ .alpnEnabled(true)
+ .fallbackEnabled(true)
+ .protocolErrorFallbackEnabled(true)
+ .altSvcCacheEnabled(true)
+ .http1OnlyCacheEnabled(true)
+ .h2cCacheEnabled(true)
+ .http2PriorKnowledgeEnabled(false)
+ .h2cUpgradeEnabled(false)
+ .happyEyeballsDelayMs(DEFAULT_HAPPY_EYEBALLS_DELAY_MS)
+ .http3BrokenCooldownMs(DEFAULT_HTTP3_BROKEN_COOLDOWN_MS)
+ .http1OnlyCooldownMs(DEFAULT_HTTP1_ONLY_COOLDOWN_MS)
+ .h2cCacheTtlMs(DEFAULT_H2C_CACHE_TTL_MS)
+ .build());
+ BUILT_IN_PROFILES.put(PROFILE_BROWSER_COMPATIBLE,
+ ProfileDefinition.builder(PROFILE_BROWSER_COMPATIBLE)
+ .label("Browser-compatible (Other Browsers)")
+ .enableHttp3(true)
+ .enableHttp2(true)
+ .enableHttp1(true)
+ .alpnEnabled(true)
+ .fallbackEnabled(true)
+ .protocolErrorFallbackEnabled(true)
+ .altSvcCacheEnabled(true)
+ .http1OnlyCacheEnabled(true)
+ .h2cCacheEnabled(true)
+ .http2PriorKnowledgeEnabled(false)
+ .h2cUpgradeEnabled(false)
+ .happyEyeballsDelayMs(0L)
+ .http3BrokenCooldownMs(DEFAULT_HTTP3_BROKEN_COOLDOWN_MS)
+ .http1OnlyCooldownMs(DEFAULT_HTTP1_ONLY_COOLDOWN_MS)
+ .h2cCacheTtlMs(DEFAULT_H2C_CACHE_TTL_MS)
+ .build());
+ BUILT_IN_PROFILES.put(PROFILE_LEGACY, ProfileDefinition.builder(PROFILE_LEGACY)
+ .label("Legacy / Older Systems")
+ .enableHttp3(false)
+ .enableHttp2(false)
+ .enableHttp1(true)
+ .alpnEnabled(false)
+ .fallbackEnabled(false)
+ .protocolErrorFallbackEnabled(false)
+ .altSvcCacheEnabled(false)
+ .http1OnlyCacheEnabled(false)
+ .h2cCacheEnabled(true)
+ .http2PriorKnowledgeEnabled(false)
+ .h2cUpgradeEnabled(true)
+ .happyEyeballsDelayMs(0L)
+ .http3BrokenCooldownMs(0L)
+ .http1OnlyCooldownMs(0L)
+ .h2cCacheTtlMs(DEFAULT_H2C_CACHE_TTL_MS)
+ .build());
+ BUILT_IN_PROFILES.put(PROFILE_BROWSER_LIKE_CUSTOM,
+ ProfileDefinition.builder(PROFILE_BROWSER_LIKE_CUSTOM)
+ .label("Browser-like (Custom)")
+ .parentId(PROFILE_BROWSER_LIKE)
+ .build());
+ BUILT_IN_PROFILE_IDS.addAll(BUILT_IN_PROFILES.keySet());
+ Collections.addAll(PROFILE_FIELDS, "label", "extends", "enableHttp3", "enableHttp2",
+ "enableHttp1", "alpnEnabled", "fallbackEnabled", "protocolErrorFallbackEnabled",
+ "altSvcCacheEnabled", "http1OnlyCacheEnabled", "h2cCacheEnabled",
+ "http2PriorKnowledge", "http2PriorKnowledgeEnabled", "h2cUpgradeEnabled",
+ "http1Upgrade", "happyEyeballsDelayMs", "http3BrokenCooldownMs",
+ "http1OnlyCooldownMs", "h2cCacheTtlMs");
+ }
+
+ private HTTP2ClientProfiles() {
+ }
+
+ public static List availableProfiles() {
+ Map definitions = loadProfileDefinitions();
+ List profiles = new ArrayList<>();
+ for (String profileId : definitions.keySet()) {
+ HTTP2ClientProfile profile = resolveFromDefinitions(profileId, definitions);
+ if (profile != null) {
+ profiles.add(profile);
+ }
+ }
+ return profiles;
+ }
+
+ public static HTTP2ClientProfile resolve(String profileId) {
+ String selected = StringUtils.trimToNull(profileId);
+ if (selected == null) {
+ selected = BzmHttpPluginProperties.getPropDefault(PROFILE_PROPERTY, DEFAULT_PROFILE_ID);
+ }
+ selected = normalizeProfileId(selected);
+ Map definitions = loadProfileDefinitions();
+ HTTP2ClientProfile profile = resolveFromDefinitions(selected, definitions);
+ if (profile != null) {
+ return profile;
+ }
+ LOG.warn("Unknown or invalid HTTP profile '{}'; falling back to '{}'",
+ selected, DEFAULT_PROFILE_ID);
+ return resolveFromDefinitions(DEFAULT_PROFILE_ID, definitions);
+ }
+
+ public static String normalizeProfileId(String profileId) {
+ String selected = StringUtils.trimToEmpty(profileId).toLowerCase(Locale.ROOT);
+ return selected.isEmpty() ? DEFAULT_PROFILE_ID : selected;
+ }
+
+ public static boolean isSamplerCustomProfile(String profileId) {
+ return PROFILE_BROWSER_LIKE_CUSTOM.equals(normalizeProfileId(profileId));
+ }
+
+ private static HTTP2ClientProfile resolveFromDefinitions(
+ String profileId, Map definitions) {
+ ProfileDefinition definition =
+ resolveDefinition(profileId, definitions, new HashSet<>());
+ return definition != null ? definition.toProfile(profileId) : null;
+ }
+
+ private static ProfileDefinition resolveDefinition(String profileId,
+ Map definitions,
+ Set stack) {
+ String normalizedProfileId = normalizeProfileId(profileId);
+ if (!stack.add(normalizedProfileId)) {
+ LOG.warn("Circular HTTP profile inheritance detected at '{}'", normalizedProfileId);
+ return null;
+ }
+ ProfileDefinition definition = definitions.get(normalizedProfileId);
+ if (definition == null) {
+ return null;
+ }
+ ProfileDefinition resolved = definition;
+ if (!StringUtils.isBlank(definition.parentId)) {
+ ProfileDefinition parent = resolveDefinition(definition.parentId, definitions, stack);
+ if (parent == null) {
+ LOG.warn("HTTP profile '{}' extends unknown profile '{}'",
+ normalizedProfileId, definition.parentId);
+ return null;
+ }
+ resolved = parent.merge(definition);
+ }
+ stack.remove(normalizedProfileId);
+ return resolved;
+ }
+
+ private static Map loadProfileDefinitions() {
+ Map definitions = new LinkedHashMap<>(BUILT_IN_PROFILES);
+ Properties profileProperties = new Properties();
+ profileProperties.putAll(loadFileProfileProperties());
+ profileProperties.putAll(JMeterUtils.getJMeterProperties());
+ Map customBuilders = new HashMap<>();
+ for (String key : new TreeSet<>(profileProperties.stringPropertyNames())) {
+ if (!key.startsWith(PROFILE_PREFIX) || PROFILE_FILE_PROPERTY.equals(key)) {
+ continue;
+ }
+ ProfileProperty profileProperty = parseProfileProperty(key);
+ if (profileProperty == null) {
+ continue;
+ }
+ if (BUILT_IN_PROFILE_IDS.contains(profileProperty.profileId)) {
+ LOG.warn("Ignoring custom HTTP profile property for built-in profile '{}': {}",
+ profileProperty.profileId, key);
+ continue;
+ }
+ ProfileDefinition.Builder builder = customBuilders.computeIfAbsent(
+ profileProperty.profileId, ProfileDefinition::builder);
+ applyProfileProperty(builder, profileProperty.field, profileProperties.getProperty(key), key);
+ }
+ for (String profileId : new TreeSet<>(customBuilders.keySet())) {
+ ProfileDefinition definition = customBuilders.get(profileId).defaultParentIfMissing().build();
+ definitions.put(profileId, definition);
+ }
+ return definitions;
+ }
+
+ private static Properties loadFileProfileProperties() {
+ Properties properties = new Properties();
+ String file = BzmHttpPluginProperties.resolveRaw(PROFILE_FILE_PROPERTY);
+ if (StringUtils.isBlank(file)) {
+ return properties;
+ }
+ Path path = Paths.get(file.trim());
+ try (InputStream input = Files.newInputStream(path)) {
+ properties.load(input);
+ } catch (IOException e) {
+ LOG.warn("Could not load HTTP profile definitions from '{}'", path, e);
+ }
+ return properties;
+ }
+
+ private static ProfileProperty parseProfileProperty(String key) {
+ String suffix = key.substring(PROFILE_PREFIX.length());
+ int separator = suffix.indexOf('.');
+ if (separator <= 0 || separator >= suffix.length() - 1) {
+ LOG.warn("Ignoring malformed HTTP profile property '{}'", key);
+ return null;
+ }
+ String profileId = normalizeProfileId(suffix.substring(0, separator));
+ if (!PROFILE_ID_PATTERN.matcher(profileId).matches()) {
+ LOG.warn("Ignoring HTTP profile property with invalid profile id '{}': {}",
+ profileId, key);
+ return null;
+ }
+ String field = suffix.substring(separator + 1);
+ if (!PROFILE_FIELDS.contains(field)) {
+ LOG.warn("Ignoring HTTP profile property with unknown field '{}': {}", field, key);
+ return null;
+ }
+ return new ProfileProperty(profileId, field);
+ }
+
+ private static void applyProfileProperty(ProfileDefinition.Builder builder, String field,
+ String value, String key) {
+ if ("label".equals(field)) {
+ builder.label(StringUtils.trimToNull(value));
+ } else if ("extends".equals(field)) {
+ builder.parentId(normalizeProfileId(value));
+ } else if ("enableHttp3".equals(field)) {
+ builder.enableHttp3(parseBoolean(value, key));
+ } else if ("enableHttp2".equals(field)) {
+ builder.enableHttp2(parseBoolean(value, key));
+ } else if ("enableHttp1".equals(field)) {
+ builder.enableHttp1(parseBoolean(value, key));
+ } else if ("alpnEnabled".equals(field)) {
+ builder.alpnEnabled(parseBoolean(value, key));
+ } else if ("fallbackEnabled".equals(field)) {
+ builder.fallbackEnabled(parseBoolean(value, key));
+ } else if ("protocolErrorFallbackEnabled".equals(field)) {
+ builder.protocolErrorFallbackEnabled(parseBoolean(value, key));
+ } else if ("altSvcCacheEnabled".equals(field)) {
+ builder.altSvcCacheEnabled(parseBoolean(value, key));
+ } else if ("http1OnlyCacheEnabled".equals(field)) {
+ builder.http1OnlyCacheEnabled(parseBoolean(value, key));
+ } else if ("h2cCacheEnabled".equals(field)) {
+ builder.h2cCacheEnabled(parseBoolean(value, key));
+ } else if ("http2PriorKnowledge".equals(field)
+ || "http2PriorKnowledgeEnabled".equals(field)) {
+ builder.http2PriorKnowledgeEnabled(parseBoolean(value, key));
+ } else if ("h2cUpgradeEnabled".equals(field) || "http1Upgrade".equals(field)) {
+ builder.h2cUpgradeEnabled(parseBoolean(value, key));
+ } else if ("happyEyeballsDelayMs".equals(field)) {
+ builder.happyEyeballsDelayMs(parseLong(value, key));
+ } else if ("http3BrokenCooldownMs".equals(field)) {
+ builder.http3BrokenCooldownMs(parseLong(value, key));
+ } else if ("http1OnlyCooldownMs".equals(field)) {
+ builder.http1OnlyCooldownMs(parseLong(value, key));
+ } else if ("h2cCacheTtlMs".equals(field)) {
+ builder.h2cCacheTtlMs(parseLong(value, key));
+ }
+ }
+
+ private static Boolean parseBoolean(String value, String key) {
+ String normalized = StringUtils.trimToEmpty(value).toLowerCase(Locale.ROOT);
+ if ("true".equals(normalized)) {
+ return Boolean.TRUE;
+ }
+ if ("false".equals(normalized)) {
+ return Boolean.FALSE;
+ }
+ LOG.warn("Ignoring HTTP profile boolean property '{}' with invalid value '{}'", key, value);
+ return null;
+ }
+
+ private static Long parseLong(String value, String key) {
+ String trimmed = StringUtils.trimToEmpty(value);
+ try {
+ long parsed = Long.parseLong(trimmed);
+ if (parsed < 0) {
+ LOG.warn("Ignoring HTTP profile timing property '{}' with negative value '{}'",
+ key, value);
+ return null;
+ }
+ return parsed;
+ } catch (NumberFormatException e) {
+ LOG.warn("Ignoring HTTP profile timing property '{}' with invalid value '{}'", key, value);
+ return null;
+ }
+ }
+
+ private static final class ProfileProperty {
+ private final String profileId;
+ private final String field;
+
+ private ProfileProperty(String profileId, String field) {
+ this.profileId = profileId;
+ this.field = field;
+ }
+ }
+
+ private static final class ProfileDefinition {
+ private final String id;
+ private final String label;
+ private final String parentId;
+ private final Boolean enableHttp3;
+ private final Boolean enableHttp2;
+ private final Boolean enableHttp1;
+ private final Boolean alpnEnabled;
+ private final Boolean fallbackEnabled;
+ private final Boolean protocolErrorFallbackEnabled;
+ private final Boolean altSvcCacheEnabled;
+ private final Boolean http1OnlyCacheEnabled;
+ private final Boolean h2cCacheEnabled;
+ private final Boolean http2PriorKnowledgeEnabled;
+ private final Boolean h2cUpgradeEnabled;
+ private final Long happyEyeballsDelayMs;
+ private final Long http3BrokenCooldownMs;
+ private final Long http1OnlyCooldownMs;
+ private final Long h2cCacheTtlMs;
+
+ private ProfileDefinition(Builder builder) {
+ id = builder.id;
+ label = builder.label;
+ parentId = builder.parentId;
+ enableHttp3 = builder.enableHttp3;
+ enableHttp2 = builder.enableHttp2;
+ enableHttp1 = builder.enableHttp1;
+ alpnEnabled = builder.alpnEnabled;
+ fallbackEnabled = builder.fallbackEnabled;
+ protocolErrorFallbackEnabled = builder.protocolErrorFallbackEnabled;
+ altSvcCacheEnabled = builder.altSvcCacheEnabled;
+ http1OnlyCacheEnabled = builder.http1OnlyCacheEnabled;
+ h2cCacheEnabled = builder.h2cCacheEnabled;
+ http2PriorKnowledgeEnabled = builder.http2PriorKnowledgeEnabled;
+ h2cUpgradeEnabled = builder.h2cUpgradeEnabled;
+ happyEyeballsDelayMs = builder.happyEyeballsDelayMs;
+ http3BrokenCooldownMs = builder.http3BrokenCooldownMs;
+ http1OnlyCooldownMs = builder.http1OnlyCooldownMs;
+ h2cCacheTtlMs = builder.h2cCacheTtlMs;
+ }
+
+ private static Builder builder(String id) {
+ return new Builder(id);
+ }
+
+ private ProfileDefinition merge(ProfileDefinition child) {
+ return builder(child.id)
+ .label(StringUtils.defaultIfBlank(child.label, child.id))
+ .enableHttp3(value(child.enableHttp3, enableHttp3))
+ .enableHttp2(value(child.enableHttp2, enableHttp2))
+ .enableHttp1(value(child.enableHttp1, enableHttp1))
+ .alpnEnabled(value(child.alpnEnabled, alpnEnabled))
+ .fallbackEnabled(value(child.fallbackEnabled, fallbackEnabled))
+ .protocolErrorFallbackEnabled(value(child.protocolErrorFallbackEnabled,
+ protocolErrorFallbackEnabled))
+ .altSvcCacheEnabled(value(child.altSvcCacheEnabled, altSvcCacheEnabled))
+ .http1OnlyCacheEnabled(value(child.http1OnlyCacheEnabled, http1OnlyCacheEnabled))
+ .h2cCacheEnabled(value(child.h2cCacheEnabled, h2cCacheEnabled))
+ .http2PriorKnowledgeEnabled(value(child.http2PriorKnowledgeEnabled,
+ http2PriorKnowledgeEnabled))
+ .h2cUpgradeEnabled(value(child.h2cUpgradeEnabled, h2cUpgradeEnabled))
+ .happyEyeballsDelayMs(value(child.happyEyeballsDelayMs, happyEyeballsDelayMs))
+ .http3BrokenCooldownMs(value(child.http3BrokenCooldownMs, http3BrokenCooldownMs))
+ .http1OnlyCooldownMs(value(child.http1OnlyCooldownMs, http1OnlyCooldownMs))
+ .h2cCacheTtlMs(value(child.h2cCacheTtlMs, h2cCacheTtlMs))
+ .build();
+ }
+
+ private HTTP2ClientProfile toProfile(String profileId) {
+ return HTTP2ClientProfile.builder(profileId, StringUtils.defaultIfBlank(label, profileId))
+ .enableHttp3(Boolean.TRUE.equals(enableHttp3))
+ .enableHttp2(Boolean.TRUE.equals(enableHttp2))
+ .enableHttp1(Boolean.TRUE.equals(enableHttp1))
+ .alpnEnabled(Boolean.TRUE.equals(alpnEnabled))
+ .fallbackEnabled(Boolean.TRUE.equals(fallbackEnabled))
+ .protocolErrorFallbackEnabled(Boolean.TRUE.equals(protocolErrorFallbackEnabled))
+ .altSvcCacheEnabled(Boolean.TRUE.equals(altSvcCacheEnabled))
+ .http1OnlyCacheEnabled(Boolean.TRUE.equals(http1OnlyCacheEnabled))
+ .h2cCacheEnabled(Boolean.TRUE.equals(h2cCacheEnabled))
+ .http2PriorKnowledgeEnabled(Boolean.TRUE.equals(http2PriorKnowledgeEnabled))
+ .h2cUpgradeEnabled(Boolean.TRUE.equals(h2cUpgradeEnabled))
+ .happyEyeballsDelayMs(value(happyEyeballsDelayMs, 0L))
+ .http3BrokenCooldownMs(value(http3BrokenCooldownMs, 0L))
+ .http1OnlyCooldownMs(value(http1OnlyCooldownMs, 0L))
+ .h2cCacheTtlMs(value(h2cCacheTtlMs, 0L))
+ .build();
+ }
+
+ private static Boolean value(Boolean primary, Boolean fallback) {
+ return primary != null ? primary : fallback;
+ }
+
+ private static Long value(Long primary, Long fallback) {
+ return primary != null ? primary : fallback;
+ }
+
+ private static final class Builder {
+ private final String id;
+ private String label;
+ private String parentId;
+ private Boolean enableHttp3;
+ private Boolean enableHttp2;
+ private Boolean enableHttp1;
+ private Boolean alpnEnabled;
+ private Boolean fallbackEnabled;
+ private Boolean protocolErrorFallbackEnabled;
+ private Boolean altSvcCacheEnabled;
+ private Boolean http1OnlyCacheEnabled;
+ private Boolean h2cCacheEnabled;
+ private Boolean http2PriorKnowledgeEnabled;
+ private Boolean h2cUpgradeEnabled;
+ private Long happyEyeballsDelayMs;
+ private Long http3BrokenCooldownMs;
+ private Long http1OnlyCooldownMs;
+ private Long h2cCacheTtlMs;
+
+ private Builder(String id) {
+ this.id = id;
+ }
+
+ private Builder label(String label) {
+ this.label = label;
+ return this;
+ }
+
+ private Builder parentId(String parentId) {
+ this.parentId = parentId;
+ return this;
+ }
+
+ private Builder enableHttp3(Boolean enableHttp3) {
+ if (enableHttp3 != null) {
+ this.enableHttp3 = enableHttp3;
+ }
+ return this;
+ }
+
+ private Builder enableHttp2(Boolean enableHttp2) {
+ if (enableHttp2 != null) {
+ this.enableHttp2 = enableHttp2;
+ }
+ return this;
+ }
+
+ private Builder enableHttp1(Boolean enableHttp1) {
+ if (enableHttp1 != null) {
+ this.enableHttp1 = enableHttp1;
+ }
+ return this;
+ }
+
+ private Builder alpnEnabled(Boolean alpnEnabled) {
+ if (alpnEnabled != null) {
+ this.alpnEnabled = alpnEnabled;
+ }
+ return this;
+ }
+
+ private Builder fallbackEnabled(Boolean fallbackEnabled) {
+ if (fallbackEnabled != null) {
+ this.fallbackEnabled = fallbackEnabled;
+ }
+ return this;
+ }
+
+ private Builder protocolErrorFallbackEnabled(Boolean protocolErrorFallbackEnabled) {
+ if (protocolErrorFallbackEnabled != null) {
+ this.protocolErrorFallbackEnabled = protocolErrorFallbackEnabled;
+ }
+ return this;
+ }
+
+ private Builder altSvcCacheEnabled(Boolean altSvcCacheEnabled) {
+ if (altSvcCacheEnabled != null) {
+ this.altSvcCacheEnabled = altSvcCacheEnabled;
+ }
+ return this;
+ }
+
+ private Builder http1OnlyCacheEnabled(Boolean http1OnlyCacheEnabled) {
+ if (http1OnlyCacheEnabled != null) {
+ this.http1OnlyCacheEnabled = http1OnlyCacheEnabled;
+ }
+ return this;
+ }
+
+ private Builder h2cCacheEnabled(Boolean h2cCacheEnabled) {
+ if (h2cCacheEnabled != null) {
+ this.h2cCacheEnabled = h2cCacheEnabled;
+ }
+ return this;
+ }
+
+ private Builder http2PriorKnowledgeEnabled(Boolean http2PriorKnowledgeEnabled) {
+ if (http2PriorKnowledgeEnabled != null) {
+ this.http2PriorKnowledgeEnabled = http2PriorKnowledgeEnabled;
+ }
+ return this;
+ }
+
+ private Builder h2cUpgradeEnabled(Boolean h2cUpgradeEnabled) {
+ if (h2cUpgradeEnabled != null) {
+ this.h2cUpgradeEnabled = h2cUpgradeEnabled;
+ }
+ return this;
+ }
+
+ private Builder happyEyeballsDelayMs(Long happyEyeballsDelayMs) {
+ if (happyEyeballsDelayMs != null) {
+ this.happyEyeballsDelayMs = happyEyeballsDelayMs;
+ }
+ return this;
+ }
+
+ private Builder http3BrokenCooldownMs(Long http3BrokenCooldownMs) {
+ if (http3BrokenCooldownMs != null) {
+ this.http3BrokenCooldownMs = http3BrokenCooldownMs;
+ }
+ return this;
+ }
+
+ private Builder http1OnlyCooldownMs(Long http1OnlyCooldownMs) {
+ if (http1OnlyCooldownMs != null) {
+ this.http1OnlyCooldownMs = http1OnlyCooldownMs;
+ }
+ return this;
+ }
+
+ private Builder h2cCacheTtlMs(Long h2cCacheTtlMs) {
+ if (h2cCacheTtlMs != null) {
+ this.h2cCacheTtlMs = h2cCacheTtlMs;
+ }
+ return this;
+ }
+
+ private Builder defaultParentIfMissing() {
+ if (StringUtils.isBlank(parentId)) {
+ parentId = DEFAULT_PROFILE_ID;
+ }
+ return this;
+ }
+
+ private ProfileDefinition build() {
+ return new ProfileDefinition(this);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java b/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java
index 2648ffc..84aa771 100644
--- a/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java
+++ b/src/main/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClient.java
@@ -150,16 +150,7 @@ public class HTTP2JettyClient {
private static final String PROP_SKIP_REDUNDANT_MANUAL_DECODE =
"blazemeter.http.skipManualDecodeWhenAdvertised";
private static final Path DEBUG_LOG_PATH = resolveDebugLogPath();
- private static final String PROFILE_PROPERTY = "httpJettyClient.profile";
- private static final String PROFILE_BROWSER_LIKE = "browser-like";
- private static final String PROFILE_BROWSER_LIKE_CUSTOM = "browser-like-custom";
- private static final String PROFILE_BROWSER_COMPATIBLE = "browser-compatible";
- private static final String PROFILE_LEGACY = "legacy";
private static final long ALT_SVC_DEFAULT_MAX_AGE_SECONDS = 86400;
- private static final long DEFAULT_HTTP3_BROKEN_COOLDOWN_MS = 300000;
- private static final long DEFAULT_HTTP1_ONLY_COOLDOWN_MS = 300000;
- private static final long DEFAULT_H2C_CACHE_TTL_MS = 300000;
- private static final long DEFAULT_HAPPY_EYEBALLS_DELAY_MS = 250;
private static final long H3_RECENT_SUCCESS_WINDOW_MS =
TimeUnit.MINUTES.toMillis(5);
private static final Object SHARED_POOL_LOCK = new Object();
@@ -218,10 +209,10 @@ public class HTTP2JettyClient {
private int quicMaxIdleTimeout = 30000;
private int quicMaxBidirectionalStreams = 100;
private int quicMaxUnidirectionalStreams = 100;
- private long http3BrokenCooldownMs = DEFAULT_HTTP3_BROKEN_COOLDOWN_MS;
- private long http1OnlyCooldownMs = DEFAULT_HTTP1_ONLY_COOLDOWN_MS;
- private long h2cCacheTtlMs = DEFAULT_H2C_CACHE_TTL_MS;
- private long happyEyeballsDelayMs = DEFAULT_HAPPY_EYEBALLS_DELAY_MS;
+ private long http3BrokenCooldownMs = HTTP2ClientProfiles.DEFAULT_HTTP3_BROKEN_COOLDOWN_MS;
+ private long http1OnlyCooldownMs = HTTP2ClientProfiles.DEFAULT_HTTP1_ONLY_COOLDOWN_MS;
+ private long h2cCacheTtlMs = HTTP2ClientProfiles.DEFAULT_H2C_CACHE_TTL_MS;
+ private long happyEyeballsDelayMs = HTTP2ClientProfiles.DEFAULT_HAPPY_EYEBALLS_DELAY_MS;
private boolean http2PriorKnowledgeEnabled = false;
private boolean http3PriorKnowledgeEnabled = false;
private boolean enableHttp3 = true;
@@ -235,6 +226,7 @@ public class HTTP2JettyClient {
private boolean altSvcCacheEnabled = true;
private boolean http1OnlyCacheEnabled = true;
private boolean h2cCacheEnabled = true;
+ private boolean h2cUpgradeEnabled = false;
private boolean heExecutorsRegistered = false;
public HTTP2JettyClient(boolean http1UpgradeRequired, String name) {
@@ -534,7 +526,7 @@ public HTTP2JettyClient(boolean http1UpgradeRequired, String name,
configureTransport(h2cTransport);
this.httpClientH2cPrior = new HttpClient(h2cTransport);
configureHttpClient(this.httpClientH2cPrior, h2cConnector);
- this.http1UpgradeRequired = http1UpgradeRequired;
+ this.http1UpgradeRequired = http1UpgradeRequired || h2cUpgradeEnabled;
this.httpClient.setName(name);
if (httpClientNoH3 != httpClient) {
this.httpClientNoH3.setName(name + "-noh3");
@@ -705,7 +697,7 @@ public void loadProperties() {
}
public void loadProperties(HTTP2ClientProfileConfig profileConfig) {
- ProfileDefaults defaults = resolveProfileDefaults(profileConfig);
+ HTTP2ClientProfile defaults = resolveProfileDefaults(profileConfig);
requestTimeout = JMeterUtils.getPropDefault("HTTPSampler.response_timeout", 0);
byteBufferPoolFactor =
Integer.parseInt(BzmHttpPluginProperties.getPropDefault(
@@ -766,30 +758,30 @@ public void loadProperties(HTTP2ClientProfileConfig profileConfig) {
String.valueOf(quicMaxUnidirectionalStreams)));
enableHttp3 = getBooleanProp("httpJettyClient.enableHttp3",
profileConfig != null ? profileConfig.getEnableHttp3() : null,
- defaults.enableHttp3);
+ defaults.isEnableHttp3());
enableHttp2 = getBooleanProp("httpJettyClient.enableHttp2",
profileConfig != null ? profileConfig.getEnableHttp2() : null,
- defaults.enableHttp2);
+ defaults.isEnableHttp2());
enableHttp1 = getBooleanProp("httpJettyClient.enableHttp1",
profileConfig != null ? profileConfig.getEnableHttp1() : null,
- defaults.enableHttp1);
+ defaults.isEnableHttp1());
alpnEnabled = getBooleanProp("httpJettyClient.alpnEnabled",
profileConfig != null ? profileConfig.getAlpnEnabled() : null,
- defaults.alpnEnabled);
+ defaults.isAlpnEnabled());
fallbackEnabled = getBooleanProp("httpJettyClient.fallbackEnabled",
profileConfig != null ? profileConfig.getFallbackEnabled() : null,
- defaults.fallbackEnabled);
+ defaults.isFallbackEnabled());
altSvcCacheEnabled =
getBooleanProp("httpJettyClient.altSvcCacheEnabled",
profileConfig != null ? profileConfig.getAltSvcCacheEnabled() : null,
- defaults.altSvcCacheEnabled);
+ defaults.isAltSvcCacheEnabled());
http1OnlyCacheEnabled =
getBooleanProp("httpJettyClient.http1OnlyCacheEnabled",
profileConfig != null ? profileConfig.getHttp1OnlyCacheEnabled() : null,
- defaults.http1OnlyCacheEnabled);
+ defaults.isHttp1OnlyCacheEnabled());
h2cCacheEnabled = getBooleanProp("httpJettyClient.h2cCacheEnabled",
profileConfig != null ? profileConfig.getH2cCacheEnabled() : null,
- defaults.h2cCacheEnabled);
+ defaults.isH2cCacheEnabled());
protocolErrorFallbackEnabled = resolveProtocolErrorFallback(defaults, profileConfig);
goawayRetryEnabled =
Boolean.parseBoolean(BzmHttpPluginProperties.getPropDefault(
@@ -802,21 +794,24 @@ public void loadProperties(HTTP2ClientProfileConfig profileConfig) {
http3BrokenCooldownMs = getLongProp("httpJettyClient.http3BrokenCooldownMs",
profileConfig != null ? profileConfig.getHttp3BrokenCooldownMs() : null,
- defaults.http3BrokenCooldownMs);
+ defaults.getHttp3BrokenCooldownMs());
http1OnlyCooldownMs = getLongProp("httpJettyClient.http1OnlyCooldownMs",
profileConfig != null ? profileConfig.getHttp1OnlyCooldownMs() : null,
- defaults.http1OnlyCooldownMs);
+ defaults.getHttp1OnlyCooldownMs());
h2cCacheTtlMs = getLongProp("httpJettyClient.h2cCacheTtlMs",
profileConfig != null ? profileConfig.getH2cCacheTtlMs() : null,
- defaults.h2cCacheTtlMs);
+ defaults.getH2cCacheTtlMs());
http2PriorKnowledgeEnabled = getBooleanProp("httpJettyClient.http2PriorKnowledge",
profileConfig != null ? profileConfig.getHttp2PriorKnowledgeEnabled() : null,
- defaults.http2PriorKnowledgeEnabled);
+ defaults.isHttp2PriorKnowledgeEnabled());
+ h2cUpgradeEnabled = getBooleanProp("httpJettyClient.h2cUpgradeEnabled",
+ profileConfig != null ? profileConfig.getH2cUpgradeEnabled() : null,
+ defaults.isH2cUpgradeEnabled());
http3PriorKnowledgeEnabled = Boolean.parseBoolean(BzmHttpPluginProperties.getPropDefault(
"httpJettyClient.http3PriorKnowledge", "false"));
happyEyeballsDelayMs = getLongProp("httpJettyClient.happyEyeballsDelayMs",
profileConfig != null ? profileConfig.getHappyEyeballsDelayMs() : null,
- defaults.happyEyeballsDelayMs);
+ defaults.getHappyEyeballsDelayMs());
// Default reduced to 4096 (Issue #12071)
settingsMaxHeaderListSize =
Integer.parseInt(BzmHttpPluginProperties.getPropDefault(
@@ -869,7 +864,7 @@ private long getLongProp(String key, Long overrideValue, long defaultValue) {
return defaultValue;
}
- private boolean resolveProtocolErrorFallback(ProfileDefaults defaults,
+ private boolean resolveProtocolErrorFallback(HTTP2ClientProfile defaults,
HTTP2ClientProfileConfig profileConfig) {
if (profileConfig != null && profileConfig.getProtocolErrorFallbackEnabled() != null) {
return profileConfig.getProtocolErrorFallbackEnabled();
@@ -882,27 +877,12 @@ private boolean resolveProtocolErrorFallback(ProfileDefaults defaults,
if (df != null) {
return !Boolean.parseBoolean(df);
}
- return defaults.protocolErrorFallbackEnabled;
+ return defaults.isProtocolErrorFallbackEnabled();
}
- private ProfileDefaults resolveProfileDefaults(HTTP2ClientProfileConfig profileConfig) {
+ private HTTP2ClientProfile resolveProfileDefaults(HTTP2ClientProfileConfig profileConfig) {
String profile = profileConfig != null ? profileConfig.getProfile() : null;
- if (profile == null || profile.trim().isEmpty()) {
- String rawProfile =
- BzmHttpPluginProperties.getPropDefault(PROFILE_PROPERTY, PROFILE_BROWSER_LIKE).trim();
- profile = rawProfile.isEmpty() ? PROFILE_BROWSER_LIKE : rawProfile;
- }
- profile = profile.trim().toLowerCase(Locale.ROOT);
- switch (profile) {
- case PROFILE_BROWSER_COMPATIBLE:
- return ProfileDefaults.browserCompatible();
- case PROFILE_LEGACY:
- return ProfileDefaults.legacy();
- case PROFILE_BROWSER_LIKE_CUSTOM:
- case PROFILE_BROWSER_LIKE:
- default:
- return ProfileDefaults.browserLike();
- }
+ return HTTP2ClientProfiles.resolve(profile);
}
private ClientConnectionFactory.Info[] buildMainProtocols(
@@ -974,64 +954,6 @@ private String protocolList(ClientConnectionFactory.Info[] protocols) {
return out.toString();
}
- private static class ProfileDefaults {
- private final boolean enableHttp3;
- private final boolean enableHttp2;
- private final boolean enableHttp1;
- private final boolean alpnEnabled;
- private final boolean fallbackEnabled;
- private final boolean protocolErrorFallbackEnabled;
- private final boolean altSvcCacheEnabled;
- private final boolean http1OnlyCacheEnabled;
- private final boolean h2cCacheEnabled;
- private final boolean http2PriorKnowledgeEnabled;
- private final long http3BrokenCooldownMs;
- private final long http1OnlyCooldownMs;
- private final long h2cCacheTtlMs;
- private final long happyEyeballsDelayMs;
-
- private ProfileDefaults(boolean enableHttp3, boolean enableHttp2, boolean enableHttp1,
- boolean alpnEnabled, boolean fallbackEnabled,
- boolean protocolErrorFallbackEnabled,
- boolean altSvcCacheEnabled, boolean http1OnlyCacheEnabled,
- boolean h2cCacheEnabled,
- boolean http2PriorKnowledgeEnabled, long http3BrokenCooldownMs,
- long http1OnlyCooldownMs,
- long h2cCacheTtlMs, long happyEyeballsDelayMs) {
- this.enableHttp3 = enableHttp3;
- this.enableHttp2 = enableHttp2;
- this.enableHttp1 = enableHttp1;
- this.alpnEnabled = alpnEnabled;
- this.fallbackEnabled = fallbackEnabled;
- this.protocolErrorFallbackEnabled = protocolErrorFallbackEnabled;
- this.altSvcCacheEnabled = altSvcCacheEnabled;
- this.http1OnlyCacheEnabled = http1OnlyCacheEnabled;
- this.h2cCacheEnabled = h2cCacheEnabled;
- this.http2PriorKnowledgeEnabled = http2PriorKnowledgeEnabled;
- this.http3BrokenCooldownMs = http3BrokenCooldownMs;
- this.http1OnlyCooldownMs = http1OnlyCooldownMs;
- this.h2cCacheTtlMs = h2cCacheTtlMs;
- this.happyEyeballsDelayMs = happyEyeballsDelayMs;
- }
-
- private static ProfileDefaults browserLike() {
- return new ProfileDefaults(true, true, true, true, true, true, true, true, true, false,
- DEFAULT_HTTP3_BROKEN_COOLDOWN_MS, DEFAULT_HTTP1_ONLY_COOLDOWN_MS,
- DEFAULT_H2C_CACHE_TTL_MS, DEFAULT_HAPPY_EYEBALLS_DELAY_MS);
- }
-
- private static ProfileDefaults browserCompatible() {
- return new ProfileDefaults(true, true, true, true, true, true, true, true, true, false,
- DEFAULT_HTTP3_BROKEN_COOLDOWN_MS, DEFAULT_HTTP1_ONLY_COOLDOWN_MS,
- DEFAULT_H2C_CACHE_TTL_MS, 0L);
- }
-
- private static ProfileDefaults legacy() {
- return new ProfileDefaults(false, false, true, false, false, false, false, false, true,
- false, 0L, 0L, DEFAULT_H2C_CACHE_TTL_MS, 0L);
- }
- }
-
public void start() throws Exception {
if (!heExecutorsRegistered) {
ensureHappyEyeballsExecutors();
@@ -3870,4 +3792,3 @@ private void copy(InputStream input, ByteArrayOutputStream output) throws IOExce
}
}
}
-
diff --git a/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java b/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java
index 529c270..8f16e8f 100644
--- a/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java
+++ b/src/main/java/com/blazemeter/jmeter/http2/sampler/HTTP2Sampler.java
@@ -4,6 +4,7 @@
import com.blazemeter.jmeter.http2.control.HTTP2Controller;
import com.blazemeter.jmeter.http2.core.HTTP2ClientProfileConfig;
+import com.blazemeter.jmeter.http2.core.HTTP2ClientProfiles;
import com.blazemeter.jmeter.http2.core.HTTP2FutureResponseListener;
import com.blazemeter.jmeter.http2.core.HTTP2JettyClient;
import com.blazemeter.jmeter.http2.core.ProtocolErrorException;
@@ -205,8 +206,19 @@ public void setHttp1UpgradeEnabled(boolean http1UpgradeSelected) {
}
public boolean isHttp1UpgradeEnabled() {
- return getPropertyAsBoolean(HTTP1_UPGRADE_PROPERTY,
- BzmHttpPluginProperties.getPropDefault(H2C_UPGRADE_DEFAULT_PROPERTY, false));
+ Boolean samplerValue = getOptionalBoolean(HTTP1_UPGRADE_PROPERTY);
+ if (samplerValue != null) {
+ return samplerValue;
+ }
+ Boolean globalValue = getGlobalHttp1UpgradeDefault();
+ if (globalValue != null) {
+ return globalValue;
+ }
+ String storedProfile = getStoredProfile();
+ String globalProfile = getGlobalProfile();
+ String profile = storedProfile != null ? storedProfile : globalProfile;
+ profile = profile != null ? profile : HTTP2ClientProfiles.DEFAULT_PROFILE_ID;
+ return HTTP2ClientProfiles.resolve(profile).isH2cUpgradeEnabled();
}
@Override
@@ -219,9 +231,13 @@ public void setProfile(String profile) {
}
public String getProfile() {
- JMeterProperty property = getProperty(PROFILE_PROPERTY);
- if (property == null || property instanceof NullProperty) {
- boolean http1UpgradeEnabled = isHttp1UpgradeEnabled();
+ String storedProfile = getStoredProfile();
+ if (storedProfile == null) {
+ String globalProfile = getGlobalProfile();
+ if (globalProfile != null) {
+ return globalProfile;
+ }
+ boolean http1UpgradeEnabled = Boolean.TRUE.equals(getGlobalHttp1UpgradeDefault());
if (http1UpgradeEnabled && !profileInferenceWarningLogged) {
LOG.warn(
"Profile not set; inferring 'legacy' because HTTP/1 upgrade is enabled. "
@@ -229,10 +245,35 @@ public String getProfile() {
+ "ambiguity.");
profileInferenceWarningLogged = true;
}
- return http1UpgradeEnabled ? "legacy" : "browser-like";
+ return http1UpgradeEnabled ? HTTP2ClientProfiles.PROFILE_LEGACY
+ : HTTP2ClientProfiles.DEFAULT_PROFILE_ID;
+ }
+ return HTTP2ClientProfiles.normalizeProfileId(storedProfile);
+ }
+
+ private String getStoredProfile() {
+ JMeterProperty property = getProperty(PROFILE_PROPERTY);
+ if (property == null || property instanceof NullProperty) {
+ return null;
}
String value = property.getStringValue();
- return value == null || value.trim().isEmpty() ? "browser-like" : value.trim();
+ return StringUtils.trimToNull(value);
+ }
+
+ private String getGlobalProfile() {
+ String raw = BzmHttpPluginProperties.resolveRaw("httpJettyClient.profile");
+ if (StringUtils.isBlank(raw)) {
+ return null;
+ }
+ return HTTP2ClientProfiles.normalizeProfileId(raw);
+ }
+
+ private Boolean getGlobalHttp1UpgradeDefault() {
+ String raw = BzmHttpPluginProperties.resolveRaw(H2C_UPGRADE_DEFAULT_PROPERTY);
+ if (StringUtils.isBlank(raw)) {
+ return null;
+ }
+ return Boolean.parseBoolean(raw.trim());
}
public void setUiTabIndex(int index) {
@@ -683,6 +724,7 @@ private HTTP2ClientProfileConfig buildProfileConfig() {
.http1OnlyCacheEnabled(getHttp1OnlyCacheEnabled())
.h2cCacheEnabled(getH2cCacheEnabled())
.http2PriorKnowledgeEnabled(getHttp2PriorKnowledgeEnabled())
+ .h2cUpgradeEnabled(isHttp1UpgradeEnabled())
.happyEyeballsDelayMs(getHappyEyeballsDelayMs())
.http3BrokenCooldownMs(getHttp3BrokenCooldownMs())
.http1OnlyCooldownMs(getHttp1OnlyCooldownMs())
diff --git a/src/main/java/com/blazemeter/jmeter/http2/sampler/gui/HTTP2SamplerPanel.java b/src/main/java/com/blazemeter/jmeter/http2/sampler/gui/HTTP2SamplerPanel.java
index 665b9f9..bc57ebf 100644
--- a/src/main/java/com/blazemeter/jmeter/http2/sampler/gui/HTTP2SamplerPanel.java
+++ b/src/main/java/com/blazemeter/jmeter/http2/sampler/gui/HTTP2SamplerPanel.java
@@ -1,5 +1,7 @@
package com.blazemeter.jmeter.http2.sampler.gui;
+import com.blazemeter.jmeter.http2.core.HTTP2ClientProfile;
+import com.blazemeter.jmeter.http2.core.HTTP2ClientProfiles;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
@@ -50,26 +52,6 @@ public class HTTP2SamplerPanel extends JPanel {
private static final String PROFILE_COMBO_PROTOTYPE =
"Browser-compatible (Other Browsers)";
- private static final String PROFILE_BROWSER_LIKE = "browser-like";
- private static final String PROFILE_BROWSER_LIKE_CUSTOM = "browser-like-custom";
- private static final String PROFILE_BROWSER_COMPATIBLE = "browser-compatible";
- private static final String PROFILE_LEGACY = "legacy";
- private static final long DEFAULT_HTTP3_BROKEN_COOLDOWN_MS = 300000;
- private static final long DEFAULT_HTTP1_ONLY_COOLDOWN_MS = 300000;
- private static final long DEFAULT_H2C_CACHE_TTL_MS = 300000;
- private static final long DEFAULT_HAPPY_EYEBALLS_DELAY_MS = 250;
- private static final String[] PROFILE_LABELS = new String[] {
- "Browser-like (Most Browsers)",
- "Browser-compatible (Other Browsers)",
- "Legacy / Older Systems",
- "Browser-like (Custom)"
- };
- private static final String[] PROFILE_VALUES = new String[] {
- PROFILE_BROWSER_LIKE,
- PROFILE_BROWSER_COMPATIBLE,
- PROFILE_LEGACY,
- PROFILE_BROWSER_LIKE_CUSTOM
- };
private static final int URL_CONFIG_TABLE_VIEWPORT_MIN_ROWS = 3;
private static final int URL_CONFIG_TABLE_VIEWPORT_MAX_ROWS = 6;
@@ -93,7 +75,7 @@ public class HTTP2SamplerPanel extends JPanel {
private final JLabeledTextField embeddedResourcesRegexField = new JLabeledTextField(
JMeterUtils.getResString("web_testing_embedded_url_pattern"), 20);
private final JCheckBox http1Upgrade = new JCheckBox("H2C Upgrade (HTTP/1.1 Upgrade)");
- private final JComboBox profileSelector = new JComboBox<>(PROFILE_LABELS);
+ private final JComboBox profileSelector = new JComboBox<>();
private final JCheckBox enableHttp3Check = new JCheckBox("Enable HTTP/3 (Alt-Svc + QUIC)");
private final JCheckBox enableHttp2Check = new JCheckBox("Enable HTTP/2");
private final JCheckBox enableHttp1Check = new JCheckBox("Enable HTTP/1.1");
@@ -192,6 +174,7 @@ private JPanel createClientBehaviorPanel() {
JPanel behaviorPanel = new VerticalPanel();
behaviorPanel.setBorder(BorderFactory.createTitledBorder("Client Behavior"));
+ reloadProfileChoices(HTTP2ClientProfiles.DEFAULT_PROFILE_ID);
JPanel profilePanel = new JPanel(new BorderLayout(5, 0));
profilePanel.add(new JLabel("Profile"), BorderLayout.WEST);
profilePanel.add(profileSelector, BorderLayout.CENTER);
@@ -228,7 +211,9 @@ private JPanel createClientBehaviorPanel() {
timingPanel.add(h2cCacheTtlField);
behaviorPanel.add(timingPanel);
- profileSelector.setPrototypeDisplayValue(PROFILE_COMBO_PROTOTYPE);
+ profileSelector.setPrototypeDisplayValue(
+ new ProfileChoice(HTTP2ClientProfiles.PROFILE_BROWSER_COMPATIBLE,
+ PROFILE_COMBO_PROTOTYPE));
relaxHorizontalMinimumWidth(profileSelector);
profileSelector.addActionListener(e -> updateProfileControls());
enableHttp1Check.addActionListener(e -> handleProtocolToggle(enableHttp1Check));
@@ -451,7 +436,7 @@ private void applyRelatedDefaults(JCheckBox checkbox) {
}
private void updateConditionalVisibility() {
- boolean customProfile = PROFILE_BROWSER_LIKE_CUSTOM.equals(getProfile());
+ boolean customProfile = HTTP2ClientProfiles.isSamplerCustomProfile(getProfile());
boolean http1Enabled = enableHttp1Check.isSelected();
boolean http2Enabled = enableHttp2Check.isSelected();
boolean http3Enabled = enableHttp3Check.isSelected();
@@ -500,27 +485,16 @@ private void setH2cVisibility(boolean upgradeVisible, boolean cacheVisible) {
}
private void applyProfileDefaults(String profile) {
- if (PROFILE_BROWSER_COMPATIBLE.equals(profile)) {
- setProtocolDefaults(true, true, true, true);
- setFallbackDefaults(true, true);
- setCacheDefaults(true, true, true, false);
- http1Upgrade.setSelected(false);
- setTimingDefaults(0L, DEFAULT_HTTP3_BROKEN_COOLDOWN_MS, DEFAULT_HTTP1_ONLY_COOLDOWN_MS,
- DEFAULT_H2C_CACHE_TTL_MS);
- } else if (PROFILE_LEGACY.equals(profile)) {
- setProtocolDefaults(false, false, true, false);
- setFallbackDefaults(false, false);
- setCacheDefaults(false, false, true, false);
- http1Upgrade.setSelected(true);
- setTimingDefaults(0L, 0L, 0L, DEFAULT_H2C_CACHE_TTL_MS);
- } else {
- setProtocolDefaults(true, true, true, true);
- setFallbackDefaults(true, true);
- setCacheDefaults(true, true, true, false);
- http1Upgrade.setSelected(false);
- setTimingDefaults(DEFAULT_HAPPY_EYEBALLS_DELAY_MS, DEFAULT_HTTP3_BROKEN_COOLDOWN_MS,
- DEFAULT_HTTP1_ONLY_COOLDOWN_MS, DEFAULT_H2C_CACHE_TTL_MS);
- }
+ HTTP2ClientProfile defaults = HTTP2ClientProfiles.resolve(profile);
+ setProtocolDefaults(defaults.isEnableHttp3(), defaults.isEnableHttp2(),
+ defaults.isEnableHttp1(), defaults.isAlpnEnabled());
+ setFallbackDefaults(defaults.isFallbackEnabled(),
+ defaults.isProtocolErrorFallbackEnabled());
+ setCacheDefaults(defaults.isAltSvcCacheEnabled(), defaults.isHttp1OnlyCacheEnabled(),
+ defaults.isH2cCacheEnabled(), defaults.isHttp2PriorKnowledgeEnabled());
+ http1Upgrade.setSelected(defaults.isH2cUpgradeEnabled());
+ setTimingDefaults(defaults.getHappyEyeballsDelayMs(), defaults.getHttp3BrokenCooldownMs(),
+ defaults.getHttp1OnlyCooldownMs(), defaults.getH2cCacheTtlMs());
}
private void setProtocolDefaults(boolean http3, boolean http2, boolean http1, boolean alpn) {
@@ -555,7 +529,7 @@ public void resetFields() {
urlConfigGui.clear();
refreshUrlConfigLayoutAfterDataChange();
http1Upgrade.setSelected(false);
- setProfile(PROFILE_BROWSER_LIKE);
+ setProfile(HTTP2ClientProfiles.DEFAULT_PROFILE_ID);
setSelectedTabIndex(0);
retrieveEmbeddedResourcesCheckBox.setSelected(false);
concurrentDownloadCheckBox.setSelected(false);
@@ -599,22 +573,15 @@ public void setHttp1UpgradeSelected(boolean enabled) {
}
public String getProfile() {
- int index = profileSelector.getSelectedIndex();
- if (index >= 0 && index < PROFILE_VALUES.length) {
- return PROFILE_VALUES[index];
+ ProfileChoice selected = (ProfileChoice) profileSelector.getSelectedItem();
+ if (selected != null) {
+ return selected.id;
}
- return PROFILE_BROWSER_LIKE;
+ return HTTP2ClientProfiles.DEFAULT_PROFILE_ID;
}
public void setProfile(String profile) {
- int index = 0;
- for (int i = 0; i < PROFILE_VALUES.length; i++) {
- if (PROFILE_VALUES[i].equals(profile)) {
- index = i;
- break;
- }
- }
- profileSelector.setSelectedIndex(index);
+ reloadProfileChoices(HTTP2ClientProfiles.normalizeProfileId(profile));
updateProfileControls();
}
@@ -856,4 +823,45 @@ public void setConcurrentPool(String concurrentPool) {
public void setEmbeddedResourcesRegex(String embeddedResourcesRegex) {
this.embeddedResourcesRegexField.setText(embeddedResourcesRegex);
}
+
+ private void reloadProfileChoices(String selectedProfile) {
+ String selected = HTTP2ClientProfiles.normalizeProfileId(selectedProfile);
+ profileSelector.removeAllItems();
+ int selectedIndex = 0;
+ int index = 0;
+ boolean found = false;
+ for (HTTP2ClientProfile profile : HTTP2ClientProfiles.availableProfiles()) {
+ ProfileChoice choice = new ProfileChoice(profile.getId(), profile.getLabel());
+ profileSelector.addItem(choice);
+ if (choice.id.equals(selected)) {
+ selectedIndex = index;
+ found = true;
+ }
+ index++;
+ }
+ if (!found && !HTTP2ClientProfiles.DEFAULT_PROFILE_ID.equals(selected)) {
+ profileSelector.addItem(new ProfileChoice(selected, selected + " (unresolved)"));
+ selectedIndex = profileSelector.getItemCount() - 1;
+ }
+ if (profileSelector.getItemCount() == 0) {
+ profileSelector.addItem(new ProfileChoice(HTTP2ClientProfiles.DEFAULT_PROFILE_ID,
+ "Browser-like (Most Browsers)"));
+ }
+ profileSelector.setSelectedIndex(selectedIndex);
+ }
+
+ private static final class ProfileChoice {
+ private final String id;
+ private final String label;
+
+ private ProfileChoice(String id, String label) {
+ this.id = id;
+ this.label = label;
+ }
+
+ @Override
+ public String toString() {
+ return label;
+ }
+ }
}
diff --git a/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfilesTest.java b/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfilesTest.java
new file mode 100644
index 0000000..c87187b
--- /dev/null
+++ b/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfilesTest.java
@@ -0,0 +1,128 @@
+package com.blazemeter.jmeter.http2.core;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.blazemeter.jmeter.http2.HTTP2TestBase;
+import com.blazemeter.jmeter.http2.sampler.JMeterTestUtils;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+import org.apache.jmeter.util.JMeterUtils;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class HTTP2ClientProfilesTest extends HTTP2TestBase {
+
+ private static final String PROFILE_PREFIX = "blazemeter.http.profiles.";
+ private Path profileFile;
+
+ @BeforeClass
+ public static void setupClass() {
+ JMeterTestUtils.setupJmeterEnv();
+ }
+
+ @After
+ public void cleanup() throws IOException {
+ Properties properties = JMeterUtils.getJMeterProperties();
+ List