From 25e1b5cfeec6b0c4dee26b1c3bed9fdd6a226ca7 Mon Sep 17 00:00:00 2001 From: Sarthak Babbar Date: Thu, 21 May 2026 13:48:21 +0530 Subject: [PATCH] changes for configuring custom profiles --- README.md | 38 +- .../jmeter/http2/core/HTTP2ClientProfile.java | 218 +++++++ .../http2/core/HTTP2ClientProfileConfig.java | 12 + .../http2/core/HTTP2ClientProfiles.java | 581 ++++++++++++++++++ .../jmeter/http2/core/HTTP2JettyClient.java | 133 +--- .../jmeter/http2/sampler/HTTP2Sampler.java | 56 +- .../http2/sampler/gui/HTTP2SamplerPanel.java | 122 ++-- .../http2/core/HTTP2ClientProfilesTest.java | 128 ++++ .../core/HTTP2JettyClientProfileTest.java | 28 + .../HTTP2SamplerProfileDefaultsTest.java | 35 ++ 10 files changed, 1179 insertions(+), 172 deletions(-) create mode 100644 src/main/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfile.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfiles.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/core/HTTP2ClientProfilesTest.java 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 keys = new ArrayList<>(properties.keySet()); + for (Object key : keys) { + if (String.valueOf(key).startsWith(PROFILE_PREFIX) + || "httpJettyClient.profile".equals(key) + || "blazemeter.http.profile".equals(key)) { + properties.remove(key); + } + } + if (profileFile != null) { + Files.deleteIfExists(profileFile); + profileFile = null; + } + } + + @Test + public void shouldResolveBuiltInBrowserCompatibleProfile() { + HTTP2ClientProfile profile = HTTP2ClientProfiles.resolve("browser-compatible"); + + assertEquals("browser-compatible", profile.getId()); + assertTrue(profile.isEnableHttp3()); + assertTrue(profile.isEnableHttp2()); + assertTrue(profile.isEnableHttp1()); + assertEquals(0L, profile.getHappyEyeballsDelayMs()); + } + + @Test + public void shouldResolveUserProfileWithInheritance() { + set("blazemeter.http.profiles.mobile-h3.label", "Mobile H3 Conservative"); + set("blazemeter.http.profiles.mobile-h3.extends", "browser-like"); + set("blazemeter.http.profiles.mobile-h3.enableHttp3", "false"); + set("blazemeter.http.profiles.mobile-h3.happyEyeballsDelayMs", "500"); + + HTTP2ClientProfile profile = HTTP2ClientProfiles.resolve("mobile-h3"); + + assertEquals("mobile-h3", profile.getId()); + assertEquals("Mobile H3 Conservative", profile.getLabel()); + assertFalse(profile.isEnableHttp3()); + assertTrue(profile.isEnableHttp2()); + assertTrue(profile.isEnableHttp1()); + assertEquals(500L, profile.getHappyEyeballsDelayMs()); + } + + @Test + public void shouldDefaultUserProfilesToBrowserLikeParent() { + set("blazemeter.http.profiles.h2-only.enableHttp1", "false"); + set("blazemeter.http.profiles.h2-only.enableHttp3", "false"); + + HTTP2ClientProfile profile = HTTP2ClientProfiles.resolve("h2-only"); + + assertFalse(profile.isEnableHttp3()); + assertTrue(profile.isEnableHttp2()); + assertFalse(profile.isEnableHttp1()); + assertTrue(profile.isAlpnEnabled()); + } + + @Test + public void shouldExposeUserProfilesInAvailableProfiles() { + set("blazemeter.http.profiles.mobile-h3.label", "Mobile H3 Conservative"); + + boolean found = HTTP2ClientProfiles.availableProfiles().stream() + .anyMatch(profile -> "mobile-h3".equals(profile.getId()) + && "Mobile H3 Conservative".equals(profile.getLabel())); + + assertTrue(found); + } + + @Test + public void shouldLoadFileProfilesAndAllowJmeterOverrides() throws IOException { + profileFile = Files.createTempFile("bzm-http-profiles", ".properties"); + Files.write(profileFile, Arrays.asList( + "blazemeter.http.profiles.file-profile.label=File Profile", + "blazemeter.http.profiles.file-profile.enableHttp3=false")); + set("blazemeter.http.profiles.file", profileFile.toString()); + set("blazemeter.http.profiles.file-profile.enableHttp3", "true"); + + HTTP2ClientProfile profile = HTTP2ClientProfiles.resolve("file-profile"); + + assertEquals("File Profile", profile.getLabel()); + assertTrue(profile.isEnableHttp3()); + } + + @Test + public void shouldFallbackToBrowserLikeWhenProfileIsInvalid() { + set("blazemeter.http.profiles.broken.extends", "missing-profile"); + + HTTP2ClientProfile profile = HTTP2ClientProfiles.resolve("broken"); + + assertEquals(HTTP2ClientProfiles.DEFAULT_PROFILE_ID, profile.getId()); + assertTrue(profile.isEnableHttp3()); + } + + private static void set(String key, String value) { + JMeterUtils.getJMeterProperties().setProperty(key, value); + } +} diff --git a/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClientProfileTest.java b/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClientProfileTest.java index 34bfb62..ed510d6 100644 --- a/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClientProfileTest.java +++ b/src/test/java/com/blazemeter/jmeter/http2/core/HTTP2JettyClientProfileTest.java @@ -6,6 +6,9 @@ import com.blazemeter.jmeter.http2.HTTP2TestBase; import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; import org.apache.jmeter.util.JMeterUtils; import org.junit.After; import org.junit.BeforeClass; @@ -25,6 +28,7 @@ public class HTTP2JettyClientProfileTest extends HTTP2TestBase { private static final String ALT_SVC_CACHE = "httpJettyClient.altSvcCacheEnabled"; private static final String HTTP1_ONLY_CACHE = "httpJettyClient.http1OnlyCacheEnabled"; private static final String H2C_CACHE = "httpJettyClient.h2cCacheEnabled"; + private static final String PROFILE_PREFIX = "blazemeter.http.profiles."; @BeforeClass public static void setupClass() { @@ -44,6 +48,7 @@ public void cleanup() { remove(ALT_SVC_CACHE); remove(HTTP1_ONLY_CACHE); remove(H2C_CACHE); + removeCustomProfiles(); } @Test @@ -82,6 +87,19 @@ public void shouldAllowBrowserLikeCustomOverrides() throws Exception { assertEquals(0L, getLongField(client, "happyEyeballsDelayMs")); } + @Test + public void shouldApplyUserDefinedProfileDefaults() throws Exception { + set("blazemeter.http.profiles.mobile-h3.extends", "browser-like"); + set("blazemeter.http.profiles.mobile-h3.enableHttp3", "false"); + set("blazemeter.http.profiles.mobile-h3.happyEyeballsDelayMs", "500"); + set(PROFILE_PROPERTY, "mobile-h3"); + HTTP2JettyClient client = new HTTP2JettyClient(false, "test"); + + assertFalse(getBooleanField(client, "enableHttp3")); + assertTrue(getBooleanField(client, "enableHttp2")); + assertEquals(500L, getLongField(client, "happyEyeballsDelayMs")); + } + @Test public void shouldHonorDisableFallbackCompatibilityFlag() throws Exception { set(PROFILE_PROPERTY, "browser-like"); @@ -146,6 +164,16 @@ private static void remove(String key) { JMeterUtils.getJMeterProperties().remove(key); } + private static void removeCustomProfiles() { + Properties properties = JMeterUtils.getJMeterProperties(); + List keys = new ArrayList<>(properties.keySet()); + for (Object key : keys) { + if (String.valueOf(key).startsWith(PROFILE_PREFIX)) { + properties.remove(key); + } + } + } + private static boolean getBooleanField(Object target, String fieldName) throws Exception { Object value = getField(target, fieldName); return Boolean.TRUE.equals(value); diff --git a/src/test/java/com/blazemeter/jmeter/http2/sampler/HTTP2SamplerProfileDefaultsTest.java b/src/test/java/com/blazemeter/jmeter/http2/sampler/HTTP2SamplerProfileDefaultsTest.java index 661e9cb..4993535 100644 --- a/src/test/java/com/blazemeter/jmeter/http2/sampler/HTTP2SamplerProfileDefaultsTest.java +++ b/src/test/java/com/blazemeter/jmeter/http2/sampler/HTTP2SamplerProfileDefaultsTest.java @@ -12,7 +12,9 @@ public class HTTP2SamplerProfileDefaultsTest extends HTTP2TestBase { private static final String H2C_UPGRADE_DEFAULT_PROPERTY = "httpJettyClient.h2cUpgradeEnabled"; + private static final String JETTY_PROFILE_PROPERTY = "httpJettyClient.profile"; private static final String HTTP1_UPGRADE_PROPERTY = "HTTP2Sampler.http1_upgrade"; + private static final String PROFILE_PROPERTY = "HTTP2Sampler.profile"; @BeforeClass public static void setupClass() { @@ -22,7 +24,13 @@ public static void setupClass() { @After public void cleanup() { JMeterUtils.getJMeterProperties().remove(H2C_UPGRADE_DEFAULT_PROPERTY); + JMeterUtils.getJMeterProperties().remove(JETTY_PROFILE_PROPERTY); JMeterUtils.getJMeterProperties().remove(HTTP1_UPGRADE_PROPERTY); + JMeterUtils.getJMeterProperties().remove(PROFILE_PROPERTY); + JMeterUtils.getJMeterProperties().remove( + "blazemeter.http.profiles.cleartext-h2.extends"); + JMeterUtils.getJMeterProperties().remove( + "blazemeter.http.profiles.cleartext-h2.h2cUpgradeEnabled"); } @Test @@ -45,4 +53,31 @@ public void shouldDisableUpgradeWhenNoDefaultsAndNoSamplerValue() { HTTP2Sampler sampler = new HTTP2Sampler(); assertFalse(sampler.isHttp1UpgradeEnabled()); } + + @Test + public void shouldUseLegacyProfileH2cUpgradeDefault() { + HTTP2Sampler sampler = new HTTP2Sampler(); + sampler.setProfile("legacy"); + assertTrue(sampler.isHttp1UpgradeEnabled()); + } + + @Test + public void shouldUseGlobalProfileWhenSamplerProfileIsNotSet() { + JMeterUtils.getJMeterProperties().setProperty(JETTY_PROFILE_PROPERTY, "legacy"); + HTTP2Sampler sampler = new HTTP2Sampler(); + + assertTrue(sampler.isHttp1UpgradeEnabled()); + } + + @Test + public void shouldUseUserProfileH2cUpgradeDefault() { + JMeterUtils.getJMeterProperties().setProperty( + "blazemeter.http.profiles.cleartext-h2.extends", "browser-like"); + JMeterUtils.getJMeterProperties().setProperty( + "blazemeter.http.profiles.cleartext-h2.h2cUpgradeEnabled", "true"); + HTTP2Sampler sampler = new HTTP2Sampler(); + sampler.setProfile("cleartext-h2"); + + assertTrue(sampler.isHttp1UpgradeEnabled()); + } }