Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
33bac71
Improvements and alignment to JMeter for SSL KeyStore and TrustStore …
3dgiordano Jul 1, 2026
b19f911
Gracefully handle keystore load failures and reinitialize SSLManager …
3dgiordano Jul 2, 2026
5a31d83
Preserve pre-decoding headers (#107)
3dgiordano Jul 2, 2026
7318437
Preserve connection keep-alive for JMeter compatibility (#116)
3dgiordano Jul 3, 2026
bdbfb14
Handle HPACK errors and set dynamic response header size (#114)
3dgiordano Jul 3, 2026
901be38
Bump org.apache.commons:commons-lang3 from 3.12.0 to 3.18.0 (#104)
dependabot[bot] Jul 7, 2026
9215626
Update Jetty version from 12.1.6 to 12.1.7 (#119)
3dgiordano Jul 7, 2026
e695616
Bump org.assertj:assertj-core from 3.12.2 to 3.27.7 (#105)
dependabot[bot] Jul 7, 2026
ea34d4b
Bump guava from 29.0-jre to 32.0.0-jre (#52)
dependabot[bot] Jul 7, 2026
42ec366
Allow 401 status without requiring HTTP authentication (#120)
3dgiordano Jul 9, 2026
9057a4b
Add UA when default_user_agent_disabled is false (#123)
3dgiordano Jul 10, 2026
bbd856b
Support HEAD method (#122)
3dgiordano Jul 10, 2026
d290ac1
Customize HPACK header validation (#121)
3dgiordano Jul 15, 2026
615da71
Delegate cached-resource sample result to JMeter HTTPHC4Impl (#125)
3dgiordano Jul 16, 2026
5569170
Decode both zlib-wrapped and raw deflate content (#126)
3dgiordano Jul 16, 2026
45b3630
Fix User-Agent is the last request header (#128)
3dgiordano Jul 16, 2026
0f5eab6
Harden HTTP/2 cleartext (h2c) upgrade negotiation and fallback (#127)
3dgiordano Jul 16, 2026
78a7b42
Fix Exception when Cookie is the only request header (#129)
3dgiordano Jul 16, 2026
4d0b181
Effective post-redirect request headers and sentBytes when autoRedire…
3dgiordano Jul 17, 2026
e698229
Catch Cookie header even without a CookieManager (#130)
3dgiordano Jul 17, 2026
69e4ed1
Fix multipart part header casing and raw argument values (#133)
3dgiordano Jul 17, 2026
9b75b84
Load HTTP response parsers lazily (#137)
3dgiordano Jul 17, 2026
b0420af
Support file:// URLs in sampler (#136)
3dgiordano Jul 17, 2026
a99f1c8
Resolve HTTP file arguments via FileServer (#135)
3dgiordano Jul 17, 2026
5446ce4
Fix inverted Content-Type check and gate request-body attachment (#132)
3dgiordano Jul 17, 2026
3462083
Remove duplicate fallback and fix stale body reuse (#131)
3dgiordano Jul 17, 2026
60b9a52
Map Jetty client failures to HttpClient4-equivalent exception types (…
3dgiordano Jul 17, 2026
88e1edd
Add headless CLI tool to migrate stock JMeter HTTP samplers to BlazeM…
3dgiordano Jul 18, 2026
f3a90a4
Default minimum to JMeter to 5.5
3dgiordano Jul 18, 2026
0f7c22a
Matrix JMeter 5.5 and 5.6.3 for compatibility tests
3dgiordano Jul 18, 2026
c8ab244
Revert "Matrix JMeter 5.5 and 5.6.3 for compatibility tests"
3dgiordano Jul 18, 2026
fa17779
Revert "Default minimum to JMeter to 5.5"
3dgiordano Jul 18, 2026
cc82a7c
Set default minimum JMeter version to 5.5 for compatibility tests (#140)
3dgiordano Jul 18, 2026
51c99d1
Cache Test: Restore original settings on exception. (#141)
3dgiordano Jul 18, 2026
16d63d7
RFC 9110 Redirection and Legacy JMeter Redirect (#142)
3dgiordano Jul 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .github/workflows/ci-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# 5.5 is the minimum supported JMeter version, 5.6.3 the latest; both are exercised
# end-to-end (unit + HTTP parity regression tests) since ApacheJMeter_* are provided-scope
# and the same compiled plugin must behave correctly against either at runtime.
jmeter_version: ["5.5", "5.6.3"]
steps:
- name: Checkout
uses: actions/checkout@v5
Expand All @@ -21,14 +28,17 @@ jobs:
java-version: "17"
cache: maven

- name: Build and test
run: xvfb-run -a mvn -U --batch-mode clean install
- name: Build and test (JMeter ${{ matrix.jmeter_version }})
run: >-
xvfb-run -a mvn -U --batch-mode
-Djmeter.version=${{ matrix.jmeter_version }}
clean install

- name: Upload build artifacts
if: always()
uses: actions/upload-artifact@v6
with:
name: ci-build-artifacts-${{ github.run_id }}
name: ci-build-artifacts-${{ matrix.jmeter_version }}-${{ github.run_id }}
path: |
target/jmeter-bzm-http2-*.jar
target/jmeter-test
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/ci-jmeter-compatibility.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,13 @@ jobs:
env:
JMETER_PATH_BASE: ${{ github.workspace }}/.jmeter
run: |
JMETER_VERSION=5.6.3 JMETER_PATH=$JMETER_PATH_BASE/5.6.3 sh testJMeter.sh
# Verify the built plugin jar loads and runs against every JMeter version we claim
# to support: 5.5 (minimum) through 5.6.3 (latest), regardless of which version the
# jar itself was compiled against (jmeter.version in pom.xml is provided-scope).
for JMETER_VERSION in 5.5 5.6.3; do
echo "=== Compatibility check against JMeter $JMETER_VERSION ==="
JMETER_VERSION=$JMETER_VERSION JMETER_PATH=$JMETER_PATH_BASE/$JMETER_VERSION sh testJMeter.sh
done

- name: Upload compatibility artifacts on failure
if: failure()
Expand Down
35 changes: 26 additions & 9 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<jmeter.version>5.4.1</jmeter.version>
<jetty.version>12.1.6</jetty.version>
<!-- Matches jetty-project ${brotli4j.version} for Jetty 12.1.6 -->
<brotli4j.version>1.20.0</brotli4j.version>
<jmeter.version>5.5</jmeter.version>
<jetty.version>12.1.11</jetty.version>
<!-- Matches jetty-project ${brotli4j.version} for Jetty 12.1.11 -->
<brotli4j.version>1.23.0</brotli4j.version>
<checkstyle.skip>false</checkstyle.skip>
</properties>
<dependencies>
Expand All @@ -50,11 +50,11 @@
<scope>provided</scope>
</dependency>
<!-- Apache HttpClient for compilation (classes used in JettyCacheManager) -->
<!-- Using same version as JMeter 5.4.1 -->
<!-- Using same version as JMeter 5.5 (our minimum supported version) -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.14</version>
<version>4.4.15</version>
<scope>compile</scope>
</dependency>
<dependency>
Expand All @@ -66,7 +66,7 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
<version>3.18.0</version>
<scope>compile</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -189,10 +189,27 @@
<version>2.28.2</version>
<scope>test</scope>
</dependency>
<!-- Pin byte-buddy to 1.14.18 to resolve classpath conflict:
mockito-core 2.28.2 pulls byte-buddy ~1.9.x which lacks ElementMatchers.namedOneOf(),
a method required by assertj-core 3.27.7's SoftProxies initialisation.
1.14.x is the last series compatible with the Mockito 2.x instrumentation API
while also satisfying assertj-core 3.27.x. Remove these pins when mockito is upgraded. -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.18</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.14.18</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.12.2</version>
<version>3.27.7</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -275,7 +292,7 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
<version>32.0.0-jre</version>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down
14 changes: 14 additions & 0 deletions scripts/jmx-migrate.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@echo off
rem Migrates stock JMeter HTTP Request samplers to the BlazeMeter HTTP sampler in a .jmx file
rem or directory. Run from a real JMeter installation with jmeter-bzm-http2-*.jar installed
rem under lib\ext\ (this script assumes it lives at <jmeter.home>\bin\jmx-migrate.cmd).
rem
rem Usage: see `jmx-migrate.cmd --help`

setlocal
set SCRIPT_DIR=%~dp0
set JMETER_HOME_DIR=%SCRIPT_DIR%..

java -Djava.awt.headless=true ^
-cp "%JMETER_HOME_DIR%\lib\*;%JMETER_HOME_DIR%\lib\ext\*" ^
com.blazemeter.jmeter.http2.sampler.JmxBlazeMeterHttpMigratorCli %*
13 changes: 13 additions & 0 deletions scripts/jmx-migrate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh
# Migrates stock JMeter HTTP Request samplers to the BlazeMeter HTTP sampler in a .jmx file
# or directory. Run from a real JMeter installation with jmeter-bzm-http2-*.jar installed
# under lib/ext/ (this script assumes it lives at <jmeter.home>/bin/jmx-migrate.sh).
#
# Usage: see `jmx-migrate.sh --help`

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
JMETER_HOME_DIR=$(cd "$SCRIPT_DIR/.." && pwd)

exec java -Djava.awt.headless=true \
-cp "$JMETER_HOME_DIR/lib/*:$JMETER_HOME_DIR/lib/ext/*" \
com.blazemeter.jmeter.http2.sampler.JmxBlazeMeterHttpMigratorCli "$@"
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.blazemeter.jmeter.http2.cli;

import java.io.File;
import java.net.URISyntaxException;
import java.security.CodeSource;
import org.apache.jmeter.util.JMeterUtils;

/**
* Bootstraps a headless JMeter environment for command-line tools shipped inside this plugin's
* jar, without requiring the JMeter GUI or the lightweight test emulator used by unit tests.
*
* <p>Resolves {@code JMETER_HOME} by locating this jar under {@code <jmeter.home>/lib/ext/},
* matching how JMeter loads plugins; falls back to the {@code JMETER_HOME} environment variable
* or the {@code jmeter.home} system property when the jar isn't installed there.
*/
public final class JMeterCliEnvironment {

private static volatile boolean initialized;

private JMeterCliEnvironment() {
}

public static synchronized void ensureInitialized() {
if (initialized) {
return;
}
System.setProperty("java.awt.headless", "true");
if (isNotBlank(JMeterUtils.getJMeterHome())) {
// Some other bootstrap (JMeter GUI/engine, or the in-process test emulator) already
// configured JMeterUtils; reloading properties here would discard that configuration.
initialized = true;
return;
}
String home = resolveJMeterHome();
JMeterUtils.setJMeterHome(home);
File propertiesFile = new File(new File(home, "bin"), "jmeter.properties");
JMeterUtils.loadJMeterProperties(propertiesFile.getAbsolutePath());
JMeterUtils.initLocale();
JMeterUtils.initLogging();
initialized = true;
}

static String resolveJMeterHome() {
String fromProperty = System.getProperty("jmeter.home");
if (isNotBlank(fromProperty)) {
return fromProperty.trim();
}
String fromEnv = System.getenv("JMETER_HOME");
if (isNotBlank(fromEnv)) {
return fromEnv.trim();
}
String fromJarLocation = homeFromExtDirJar(locateOwnJarFile());
if (fromJarLocation != null) {
return fromJarLocation;
}
throw new IllegalStateException(
"Could not determine JMeter home. This tool expects to run from <jmeter.home>/lib/ext/, "
+ "or with -Djmeter.home=<path> / the JMETER_HOME environment variable set.");
}

/**
* @return {@code <home>} when {@code jarFile} looks like {@code <home>/lib/ext/some.jar},
* {@code null} otherwise (e.g. when running from a build output directory).
*/
static String homeFromExtDirJar(File jarFile) {
if (jarFile == null) {
return null;
}
File extDir = jarFile.getParentFile();
if (extDir == null || !"ext".equalsIgnoreCase(extDir.getName())) {
return null;
}
File libDir = extDir.getParentFile();
if (libDir == null || !"lib".equalsIgnoreCase(libDir.getName())) {
return null;
}
File home = libDir.getParentFile();
return home == null ? null : home.getAbsolutePath();
}

private static File locateOwnJarFile() {
try {
CodeSource codeSource = JMeterCliEnvironment.class.getProtectionDomain().getCodeSource();
if (codeSource == null) {
return null;
}
return new File(codeSource.getLocation().toURI());
} catch (URISyntaxException | IllegalArgumentException e) {
return null;
}
}

private static boolean isNotBlank(String value) {
return value != null && !value.trim().isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.IO;

/**
* Jetty's compression module ({@code org.eclipse.jetty.compression.*}) ships gzip, brotli, and
* zstd decoders but no deflate one, so this streams deflate content through {@link Inflater}
* directly. Note this only handles zlib-wrapped deflate (RFC 1950); it is not registered as a
* per-request decoder in {@link HTTP2JettyClient} because retrying as raw/headerless deflate
* (RFC 1951) requires the fully buffered response body - see
* {@link HTTP2JettyClient#decodeDeflate}.
*/
public class DeflateContentDecoderFactory extends ContentDecoder.Factory {

private final ByteBufferPool bufferPool;
Expand Down
Loading